@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,62 @@
1
+ <script lang="ts" setup>
2
+ const props = defineProps<{ open: boolean, entry: LeaveBalance | null }>()
3
+ const emit = defineEmits<{
4
+ confirm: [data: { total: number, note: string }]
5
+ cancel: []
6
+ }>()
7
+
8
+ const { t } = useI18n()
9
+ const total = ref(0)
10
+ const note = ref('')
11
+
12
+ watch(() => props.open, (v) => {
13
+ if (v && props.entry) {
14
+ total.value = props.entry.override_total ?? props.entry.policy_total ?? 0
15
+ note.value = props.entry.override_reason ?? ''
16
+ }
17
+ })
18
+ </script>
19
+
20
+ <template>
21
+ <UModal
22
+ :open="open"
23
+ @update:open="(v) => !v && emit('cancel')"
24
+ >
25
+ <template #content>
26
+ <UCard v-if="entry">
27
+ <div class="space-y-3 text-sm">
28
+ <h3 class="font-semibold">
29
+ {{ $t('hr.leave.override.title') }}
30
+ </h3>
31
+ <div class="text-muted">
32
+ {{ $t('hr.policy.from_policy') }}: <strong>{{ entry.policy_total ?? 'n/a' }} days</strong>
33
+ </div>
34
+ <UFormField :label="$t('hr.leave.override.total')">
35
+ <UInputNumber
36
+ v-model="total"
37
+ :min="0"
38
+ />
39
+ </UFormField>
40
+ <UFormField :label="$t('hr.leave.override.reason')">
41
+ <UTextarea
42
+ v-model="note"
43
+ :rows="2"
44
+ />
45
+ </UFormField>
46
+ <div class="flex justify-end gap-2">
47
+ <UButton
48
+ variant="soft"
49
+ color="neutral"
50
+ @click="emit('cancel')"
51
+ >
52
+ {{ $t('common.cancel') }}
53
+ </UButton>
54
+ <UButton @click="emit('confirm', { total, note })">
55
+ {{ $t('common.save') }}
56
+ </UButton>
57
+ </div>
58
+ </div>
59
+ </UCard>
60
+ </template>
61
+ </UModal>
62
+ </template>
@@ -0,0 +1,185 @@
1
+ <script lang="ts" setup>
2
+ const props = defineProps<{
3
+ personId?: string | number
4
+ prefillDate?: string
5
+ }>()
6
+
7
+ const modelOpen = defineModel<boolean>('open', { default: false })
8
+ const emit = defineEmits<{ created: [] }>()
9
+
10
+ const { t } = useI18n()
11
+ const toast = useToast()
12
+ const { createLeaveRequest, listPeople } = useHrApi()
13
+
14
+ const LEAVE_FIELD_NAMES = ['leave_type_id', 'start_date', 'end_date', 'reason']
15
+ const { pickFields, fetchAll, isReady } = useHrFieldRegistry(['hr_leave_requests'])
16
+ const leaveFields = computed(() => pickFields('hr_leave_requests', LEAVE_FIELD_NAMES))
17
+
18
+ const employees = ref<any[]>([])
19
+
20
+ async function loadEmployees() {
21
+ const res = await listPeople({ stage: 'probation,active,resigned' })
22
+ employees.value = Array.isArray(res?.data) ? res.data : []
23
+ }
24
+
25
+ const employeeOptions = computed(() =>
26
+ employees.value.map((e: any) => ({
27
+ label: e.display_name || e.email || `#${e.id}`,
28
+ value: String(e.id),
29
+ }))
30
+ )
31
+
32
+ const form = ref<Record<string, any>>({
33
+ person_id: props.personId ? String(props.personId) : '',
34
+ leave_type_id: null,
35
+ start_date: props.prefillDate || undefined,
36
+ end_date: props.prefillDate || undefined,
37
+ reason: '',
38
+ })
39
+ const computedDays = ref<number | null>(null)
40
+ const submitting = ref(false)
41
+
42
+ // Compute days client-side (weekdays only, no holiday API in BFF yet)
43
+ function countWorkingDays(start: string, end: string): number {
44
+ if (!start || !end) return 0
45
+ let count = 0
46
+ const d = new Date(start)
47
+ const e = new Date(end)
48
+ while (d <= e) {
49
+ const dow = d.getDay()
50
+ if (dow !== 0 && dow !== 6) count++
51
+ d.setDate(d.getDate() + 1)
52
+ }
53
+ return count
54
+ }
55
+
56
+ watch([() => form.value.start_date, () => form.value.end_date], ([s, e]) => {
57
+ if (s && e && e >= s) {
58
+ computedDays.value = countWorkingDays(s, e)
59
+ }
60
+ else {
61
+ computedDays.value = null
62
+ }
63
+ })
64
+
65
+ function idOf(value: any): any {
66
+ return value && typeof value === 'object' ? value.id : value
67
+ }
68
+
69
+ const canSubmit = computed(() =>
70
+ idOf(form.value.leave_type_id) != null &&
71
+ form.value.start_date &&
72
+ form.value.end_date &&
73
+ form.value.end_date >= form.value.start_date &&
74
+ (props.personId || form.value.person_id)
75
+ )
76
+
77
+ async function submit() {
78
+ if (!canSubmit.value) return
79
+ submitting.value = true
80
+ try {
81
+ const personId = props.personId ?? idOf(form.value.person_id)
82
+ await createLeaveRequest(personId, {
83
+ leave_type_id: idOf(form.value.leave_type_id),
84
+ start_date: form.value.start_date,
85
+ end_date: form.value.end_date,
86
+ reason: form.value.reason || undefined,
87
+ })
88
+ toast.add({ title: t('hr.leave.request.submitted'), color: 'success' })
89
+ modelOpen.value = false
90
+ emit('created')
91
+ resetForm()
92
+ }
93
+ catch (err: any) {
94
+ toast.add({ title: err.message ?? t('hr.toast.error'), color: 'error' })
95
+ }
96
+ finally {
97
+ submitting.value = false
98
+ }
99
+ }
100
+
101
+ function resetForm() {
102
+ form.value = {
103
+ person_id: props.personId ? String(props.personId) : '',
104
+ leave_type_id: null,
105
+ start_date: undefined,
106
+ end_date: undefined,
107
+ reason: '',
108
+ }
109
+ computedDays.value = null
110
+ }
111
+
112
+ watch(modelOpen, (v) => {
113
+ if (v) {
114
+ fetchAll()
115
+ if (props.personId) form.value.person_id = String(props.personId)
116
+ else loadEmployees()
117
+ }
118
+ })
119
+ </script>
120
+
121
+ <template>
122
+ <UModal v-model:open="modelOpen" :ui="{ content: 'sm:max-w-2xl' }">
123
+ <template #content>
124
+ <UCard>
125
+ <div class="space-y-4">
126
+ <h3 class="font-semibold text-base">
127
+ {{ $t('hr.leave.request.title') }}
128
+ </h3>
129
+
130
+ <UFormField
131
+ v-if="!personId"
132
+ :label="$t('hr.leave.request.person')"
133
+ >
134
+ <USelectMenu
135
+ v-model="form.person_id"
136
+ :items="employeeOptions"
137
+ value-key="value"
138
+ label-key="label"
139
+ :placeholder="$t('hr.leave.request.person')"
140
+ class="w-full"
141
+ />
142
+ </UFormField>
143
+
144
+ <div v-if="!isReady('hr_leave_requests')" class="py-4 text-sm text-muted text-center">
145
+ {{ $t('common.loading') }}
146
+ </div>
147
+ <FormRoot
148
+ v-else-if="leaveFields.length"
149
+ v-model="form"
150
+ :fields="leaveFields"
151
+ />
152
+
153
+ <div
154
+ v-if="computedDays !== null"
155
+ class="flex items-center gap-2 text-sm text-muted bg-elevated/50 rounded-lg px-3 py-2"
156
+ >
157
+ <UIcon
158
+ name="i-ph-info-light"
159
+ class="size-4 text-primary"
160
+ />
161
+ <span>{{ $t('hr.leave.request.days_count', { count: computedDays }) }}</span>
162
+ <span class="text-xs">— {{ $t('hr.leave.request.days_info') }}</span>
163
+ </div>
164
+
165
+ <div class="flex justify-end gap-2 pt-2">
166
+ <UButton
167
+ variant="soft"
168
+ color="neutral"
169
+ @click="modelOpen = false"
170
+ >
171
+ {{ $t('common.cancel') }}
172
+ </UButton>
173
+ <UButton
174
+ :loading="submitting"
175
+ :disabled="!canSubmit"
176
+ @click="submit"
177
+ >
178
+ {{ $t('hr.leave.request.submit') }}
179
+ </UButton>
180
+ </div>
181
+ </div>
182
+ </UCard>
183
+ </template>
184
+ </UModal>
185
+ </template>
@@ -0,0 +1,289 @@
1
+ <template>
2
+ <div class="flex flex-col h-full">
3
+ <!-- Stats bar -->
4
+ <div class="grid grid-cols-4 gap-4 px-4 py-4 border-b border-default">
5
+ <div
6
+ v-for="stat in leaveStats"
7
+ :key="stat.label"
8
+ class="flex items-center gap-3 p-3 bg-elevated/50 rounded-lg"
9
+ >
10
+ <div
11
+ class="p-2 rounded-lg"
12
+ :class="stat.bgClass"
13
+ >
14
+ <UIcon
15
+ :name="stat.icon"
16
+ class="size-4"
17
+ :class="stat.iconClass"
18
+ />
19
+ </div>
20
+ <div>
21
+ <div class="text-lg font-bold text-default">
22
+ {{ stat.value }}
23
+ </div>
24
+ <div class="text-xs text-muted">
25
+ {{ stat.label }}
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </div>
30
+
31
+ <!-- Toolbar -->
32
+ <div class="flex items-center gap-3 py-3 border-b border-default">
33
+ <div class="flex items-center gap-1 bg-elevated rounded-lg p-0.5">
34
+ <button
35
+ v-for="tab in statusTabs"
36
+ :key="tab.value"
37
+ class="px-3 py-1.5 text-xs font-medium rounded-md transition-colors cursor-pointer"
38
+ :class="activeFilter === tab.value
39
+ ? 'bg-default text-default shadow-sm'
40
+ : 'text-muted hover:text-default'"
41
+ @click="activeFilter = tab.value"
42
+ >
43
+ {{ tab.label }}
44
+ <UBadge
45
+ :label="String(tab.count)"
46
+ variant="subtle"
47
+ size="md"
48
+ class="ml-1"
49
+ />
50
+ </button>
51
+ </div>
52
+ <div class="flex-1" />
53
+ <UButton
54
+ icon="i-ph-plus"
55
+ :label="$t('hr.leave.actions.create')"
56
+ color="primary"
57
+ size="sm"
58
+ @click="showRequestModal = true"
59
+ />
60
+ </div>
61
+
62
+ <!-- Table -->
63
+ <div class="flex-1 overflow-auto">
64
+ <table class="w-full text-sm">
65
+ <thead class="bg-elevated/50 sticky top-0 z-10">
66
+ <tr class="text-left text-xs text-muted uppercase tracking-wider">
67
+ <th class="px-4 py-2.5 font-medium">{{ $t('hr.leave.table.employee') }}</th>
68
+ <th class="px-4 py-2.5 font-medium">{{ $t('hr.leave.table.type') }}</th>
69
+ <th class="px-4 py-2.5 font-medium">{{ $t('hr.leave.table.dates') }}</th>
70
+ <th class="px-4 py-2.5 font-medium">{{ $t('hr.leave.table.days') }}</th>
71
+ <th class="px-4 py-2.5 font-medium">{{ $t('hr.leave.table.status') }}</th>
72
+ <th class="px-4 py-2.5 font-medium text-right">{{ $t('hr.leave.table.actions') }}</th>
73
+ </tr>
74
+ </thead>
75
+ <tbody class="divide-y divide-default">
76
+ <tr
77
+ v-for="req in filteredRequests"
78
+ :key="req.id"
79
+ class="hover:bg-elevated/50 transition-colors"
80
+ >
81
+ <td class="px-4 py-3">
82
+ <div class="flex items-center gap-2">
83
+ <UAvatar
84
+ icon="i-ph-user-light"
85
+ size="xs"
86
+ class="bg-primary/10 text-primary"
87
+ />
88
+ <span class="font-medium text-default">{{ personName(req) }}</span>
89
+ </div>
90
+ </td>
91
+ <td class="px-4 py-3">
92
+ <UBadge
93
+ :label="leaveTypeName(req)"
94
+ :color="leaveTypeColor(req)"
95
+ variant="subtle"
96
+ size="md"
97
+ />
98
+ </td>
99
+ <td class="px-4 py-3 text-muted text-xs">
100
+ {{ fmtDate(req.start_date || req.from_date) }} → {{ fmtDate(req.end_date || req.to_date) }}
101
+ </td>
102
+ <td class="px-4 py-3 text-default font-medium">{{ req.days_count ?? req.days }}</td>
103
+ <td class="px-4 py-3">
104
+ <UBadge
105
+ :label="$t(`hr.leave.status.${req.status.toLowerCase()}`)"
106
+ :color="statusColor(req.status)"
107
+ variant="subtle"
108
+ size="md"
109
+ />
110
+ </td>
111
+ <td class="px-4 py-3 text-right">
112
+ <div
113
+ v-if="canApprove && req.status?.toLowerCase() === 'pending'"
114
+ class="flex items-center gap-1.5 justify-end"
115
+ >
116
+ <UButton
117
+ icon="i-ph-check-light"
118
+ color="success"
119
+ size="xs"
120
+ :label="$t('hr.leave.actions.approve')"
121
+ @click="handleApprove(req)"
122
+ />
123
+ <UButton
124
+ icon="i-ph-x-light"
125
+ color="error"
126
+ variant="outline"
127
+ size="xs"
128
+ :label="$t('hr.leave.actions.reject')"
129
+ @click="handleReject(req)"
130
+ />
131
+ </div>
132
+ <UButton
133
+ v-else-if="canApprove && req.status?.toLowerCase() === 'approved'"
134
+ icon="i-ph-arrow-counter-clockwise-light"
135
+ color="neutral"
136
+ variant="ghost"
137
+ size="xs"
138
+ :label="$t('hr.leave.status.pending')"
139
+ @click="setStatus(req.id, 'pending' as any)"
140
+ />
141
+ </td>
142
+ </tr>
143
+ <tr v-if="filteredRequests.length === 0">
144
+ <td
145
+ colspan="6"
146
+ class="px-4 py-16 text-center text-sm text-muted"
147
+ >
148
+ {{ $t('common.no_data') }}
149
+ </td>
150
+ </tr>
151
+ </tbody>
152
+ </table>
153
+ </div>
154
+
155
+ <div class="flex items-center justify-between px-4 py-2.5 border-t border-default text-xs text-muted">
156
+ <span>{{ filteredRequests.length }} {{ $t('hr.leave.views.list').toLowerCase() }}</span>
157
+ </div>
158
+
159
+ <!-- Reject reason modal -->
160
+ <UModal v-model:open="rejectModalOpen">
161
+ <template #content>
162
+ <UCard>
163
+ <div class="space-y-3">
164
+ <h3 class="font-semibold">{{ $t('hr.leave.reject_modal.title') }}</h3>
165
+ <UFormField :label="$t('hr.leave.reject_modal.reason')">
166
+ <UTextarea
167
+ v-model="rejectReason"
168
+ :rows="3"
169
+ class="w-full"
170
+ />
171
+ </UFormField>
172
+ <div class="flex justify-end gap-2">
173
+ <UButton
174
+ variant="soft"
175
+ color="neutral"
176
+ @click="rejectModalOpen = false"
177
+ >
178
+ {{ $t('common.cancel') }}
179
+ </UButton>
180
+ <UButton
181
+ color="error"
182
+ @click="confirmReject"
183
+ >
184
+ {{ $t('hr.leave.actions.reject') }}
185
+ </UButton>
186
+ </div>
187
+ </div>
188
+ </UCard>
189
+ </template>
190
+ </UModal>
191
+
192
+ <!-- New request modal -->
193
+ <HrLeaveRequestModal
194
+ v-model:open="showRequestModal"
195
+ @created="onRequestCreated"
196
+ />
197
+ </div>
198
+ </template>
199
+
200
+ <script lang="ts" setup>
201
+ const { t } = useI18n()
202
+ const toast = useToast()
203
+ const { can } = usePermissionRegistry()
204
+ const canApprove = computed(() => can('hr', 'leave.approve') || can('hr', 'leave.approve.dept'))
205
+ const { leaveRequests, pendingLeave, approvedLeave, rejectedLeave, totalDaysUsed, setStatus, fetchRequests } = useLeave()
206
+
207
+ const activeFilter = useState<'all' | 'pending' | 'approved' | 'rejected'>('leave.filter', () => 'pending')
208
+ const rejectModalOpen = ref(false)
209
+ const rejectReason = ref('')
210
+ const pendingRejectId = ref<string | number | null>(null)
211
+ const showRequestModal = ref(false)
212
+
213
+ const leaveStats = computed(() => [
214
+ { label: t('hr.leave.stats.pending'), value: String(pendingLeave.value.length), icon: 'i-ph-clock-light', bgClass: 'bg-warning/10', iconClass: 'text-warning' },
215
+ { label: t('hr.leave.stats.approved'), value: String(approvedLeave.value.length), icon: 'i-ph-check-circle-light', bgClass: 'bg-success/10', iconClass: 'text-success' },
216
+ { label: t('hr.leave.stats.rejected'), value: String(rejectedLeave.value.length), icon: 'i-ph-x-circle-light', bgClass: 'bg-error/10', iconClass: 'text-error' },
217
+ { label: t('hr.leave.stats.total_days'), value: String(totalDaysUsed.value), icon: 'i-ph-chart-bar-light', bgClass: 'bg-primary/10', iconClass: 'text-primary' },
218
+ ])
219
+
220
+ const statusTabs = computed(() => [
221
+ { label: t('hr.leave.status.pending'), value: 'pending' as const, count: pendingLeave.value.length },
222
+ { label: t('hr.leave.status.approved'), value: 'approved' as const, count: approvedLeave.value.length },
223
+ { label: t('hr.leave.status.rejected'), value: 'rejected' as const, count: rejectedLeave.value.length },
224
+ { label: t('common.all'), value: 'all' as const, count: leaveRequests.value.length },
225
+ ])
226
+
227
+ const filteredRequests = computed(() => {
228
+ if (activeFilter.value === 'all') return leaveRequests.value
229
+ return leaveRequests.value.filter(r => r.status?.toLowerCase() === activeFilter.value)
230
+ })
231
+
232
+ function personName(req: LeaveRequest): string {
233
+ if (req.person_name) return req.person_name
234
+ const pid = req.person_id as any
235
+ if (pid && typeof pid === 'object') return pid.display_name ?? pid.full_name ?? (pid.id ? `#${pid.id}` : '—')
236
+ return pid ? `#${pid}` : '—'
237
+ }
238
+
239
+ function leaveTypeName(req: LeaveRequest): string {
240
+ if (req.leave_type) return req.leave_type
241
+ const lt = req.leave_type_id as any
242
+ if (lt && typeof lt === 'object') return lt.name ?? lt.label ?? '—'
243
+ return '—'
244
+ }
245
+
246
+ function fmtDate(value?: string): string {
247
+ if (!value) return '—'
248
+ const d = new Date(value)
249
+ if (Number.isNaN(d.getTime())) return value
250
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
251
+ }
252
+
253
+ const TYPE_COLOR: Record<string, string> = { annual: 'primary', sick: 'error', personal: 'warning', remote: 'info' }
254
+ function leaveTypeColor(req: LeaveRequest): string {
255
+ const name = leaveTypeName(req).toLowerCase()
256
+ return TYPE_COLOR[name] ?? 'neutral'
257
+ }
258
+
259
+ function statusColor(status: string): string {
260
+ const s = status?.toLowerCase()
261
+ if (s === 'approved') return 'success'
262
+ if (s === 'pending') return 'warning'
263
+ return 'error'
264
+ }
265
+
266
+ async function handleApprove(req: LeaveRequest) {
267
+ await setStatus(req.id, 'approved' as any)
268
+ toast.add({ title: t('hr.toast.approved'), color: 'success' })
269
+ }
270
+
271
+ function handleReject(req: LeaveRequest) {
272
+ pendingRejectId.value = req.id
273
+ rejectReason.value = ''
274
+ rejectModalOpen.value = true
275
+ }
276
+
277
+ async function confirmReject() {
278
+ if (!pendingRejectId.value) return
279
+ await setStatus(pendingRejectId.value, 'rejected' as any, rejectReason.value || undefined)
280
+ toast.add({ title: t('hr.toast.rejected'), color: 'warning' })
281
+ rejectModalOpen.value = false
282
+ pendingRejectId.value = null
283
+ }
284
+
285
+ async function onRequestCreated() {
286
+ showRequestModal.value = false
287
+ await fetchRequests()
288
+ }
289
+ </script>