@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.
- package/app/components/hr/contact/tab.vue +238 -0
- package/app/components/hr/department/card.vue +141 -0
- package/app/components/hr/department/form-modal.vue +90 -0
- package/app/components/hr/employees/assets/tab.vue +432 -0
- package/app/components/hr/employees/compensation.vue +136 -0
- package/app/components/hr/employees/contract.vue +77 -0
- package/app/components/hr/employees/insurance.vue +164 -0
- package/app/components/hr/employees/leave/tab.vue +180 -0
- package/app/components/hr/employees/provisioning/form-modal.vue +219 -0
- package/app/components/hr/employees/provisioning/tab.vue +187 -0
- package/app/components/hr/employees/tab.vue +38 -0
- package/app/components/hr/leave/calendar-tab.vue +649 -0
- package/app/components/hr/leave/override-modal.vue +62 -0
- package/app/components/hr/leave/request-modal.vue +185 -0
- package/app/components/hr/leave/requests-tab.vue +289 -0
- package/app/components/hr/leave/timeline-tab.vue +259 -0
- package/app/components/hr/offboarding/tab.vue +303 -0
- package/app/components/hr/person/activity/tab.vue +65 -0
- package/app/components/hr/person/activity/timeline.vue +119 -0
- package/app/components/hr/person/detail.vue +303 -0
- package/app/components/hr/person/document/tab-documents.vue +120 -0
- package/app/components/hr/person/document/template-edit-drawer.vue +215 -0
- package/app/components/hr/person/document/template-preview-card.vue +39 -0
- package/app/components/hr/person/document/trigger-modal.vue +121 -0
- package/app/components/hr/person/employee-form-modal.vue +78 -0
- package/app/components/hr/person/form-modal.vue +78 -0
- package/app/components/hr/person/list-row.vue +40 -0
- package/app/components/hr/person/profile/tab.vue +231 -0
- package/app/components/hr/settings/automation.vue +113 -0
- package/app/components/hr/settings/documents.vue +200 -0
- package/app/components/hr/settings/general.vue +87 -0
- package/app/components/hr/settings/holidays.vue +171 -0
- package/app/components/hr/settings/integrations.vue +185 -0
- package/app/components/hr/settings/policies.vue +83 -0
- package/app/components/hr/settings/policy/benefit-override-modal.vue +59 -0
- package/app/components/hr/settings/policy/editor-eligibility.vue +27 -0
- package/app/components/hr/settings/policy/editor-leave-base.vue +37 -0
- package/app/components/hr/settings/policy/editor-tenure-bonus.vue +61 -0
- package/app/components/hr/settings/recruitment.vue +128 -0
- package/app/components/hr/settings/taxonomies.vue +170 -0
- package/app/components/hr/shared/row.vue +21 -0
- package/app/components/hr/shared/section.vue +20 -0
- package/app/components/hr/shared/source-badge.vue +42 -0
- package/app/components/hr/shared/stage-badge.vue +24 -0
- package/app/components/hr/shared/workflow-timeline.vue +27 -0
- package/app/components/hr/talents/app-sidebar.vue +54 -0
- package/app/components/hr/talents/application-form-modal.vue +114 -0
- package/app/components/hr/talents/pipeline-picker.vue +56 -0
- package/app/components/hr/talents/step-detail.vue +133 -0
- package/app/components/hr/talents/step-stepper.vue +85 -0
- package/app/components/hr/talents/tab.vue +263 -0
- package/app/composables/use-departments.ts +59 -0
- package/app/composables/use-employee-detail.ts +24 -0
- package/app/composables/use-holidays.ts +48 -0
- package/app/composables/use-hr-api.ts +210 -0
- package/app/composables/use-hr-field-registry.ts +76 -0
- package/app/composables/use-hr-policies.ts +66 -0
- package/app/composables/use-hr-settings.ts +118 -0
- package/app/composables/use-leave.ts +71 -0
- package/app/composables/use-offboarding.ts +49 -0
- package/app/composables/use-people.ts +149 -0
- package/app/composables/use-providers.ts +44 -0
- package/app/composables/use-recruitment-workflow.ts +173 -0
- package/app/composables/use-templates.ts +44 -0
- package/app/composables/use-triggers.ts +26 -0
- package/app/config/column-renderers.ts +4 -0
- package/app/config/form-layouts.ts +193 -0
- package/app/data/hr-schema.ts +2608 -0
- package/app/lib/policy-engine.ts +116 -0
- package/app/pages/hr/departments.vue +114 -0
- package/app/pages/hr/employees/[id]/activity.vue +10 -0
- package/app/pages/hr/employees/[id]/assets.vue +14 -0
- package/app/pages/hr/employees/[id]/employment.vue +14 -0
- package/app/pages/hr/employees/[id]/index.vue +9 -0
- package/app/pages/hr/employees/[id]/offboarding.vue +7 -0
- package/app/pages/hr/employees/[id]/profile.vue +11 -0
- package/app/pages/hr/employees/[id]/provisioning.vue +17 -0
- package/app/pages/hr/employees/[id].vue +313 -0
- package/app/pages/hr/employees/index.vue +291 -0
- package/app/pages/hr/index.vue +3 -0
- package/app/pages/hr/leave.vue +79 -0
- package/app/pages/hr/settings.vue +43 -0
- package/app/pages/hr/setup.vue +3 -0
- package/app/pages/hr/talents/[id]/interview/[stepId].vue +231 -0
- package/app/pages/hr/talents/[id].vue +52 -0
- package/app/pages/hr/talents/index.vue +224 -0
- package/app/pages/hr.vue +129 -0
- package/app/plugins/hr-contacts-sync.client.ts +3 -0
- package/app/plugins/hr-extensions.ts +36 -0
- package/app/plugins/hr-setup.ts +5 -0
- package/app/plugins/navigations.ts +22 -0
- package/app/utils/hr-permissions.ts +27 -0
- package/app/utils/hr-policy-seed-step.ts +110 -0
- package/i18n/locales/en.json +726 -0
- package/i18n/locales/vi.json +688 -0
- package/nuxt.config.ts +19 -0
- package/package.json +27 -0
- package/server/api/hr/departments/[id].delete.ts +12 -0
- package/server/api/hr/departments/[id].patch.ts +14 -0
- package/server/api/hr/departments/index.get.ts +11 -0
- package/server/api/hr/departments/index.post.ts +13 -0
- package/server/api/hr/documents/templates/[id]/preview.post.ts +16 -0
- package/server/api/hr/documents/templates/[id].delete.ts +14 -0
- package/server/api/hr/documents/templates/[id].patch.ts +16 -0
- package/server/api/hr/documents/templates/index.get.ts +15 -0
- package/server/api/hr/documents/templates/index.post.ts +15 -0
- package/server/api/hr/documents/triggers/[id].patch.ts +16 -0
- package/server/api/hr/documents/triggers/index.get.ts +13 -0
- package/server/api/hr/fields/[collection].get.ts +14 -0
- package/server/api/hr/holidays/[id].delete.ts +14 -0
- package/server/api/hr/holidays/[id].patch.ts +16 -0
- package/server/api/hr/holidays/copy.post.ts +15 -0
- package/server/api/hr/holidays/index.get.ts +15 -0
- package/server/api/hr/holidays/index.post.ts +15 -0
- package/server/api/hr/leave/requests/[id].patch.ts +22 -0
- package/server/api/hr/leave/requests.get.ts +15 -0
- package/server/api/hr/leave/types.get.ts +13 -0
- package/server/api/hr/offboarding/[id]/cancel.post.ts +8 -0
- package/server/api/hr/offboarding/[id]/deprovision.post.ts +8 -0
- package/server/api/hr/offboarding/[id]/finalize.post.ts +8 -0
- package/server/api/hr/offboarding/[id]/return-assets.post.ts +8 -0
- package/server/api/hr/offboarding/[id]/settlement.get.ts +8 -0
- package/server/api/hr/offboarding/[id]/tasks/[taskId].patch.ts +10 -0
- package/server/api/hr/offboarding/[id].get.ts +8 -0
- package/server/api/hr/offboarding/[id].patch.ts +9 -0
- package/server/api/hr/offboarding/index.get.ts +7 -0
- package/server/api/hr/people/[id]/applications/[appId]/interviews/[iid].delete.ts +16 -0
- package/server/api/hr/people/[id]/applications/[appId]/interviews/[iid].patch.ts +18 -0
- package/server/api/hr/people/[id]/applications/[appId]/interviews/index.get.ts +15 -0
- package/server/api/hr/people/[id]/applications/[appId]/interviews/index.post.ts +17 -0
- package/server/api/hr/people/[id]/applications/[appId].patch.ts +17 -0
- package/server/api/hr/people/[id]/applications/index.get.ts +14 -0
- package/server/api/hr/people/[id]/applications/index.post.ts +16 -0
- package/server/api/hr/people/[id]/assets/[aid].delete.ts +13 -0
- package/server/api/hr/people/[id]/assets/[aid].patch.ts +15 -0
- package/server/api/hr/people/[id]/assets/index.get.ts +12 -0
- package/server/api/hr/people/[id]/assets/index.post.ts +14 -0
- package/server/api/hr/people/[id]/compensations.get.ts +14 -0
- package/server/api/hr/people/[id]/compensations.patch.ts +16 -0
- package/server/api/hr/people/[id]/contracts.get.ts +14 -0
- package/server/api/hr/people/[id]/contracts.patch.ts +16 -0
- package/server/api/hr/people/[id]/documents/[did].delete.ts +15 -0
- package/server/api/hr/people/[id]/documents/index.get.ts +14 -0
- package/server/api/hr/people/[id]/documents/index.post.ts +16 -0
- package/server/api/hr/people/[id]/insurances.get.ts +14 -0
- package/server/api/hr/people/[id]/insurances.patch.ts +16 -0
- package/server/api/hr/people/[id]/leave-balances/[bid].patch.ts +17 -0
- package/server/api/hr/people/[id]/leave-balances/index.get.ts +14 -0
- package/server/api/hr/people/[id]/leave-requests/index.get.ts +14 -0
- package/server/api/hr/people/[id]/leave-requests/index.post.ts +16 -0
- package/server/api/hr/people/[id]/link-user.post.ts +16 -0
- package/server/api/hr/people/[id]/notes/[nid].delete.ts +15 -0
- package/server/api/hr/people/[id]/notes/index.get.ts +14 -0
- package/server/api/hr/people/[id]/notes/index.post.ts +16 -0
- package/server/api/hr/people/[id]/offboarding/cases.get.ts +12 -0
- package/server/api/hr/people/[id]/offboarding.get.ts +12 -0
- package/server/api/hr/people/[id]/offboarding.post.ts +14 -0
- package/server/api/hr/people/[id]/provisioning/[logId]/retry.post.ts +7 -0
- package/server/api/hr/people/[id]/provisioning/index.get.ts +6 -0
- package/server/api/hr/people/[id]/provisioning/index.post.ts +7 -0
- package/server/api/hr/people/[id]/transition.post.ts +19 -0
- package/server/api/hr/people/[id]/transitions.get.ts +14 -0
- package/server/api/hr/people/[id].delete.ts +15 -0
- package/server/api/hr/people/[id].get.ts +14 -0
- package/server/api/hr/people/[id].patch.ts +17 -0
- package/server/api/hr/people/index.get.ts +15 -0
- package/server/api/hr/people/index.post.ts +19 -0
- package/server/api/hr/policies/[id].patch.ts +16 -0
- package/server/api/hr/policies/index.get.ts +13 -0
- package/server/api/hr/providers/[id]/test.post.ts +6 -0
- package/server/api/hr/providers/[id].delete.ts +6 -0
- package/server/api/hr/providers/[id].patch.ts +7 -0
- package/server/api/hr/providers/index.get.ts +5 -0
- package/server/api/hr/providers/index.post.ts +6 -0
- package/server/api/hr/settings/employment-types/[id].delete.ts +14 -0
- package/server/api/hr/settings/employment-types/[id].patch.ts +16 -0
- package/server/api/hr/settings/employment-types/index.get.ts +13 -0
- package/server/api/hr/settings/employment-types/index.post.ts +15 -0
- package/server/api/hr/settings/index.get.ts +13 -0
- package/server/api/hr/settings/index.patch.ts +15 -0
- package/server/api/hr/settings/leave-types/[id].delete.ts +14 -0
- package/server/api/hr/settings/leave-types/[id].patch.ts +16 -0
- package/server/api/hr/settings/leave-types/index.get.ts +13 -0
- package/server/api/hr/settings/leave-types/index.post.ts +15 -0
- package/shared/types/form-layout.ts +30 -0
- package/shared/types/index.ts +2 -0
- package/shared/types/integration.ts +41 -0
- package/shared/types/leave.ts +53 -0
- package/shared/types/offboarding.ts +46 -0
- package/shared/types/person.ts +54 -0
- package/shared/types/settings.ts +16 -0
- package/shared/utils/template-render.ts +155 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-col h-full">
|
|
3
|
+
<!-- Toolbar -->
|
|
4
|
+
<div class="flex items-center gap-2 px-4 py-3 border-b border-default">
|
|
5
|
+
<UButton
|
|
6
|
+
icon="i-ph-caret-left"
|
|
7
|
+
size="sm"
|
|
8
|
+
variant="ghost"
|
|
9
|
+
@click="prevMonth"
|
|
10
|
+
/>
|
|
11
|
+
<UButton
|
|
12
|
+
:label="$t('calendar.today')"
|
|
13
|
+
size="sm"
|
|
14
|
+
variant="outline"
|
|
15
|
+
@click="goToday"
|
|
16
|
+
/>
|
|
17
|
+
<UButton
|
|
18
|
+
icon="i-ph-caret-right"
|
|
19
|
+
size="sm"
|
|
20
|
+
variant="ghost"
|
|
21
|
+
@click="nextMonth"
|
|
22
|
+
/>
|
|
23
|
+
<span class="ml-2 text-base font-semibold">{{ rangeLabel }}</span>
|
|
24
|
+
<div class="flex-1" />
|
|
25
|
+
<UInput
|
|
26
|
+
v-model="searchQuery"
|
|
27
|
+
icon="i-ph-magnifying-glass"
|
|
28
|
+
:placeholder="$t('common.search')"
|
|
29
|
+
size="sm"
|
|
30
|
+
class="w-56"
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<!-- Legend -->
|
|
35
|
+
<div class="flex items-center gap-4 text-xs text-muted px-4 py-2 border-b border-default">
|
|
36
|
+
<span class="flex items-center gap-1.5">
|
|
37
|
+
<span class="size-2.5 rounded-sm bg-success/60" />
|
|
38
|
+
{{ $t('hr.leave.status.approved') }}
|
|
39
|
+
</span>
|
|
40
|
+
<span class="flex items-center gap-1.5">
|
|
41
|
+
<span class="size-2.5 rounded-sm border border-dashed border-warning bg-warning/20" />
|
|
42
|
+
{{ $t('hr.leave.status.pending') }}
|
|
43
|
+
</span>
|
|
44
|
+
<span class="flex items-center gap-1.5">
|
|
45
|
+
<span class="size-2.5 rounded-sm bg-error/30" />
|
|
46
|
+
{{ $t('holidays.title') }}
|
|
47
|
+
</span>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- Gantt grid -->
|
|
51
|
+
<div class="flex-1 overflow-auto">
|
|
52
|
+
<div
|
|
53
|
+
class="grid"
|
|
54
|
+
:style="gridStyle"
|
|
55
|
+
>
|
|
56
|
+
<!-- Header row -->
|
|
57
|
+
<div class="sticky left-0 top-0 z-20 bg-default border-b border-r border-default px-4 py-2.5 flex items-center">
|
|
58
|
+
<span class="text-xs uppercase tracking-wider text-muted font-medium">
|
|
59
|
+
{{ activeEmployees.length }} members
|
|
60
|
+
</span>
|
|
61
|
+
</div>
|
|
62
|
+
<div
|
|
63
|
+
v-for="day in days"
|
|
64
|
+
:key="day.iso"
|
|
65
|
+
class="sticky top-0 z-10 bg-default border-b border-r border-default text-center py-1.5 last:border-r-0 min-w-[32px]"
|
|
66
|
+
:class="[
|
|
67
|
+
day.isWeekend && 'bg-elevated/40',
|
|
68
|
+
day.isToday && 'bg-primary/10',
|
|
69
|
+
day.isHoliday && !day.isWeekend && 'bg-error/10',
|
|
70
|
+
]"
|
|
71
|
+
:title="day.holidayName"
|
|
72
|
+
>
|
|
73
|
+
<div
|
|
74
|
+
class="text-xs font-medium leading-tight"
|
|
75
|
+
:class="day.isToday ? 'text-primary' : day.isHoliday ? 'text-error' : ''"
|
|
76
|
+
>{{ day.d }}</div>
|
|
77
|
+
<div class="text-[9px] text-muted leading-tight">{{ day.dow }}</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<!-- Employee rows -->
|
|
81
|
+
<template
|
|
82
|
+
v-for="emp in activeEmployees"
|
|
83
|
+
:key="emp.id"
|
|
84
|
+
>
|
|
85
|
+
<div class="sticky left-0 z-10 bg-default border-b border-r border-default px-4 py-3 flex items-center gap-3 min-w-0 min-w-[220px]">
|
|
86
|
+
<UAvatar
|
|
87
|
+
icon="i-ph-user-light"
|
|
88
|
+
size="sm"
|
|
89
|
+
class="bg-primary/10 text-primary shrink-0"
|
|
90
|
+
/>
|
|
91
|
+
<div class="min-w-0">
|
|
92
|
+
<div class="text-sm font-medium truncate">{{ emp.display_name }}</div>
|
|
93
|
+
<div class="text-xs text-muted truncate">{{ emp.job_title }}</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
<div
|
|
97
|
+
class="relative border-b border-default"
|
|
98
|
+
:style="{ gridColumn: `2 / span ${days.length}` }"
|
|
99
|
+
>
|
|
100
|
+
<!-- Day cells -->
|
|
101
|
+
<div
|
|
102
|
+
class="grid h-full absolute inset-0"
|
|
103
|
+
:style="{ gridTemplateColumns: `repeat(${days.length}, 1fr)` }"
|
|
104
|
+
>
|
|
105
|
+
<div
|
|
106
|
+
v-for="day in days"
|
|
107
|
+
:key="day.iso"
|
|
108
|
+
class="border-r border-default last:border-r-0 min-h-[44px]"
|
|
109
|
+
:class="[
|
|
110
|
+
day.isWeekend && 'bg-elevated/40',
|
|
111
|
+
day.isToday && 'bg-primary/5',
|
|
112
|
+
day.isHoliday && !day.isWeekend && 'bg-error/5',
|
|
113
|
+
]"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
<!-- Leave pills -->
|
|
117
|
+
<div
|
|
118
|
+
v-for="bar in barsByPerson[String(emp.id)] || []"
|
|
119
|
+
:key="bar.id"
|
|
120
|
+
class="absolute inset-y-1.5 flex items-center px-2 rounded text-xs font-medium overflow-hidden whitespace-nowrap cursor-pointer"
|
|
121
|
+
:class="[
|
|
122
|
+
bar.status?.toLowerCase() === 'approved' ? 'bg-success/20 text-success border border-success/30' : 'bg-warning/20 text-warning border border-dashed border-warning/50 opacity-80',
|
|
123
|
+
]"
|
|
124
|
+
:style="{ left: `${bar.leftPct}%`, width: `${bar.widthPct}%`, minWidth: '40px' }"
|
|
125
|
+
:title="`${bar.leaveType} · ${bar.start} → ${bar.end} (${bar.days}d)`"
|
|
126
|
+
>
|
|
127
|
+
<span class="truncate">{{ bar.leaveType }}</span>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</template>
|
|
131
|
+
|
|
132
|
+
<div
|
|
133
|
+
v-if="activeEmployees.length === 0"
|
|
134
|
+
class="col-span-full py-16 text-center text-sm text-muted"
|
|
135
|
+
>
|
|
136
|
+
{{ $t('common.no_data') }}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</template>
|
|
142
|
+
|
|
143
|
+
<script lang="ts" setup>
|
|
144
|
+
const { t } = useI18n()
|
|
145
|
+
const { leaveRequests } = useLeave()
|
|
146
|
+
const { holidays } = useHolidays()
|
|
147
|
+
const { employees } = usePeople()
|
|
148
|
+
|
|
149
|
+
const cursor = useState('timeline.cursor', () => new Date())
|
|
150
|
+
const searchQuery = ref('')
|
|
151
|
+
|
|
152
|
+
const WEEKDAYS_SHORT = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
|
|
153
|
+
|
|
154
|
+
function toIso(d: Date): string {
|
|
155
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
156
|
+
}
|
|
157
|
+
function dayDiff(a: Date, b: Date): number {
|
|
158
|
+
return Math.round((b.getTime() - a.getTime()) / 86400000)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const monthStart = computed(() => new Date(cursor.value.getFullYear(), cursor.value.getMonth(), 1))
|
|
162
|
+
const monthEnd = computed(() => new Date(cursor.value.getFullYear(), cursor.value.getMonth() + 1, 0))
|
|
163
|
+
|
|
164
|
+
const holidaySet = computed(() => {
|
|
165
|
+
const m = new Map<string, string>()
|
|
166
|
+
for (const h of holidays.value) {
|
|
167
|
+
if (h.date) m.set(toIso(new Date(h.date)), h.name)
|
|
168
|
+
}
|
|
169
|
+
return m
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
interface DayBucket { iso: string, d: number, dow: string, isWeekend: boolean, isToday: boolean, isHoliday: boolean, holidayName?: string }
|
|
173
|
+
|
|
174
|
+
const days = computed<DayBucket[]>(() => {
|
|
175
|
+
const today = toIso(new Date())
|
|
176
|
+
const list: DayBucket[] = []
|
|
177
|
+
const d = new Date(monthStart.value)
|
|
178
|
+
while (d <= monthEnd.value) {
|
|
179
|
+
const iso = toIso(d)
|
|
180
|
+
const dow = d.getDay()
|
|
181
|
+
list.push({
|
|
182
|
+
iso, d: d.getDate(), dow: WEEKDAYS_SHORT[dow]!,
|
|
183
|
+
isWeekend: dow === 0 || dow === 6,
|
|
184
|
+
isToday: iso === today,
|
|
185
|
+
isHoliday: holidaySet.value.has(iso),
|
|
186
|
+
holidayName: holidaySet.value.get(iso),
|
|
187
|
+
})
|
|
188
|
+
d.setDate(d.getDate() + 1)
|
|
189
|
+
}
|
|
190
|
+
return list
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const rangeLabel = computed(() => {
|
|
194
|
+
const opts: Intl.DateTimeFormatOptions = { month: 'long', year: 'numeric' }
|
|
195
|
+
return monthStart.value.toLocaleDateString('en-US', opts)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
const gridStyle = computed(() => ({
|
|
199
|
+
gridTemplateColumns: `minmax(220px, 220px) repeat(${days.value.length}, minmax(32px, 1fr))`,
|
|
200
|
+
}))
|
|
201
|
+
|
|
202
|
+
const activeEmployees = computed(() => {
|
|
203
|
+
const list = employees.value.filter((e: Person) => e.stage === 'active' || e.stage === 'probation')
|
|
204
|
+
const q = searchQuery.value.trim().toLowerCase()
|
|
205
|
+
return q ? list.filter((e: Person) => (e.display_name ?? '').toLowerCase().includes(q)) : list
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
interface Bar { id: string, leaveType: string, start: string, end: string, days: number, status: string, leftPct: number, widthPct: number }
|
|
209
|
+
|
|
210
|
+
const barsByPerson = computed<Record<string, Bar[]>>(() => {
|
|
211
|
+
const out: Record<string, Bar[]> = {}
|
|
212
|
+
const startIso = toIso(monthStart.value)
|
|
213
|
+
const endIso = toIso(monthEnd.value)
|
|
214
|
+
const totalDays = days.value.length
|
|
215
|
+
|
|
216
|
+
for (const lr of leaveRequests.value) {
|
|
217
|
+
const st = lr.status?.toLowerCase()
|
|
218
|
+
if (st === 'rejected' || st === 'cancelled') continue
|
|
219
|
+
const lrStart = (lr.start_date || lr.from_date) ? toIso(new Date(lr.start_date || lr.from_date)) : ''
|
|
220
|
+
const lrEnd = (lr.end_date || lr.to_date) ? toIso(new Date(lr.end_date || lr.to_date)) : ''
|
|
221
|
+
if (lrEnd < startIso || lrStart > endIso) continue
|
|
222
|
+
|
|
223
|
+
const clampedStart = lrStart < startIso ? startIso : lrStart
|
|
224
|
+
const clampedEnd = lrEnd > endIso ? endIso : lrEnd
|
|
225
|
+
|
|
226
|
+
const startD = new Date(monthStart.value)
|
|
227
|
+
const offsetDays = dayDiff(startD, new Date(clampedStart))
|
|
228
|
+
const spanDays = dayDiff(new Date(clampedStart), new Date(clampedEnd)) + 1
|
|
229
|
+
|
|
230
|
+
const leftPct = (offsetDays / totalDays) * 100
|
|
231
|
+
const widthPct = (spanDays / totalDays) * 100
|
|
232
|
+
|
|
233
|
+
let typeName = lr.leave_type ?? ''
|
|
234
|
+
if (!typeName && lr.leave_type_id && typeof lr.leave_type_id === 'object') {
|
|
235
|
+
typeName = (lr.leave_type_id as any).name ?? (lr.leave_type_id as any).label ?? ''
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const bar: Bar = {
|
|
239
|
+
id: String(lr.id),
|
|
240
|
+
leaveType: typeName || 'Leave',
|
|
241
|
+
start: lrStart,
|
|
242
|
+
end: lrEnd,
|
|
243
|
+
days: lr.days_count ?? lr.days ?? spanDays,
|
|
244
|
+
status: lr.status,
|
|
245
|
+
leftPct,
|
|
246
|
+
widthPct,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const pid = String((lr.person_id as any)?.id ?? lr.person_id)
|
|
250
|
+
if (!out[pid]) out[pid] = []
|
|
251
|
+
out[pid]!.push(bar)
|
|
252
|
+
}
|
|
253
|
+
return out
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
function prevMonth() { cursor.value = new Date(cursor.value.getFullYear(), cursor.value.getMonth() - 1, 1) }
|
|
257
|
+
function nextMonth() { cursor.value = new Date(cursor.value.getFullYear(), cursor.value.getMonth() + 1, 1) }
|
|
258
|
+
function goToday() { cursor.value = new Date() }
|
|
259
|
+
</script>
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import HrInfoSection from "../shared/section.vue"
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{ person: Person }>()
|
|
5
|
+
|
|
6
|
+
const { t, te } = useI18n()
|
|
7
|
+
const toast = useToast()
|
|
8
|
+
const api = useHrApi()
|
|
9
|
+
const { onUpdated } = useEmployeeDetail()
|
|
10
|
+
const {
|
|
11
|
+
refreshKey, fetchForPerson, listForPerson, get, updateCase, updateTask,
|
|
12
|
+
returnAssets, deprovision, getSettlement, finalize, cancel,
|
|
13
|
+
} = useOffboarding()
|
|
14
|
+
|
|
15
|
+
const offcase = ref<Offboarding | null>(null)
|
|
16
|
+
const allCases = ref<Offboarding[]>([])
|
|
17
|
+
const settlement = ref<OffboardingSettlement | null>(null)
|
|
18
|
+
const loading = ref(false)
|
|
19
|
+
const busy = ref(false)
|
|
20
|
+
const showFinalize = ref(false)
|
|
21
|
+
const payoutAmount = ref<number | null>(null)
|
|
22
|
+
const detailCase = ref<Offboarding | null>(null)
|
|
23
|
+
const detailLoading = ref(false)
|
|
24
|
+
|
|
25
|
+
const history = computed(() => allCases.value.filter(c => c.id !== offcase.value?.id))
|
|
26
|
+
|
|
27
|
+
const TASK_ICON: Record<string, string> = {
|
|
28
|
+
knowledge_transfer: "i-ph-handshake-light",
|
|
29
|
+
asset_return: "i-ph-package-light",
|
|
30
|
+
access_revoke: "i-ph-lock-key-light",
|
|
31
|
+
leave_settlement: "i-ph-umbrella-light",
|
|
32
|
+
exit_interview: "i-ph-microphone-light",
|
|
33
|
+
finance_clearance: "i-ph-money-light",
|
|
34
|
+
manager_signoff: "i-ph-seal-check-light",
|
|
35
|
+
exit_documents: "i-ph-file-doc-light",
|
|
36
|
+
}
|
|
37
|
+
const STATUS_COLOR: Record<string, string> = { initiated: "info", in_progress: "warning", completed: "success", cancelled: "error" }
|
|
38
|
+
|
|
39
|
+
const readonly = computed(() => ["completed", "cancelled"].includes(offcase.value?.status ?? ""))
|
|
40
|
+
const sortedTasks = computed(() => [...(offcase.value?.tasks ?? [])].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0)))
|
|
41
|
+
|
|
42
|
+
function taskLabel(tk: OffboardingTask) {
|
|
43
|
+
const key = `hr.offboarding.tasks.${tk.key}`
|
|
44
|
+
return te(key) ? t(key) : tk.label
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function load() {
|
|
48
|
+
loading.value = true
|
|
49
|
+
try {
|
|
50
|
+
const [current, cases] = await Promise.all([
|
|
51
|
+
fetchForPerson(props.person.id),
|
|
52
|
+
listForPerson(props.person.id),
|
|
53
|
+
])
|
|
54
|
+
offcase.value = current
|
|
55
|
+
allCases.value = cases
|
|
56
|
+
payoutAmount.value = offcase.value?.leave_payout_amount ?? null
|
|
57
|
+
}
|
|
58
|
+
finally { loading.value = false }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function openDetail(id: number) {
|
|
62
|
+
detailLoading.value = true
|
|
63
|
+
try { detailCase.value = await get(id) }
|
|
64
|
+
finally { detailLoading.value = false }
|
|
65
|
+
}
|
|
66
|
+
async function reloadCase() {
|
|
67
|
+
if (offcase.value) offcase.value = await fetchForPerson(props.person.id)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function setTaskStatus(tk: OffboardingTask, status: string) {
|
|
71
|
+
if (readonly.value) return
|
|
72
|
+
await updateTask(offcase.value!.id, tk.id, { status: status as any })
|
|
73
|
+
await reloadCase()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function onReturnAssets() {
|
|
77
|
+
busy.value = true
|
|
78
|
+
try {
|
|
79
|
+
const r = await returnAssets(offcase.value!.id)
|
|
80
|
+
toast.add({ title: t("hr.offboarding.toast.assets_returned", { count: r?.returned ?? 0 }), color: "success" })
|
|
81
|
+
await reloadCase()
|
|
82
|
+
}
|
|
83
|
+
catch (e: any) { toast.add({ title: e?.message ?? t("hr.toast.error"), color: "error" }) }
|
|
84
|
+
finally { busy.value = false }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function onDeprovision() {
|
|
88
|
+
busy.value = true
|
|
89
|
+
try {
|
|
90
|
+
await deprovision(offcase.value!.id)
|
|
91
|
+
toast.add({ title: t("hr.offboarding.toast.deprovisioned"), color: "success" })
|
|
92
|
+
await reloadCase()
|
|
93
|
+
}
|
|
94
|
+
catch (e: any) { toast.add({ title: e?.message ?? t("hr.toast.error"), color: "error" }) }
|
|
95
|
+
finally { busy.value = false }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function onLoadSettlement() {
|
|
99
|
+
busy.value = true
|
|
100
|
+
try { settlement.value = await getSettlement(offcase.value!.id) }
|
|
101
|
+
catch (e: any) { toast.add({ title: e?.message ?? t("hr.toast.error"), color: "error" }) }
|
|
102
|
+
finally { busy.value = false }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function onSavePayout() {
|
|
106
|
+
busy.value = true
|
|
107
|
+
try {
|
|
108
|
+
await updateCase(offcase.value!.id, {
|
|
109
|
+
leave_payout_days: settlement.value?.totalRemainingDays ?? null,
|
|
110
|
+
leave_payout_amount: payoutAmount.value ?? null,
|
|
111
|
+
})
|
|
112
|
+
toast.add({ title: t("hr.toast.saved"), color: "success" })
|
|
113
|
+
await reloadCase()
|
|
114
|
+
}
|
|
115
|
+
catch (e: any) { toast.add({ title: e?.message ?? t("hr.toast.error"), color: "error" }) }
|
|
116
|
+
finally { busy.value = false }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function onFinalize() {
|
|
120
|
+
busy.value = true
|
|
121
|
+
try {
|
|
122
|
+
await finalize(offcase.value!.id)
|
|
123
|
+
showFinalize.value = false
|
|
124
|
+
toast.add({ title: t("hr.offboarding.toast.finalized"), color: "success" })
|
|
125
|
+
await reloadCase()
|
|
126
|
+
const updated = await api.getPerson(String(props.person.id))
|
|
127
|
+
if (updated) onUpdated(updated as Person)
|
|
128
|
+
}
|
|
129
|
+
catch (e: any) { toast.add({ title: e?.message ?? t("hr.toast.error"), color: "error" }) }
|
|
130
|
+
finally { busy.value = false }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function onCancel() {
|
|
134
|
+
busy.value = true
|
|
135
|
+
try {
|
|
136
|
+
await cancel(offcase.value!.id)
|
|
137
|
+
toast.add({ title: t("hr.offboarding.toast.cancelled"), color: "success" })
|
|
138
|
+
await reloadCase()
|
|
139
|
+
}
|
|
140
|
+
catch (e: any) { toast.add({ title: e?.message ?? t("hr.toast.error"), color: "error" }) }
|
|
141
|
+
finally { busy.value = false }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
onMounted(load)
|
|
145
|
+
watch(refreshKey, load)
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
<template>
|
|
149
|
+
<div class="p-6 max-w-4xl space-y-6">
|
|
150
|
+
<div v-if="loading" class="px-4 py-8 text-sm text-muted">{{ t('common.loading') }}</div>
|
|
151
|
+
|
|
152
|
+
<!-- No case -->
|
|
153
|
+
<div v-else-if="!offcase" class="px-4 py-16 text-center text-muted">
|
|
154
|
+
<UIcon name="i-ph-sign-out-light" class="size-10 mb-3 opacity-40" />
|
|
155
|
+
<p class="text-sm">{{ t('hr.offboarding.no_case') }}</p>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<template v-else>
|
|
159
|
+
<!-- Summary -->
|
|
160
|
+
<HrInfoSection :title="t('hr.offboarding.title')" icon="i-ph-sign-out-light">
|
|
161
|
+
<template #actions>
|
|
162
|
+
<UBadge :color="(STATUS_COLOR[offcase.status] ?? 'neutral') as any" variant="subtle" size="md">
|
|
163
|
+
{{ t(`hr.offboarding.status.${offcase.status}`) }}
|
|
164
|
+
</UBadge>
|
|
165
|
+
</template>
|
|
166
|
+
<div class="p-4 grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
167
|
+
<div><div class="text-xs uppercase tracking-wider text-muted mb-1">{{ t('hr.offboarding.fields.exit_type') }}</div>{{ t(`hr.offboarding.exit_type.${offcase.exit_type}`) }}</div>
|
|
168
|
+
<div><div class="text-xs uppercase tracking-wider text-muted mb-1">{{ t('hr.offboarding.fields.notice_date') }}</div>{{ offcase.notice_date?.slice(0,10) ?? '—' }}</div>
|
|
169
|
+
<div><div class="text-xs uppercase tracking-wider text-muted mb-1">{{ t('hr.offboarding.fields.last_working_day') }}</div>{{ offcase.last_working_day?.slice(0,10) ?? '—' }}</div>
|
|
170
|
+
<div><div class="text-xs uppercase tracking-wider text-muted mb-1">{{ t('hr.offboarding.fields.rehire_eligible') }}</div>{{ offcase.rehire_eligible ? '✓' : '—' }}</div>
|
|
171
|
+
<div v-if="offcase.reason" class="col-span-2 md:col-span-4"><div class="text-xs uppercase tracking-wider text-muted mb-1">{{ t('hr.offboarding.fields.reason') }}</div>{{ offcase.reason }}</div>
|
|
172
|
+
</div>
|
|
173
|
+
</HrInfoSection>
|
|
174
|
+
|
|
175
|
+
<!-- Checklist -->
|
|
176
|
+
<HrInfoSection :title="t('hr.offboarding.sections.checklist')" icon="i-ph-list-checks-light">
|
|
177
|
+
<ul class="divide-y divide-default">
|
|
178
|
+
<li v-for="tk in sortedTasks" :key="tk.id" class="flex items-center gap-3 px-4 py-3">
|
|
179
|
+
<UIcon :name="TASK_ICON[tk.key] ?? 'i-ph-circle-light'" class="size-5 shrink-0" :class="tk.status === 'done' ? 'text-success' : 'text-muted'" />
|
|
180
|
+
<div class="flex-1 min-w-0">
|
|
181
|
+
<div class="text-sm font-medium text-default">{{ taskLabel(tk) }}</div>
|
|
182
|
+
</div>
|
|
183
|
+
<UButton
|
|
184
|
+
v-if="tk.key === 'asset_return' && !readonly"
|
|
185
|
+
size="xs" variant="soft" icon="i-ph-package-light"
|
|
186
|
+
:label="t('hr.offboarding.actions.return_all')" :loading="busy" @click="onReturnAssets"
|
|
187
|
+
/>
|
|
188
|
+
<UButton
|
|
189
|
+
v-if="tk.key === 'access_revoke' && !readonly"
|
|
190
|
+
size="xs" variant="soft" icon="i-ph-lock-key-light"
|
|
191
|
+
:label="t('hr.offboarding.actions.deprovision')" :loading="busy" @click="onDeprovision"
|
|
192
|
+
/>
|
|
193
|
+
<UBadge v-if="tk.status === 'done'" color="success" variant="subtle" size="sm">{{ t('hr.offboarding.task_status.done') }}</UBadge>
|
|
194
|
+
<UBadge v-else-if="tk.status === 'na'" color="neutral" variant="subtle" size="sm">{{ t('hr.offboarding.task_status.na') }}</UBadge>
|
|
195
|
+
<div v-if="!readonly" class="flex items-center gap-1">
|
|
196
|
+
<UButton v-if="tk.status !== 'done'" size="xs" color="success" variant="ghost" icon="i-ph-check-light" @click="setTaskStatus(tk, 'done')" />
|
|
197
|
+
<UButton v-if="tk.status === 'done'" size="xs" color="neutral" variant="ghost" icon="i-ph-arrow-counter-clockwise-light" @click="setTaskStatus(tk, 'pending')" />
|
|
198
|
+
<UButton v-if="tk.status !== 'na'" size="xs" color="neutral" variant="ghost" icon="i-ph-minus-circle-light" @click="setTaskStatus(tk, 'na')" />
|
|
199
|
+
</div>
|
|
200
|
+
</li>
|
|
201
|
+
</ul>
|
|
202
|
+
</HrInfoSection>
|
|
203
|
+
|
|
204
|
+
<!-- Settlement -->
|
|
205
|
+
<HrInfoSection :title="t('hr.offboarding.sections.settlement')" icon="i-ph-umbrella-light">
|
|
206
|
+
<div class="p-4 space-y-3">
|
|
207
|
+
<UButton v-if="!settlement" size="sm" variant="soft" icon="i-ph-calculator-light" :label="t('hr.offboarding.actions.compute_settlement')" :loading="busy" @click="onLoadSettlement" />
|
|
208
|
+
<template v-else>
|
|
209
|
+
<div class="text-sm">
|
|
210
|
+
<span class="text-muted">{{ t('hr.offboarding.settlement.remaining_days') }}: </span>
|
|
211
|
+
<span class="font-semibold text-default">{{ settlement.totalRemainingDays }}</span>
|
|
212
|
+
</div>
|
|
213
|
+
<ul class="text-xs text-muted space-y-1">
|
|
214
|
+
<li v-for="(l, i) in settlement.lines" :key="i">{{ l.leave_type }}: {{ l.remaining }} ({{ l.allocated }} − {{ l.used }})</li>
|
|
215
|
+
</ul>
|
|
216
|
+
<div v-if="!readonly" class="flex items-end gap-3 pt-2">
|
|
217
|
+
<UFormField :label="t('hr.offboarding.settlement.payout_amount')">
|
|
218
|
+
<UInput v-model.number="payoutAmount" type="number" class="w-48" />
|
|
219
|
+
</UFormField>
|
|
220
|
+
<UButton size="sm" :label="t('common.save')" :loading="busy" @click="onSavePayout" />
|
|
221
|
+
</div>
|
|
222
|
+
</template>
|
|
223
|
+
</div>
|
|
224
|
+
</HrInfoSection>
|
|
225
|
+
|
|
226
|
+
<!-- Actions -->
|
|
227
|
+
<div v-if="!readonly" class="flex items-center justify-end gap-2">
|
|
228
|
+
<UButton color="neutral" variant="outline" icon="i-ph-x-light" :label="t('hr.offboarding.actions.cancel')" :loading="busy" @click="onCancel" />
|
|
229
|
+
<UButton color="primary" icon="i-ph-flag-checkered-light" :label="t('hr.offboarding.actions.finalize')" :disabled="busy" @click="showFinalize = true" />
|
|
230
|
+
</div>
|
|
231
|
+
</template>
|
|
232
|
+
|
|
233
|
+
<!-- History -->
|
|
234
|
+
<HrInfoSection v-if="history.length" :title="t('hr.offboarding.sections.history')" icon="i-ph-clock-counter-clockwise-light">
|
|
235
|
+
<ul class="divide-y divide-default">
|
|
236
|
+
<li
|
|
237
|
+
v-for="c in history" :key="c.id"
|
|
238
|
+
class="flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-elevated/50 transition-colors"
|
|
239
|
+
@click="openDetail(c.id)"
|
|
240
|
+
>
|
|
241
|
+
<UIcon name="i-ph-sign-out-light" class="size-5 shrink-0 text-muted" />
|
|
242
|
+
<div class="flex-1 min-w-0">
|
|
243
|
+
<div class="text-sm font-medium text-default">{{ t(`hr.offboarding.exit_type.${c.exit_type}`) }}</div>
|
|
244
|
+
<div class="text-xs text-muted">{{ t('hr.offboarding.fields.last_working_day') }}: {{ c.last_working_day?.slice(0,10) ?? '—' }}</div>
|
|
245
|
+
</div>
|
|
246
|
+
<UBadge :color="(STATUS_COLOR[c.status] ?? 'neutral') as any" variant="subtle" size="sm">
|
|
247
|
+
{{ t(`hr.offboarding.status.${c.status}`) }}
|
|
248
|
+
</UBadge>
|
|
249
|
+
<UIcon name="i-ph-caret-right-light" class="size-4 shrink-0 text-muted" />
|
|
250
|
+
</li>
|
|
251
|
+
</ul>
|
|
252
|
+
</HrInfoSection>
|
|
253
|
+
|
|
254
|
+
<!-- History detail (read-only) -->
|
|
255
|
+
<UModal
|
|
256
|
+
:open="!!detailCase"
|
|
257
|
+
:title="detailCase ? t(`hr.offboarding.exit_type.${detailCase.exit_type}`) : ''"
|
|
258
|
+
:ui="{ content: 'sm:max-w-2xl' }"
|
|
259
|
+
@update:open="(v: boolean) => { if (!v) detailCase = null }"
|
|
260
|
+
>
|
|
261
|
+
<template #body>
|
|
262
|
+
<div v-if="detailCase" class="space-y-5">
|
|
263
|
+
<div class="flex items-center gap-2">
|
|
264
|
+
<UBadge :color="(STATUS_COLOR[detailCase.status] ?? 'neutral') as any" variant="subtle" size="md">
|
|
265
|
+
{{ t(`hr.offboarding.status.${detailCase.status}`) }}
|
|
266
|
+
</UBadge>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="grid grid-cols-2 gap-4 text-sm">
|
|
269
|
+
<div><div class="text-xs uppercase tracking-wider text-muted mb-1">{{ t('hr.offboarding.fields.notice_date') }}</div>{{ detailCase.notice_date?.slice(0,10) ?? '—' }}</div>
|
|
270
|
+
<div><div class="text-xs uppercase tracking-wider text-muted mb-1">{{ t('hr.offboarding.fields.last_working_day') }}</div>{{ detailCase.last_working_day?.slice(0,10) ?? '—' }}</div>
|
|
271
|
+
<div><div class="text-xs uppercase tracking-wider text-muted mb-1">{{ t('hr.offboarding.fields.rehire_eligible') }}</div>{{ detailCase.rehire_eligible ? '✓' : '—' }}</div>
|
|
272
|
+
<div v-if="detailCase.leave_payout_days != null"><div class="text-xs uppercase tracking-wider text-muted mb-1">{{ t('hr.offboarding.settlement.remaining_days') }}</div>{{ detailCase.leave_payout_days }}</div>
|
|
273
|
+
<div v-if="detailCase.reason" class="col-span-2"><div class="text-xs uppercase tracking-wider text-muted mb-1">{{ t('hr.offboarding.fields.reason') }}</div>{{ detailCase.reason }}</div>
|
|
274
|
+
</div>
|
|
275
|
+
<div>
|
|
276
|
+
<div class="text-xs uppercase tracking-wider text-muted mb-2">{{ t('hr.offboarding.sections.checklist') }}</div>
|
|
277
|
+
<ul class="divide-y divide-default border border-default rounded-lg">
|
|
278
|
+
<li v-for="tk in [...(detailCase.tasks ?? [])].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))" :key="tk.id" class="flex items-center gap-3 px-3 py-2">
|
|
279
|
+
<UIcon :name="TASK_ICON[tk.key] ?? 'i-ph-circle-light'" class="size-4 shrink-0" :class="tk.status === 'done' ? 'text-success' : 'text-muted'" />
|
|
280
|
+
<span class="flex-1 text-sm">{{ taskLabel(tk) }}</span>
|
|
281
|
+
<UBadge v-if="tk.status === 'done'" color="success" variant="subtle" size="sm">{{ t('hr.offboarding.task_status.done') }}</UBadge>
|
|
282
|
+
<UBadge v-else-if="tk.status === 'na'" color="neutral" variant="subtle" size="sm">{{ t('hr.offboarding.task_status.na') }}</UBadge>
|
|
283
|
+
</li>
|
|
284
|
+
</ul>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</template>
|
|
288
|
+
</UModal>
|
|
289
|
+
|
|
290
|
+
<!-- Finalize confirm -->
|
|
291
|
+
<UModal :open="showFinalize" :title="t('hr.offboarding.actions.finalize')" @update:open="showFinalize = $event">
|
|
292
|
+
<template #body>
|
|
293
|
+
<p class="text-sm text-muted">{{ t('hr.offboarding.finalize_confirm', { name: person.display_name }) }}</p>
|
|
294
|
+
</template>
|
|
295
|
+
<template #footer>
|
|
296
|
+
<div class="flex justify-end gap-2">
|
|
297
|
+
<UButton color="neutral" variant="outline" :label="t('common.cancel')" @click="showFinalize = false" />
|
|
298
|
+
<UButton color="primary" :label="t('hr.offboarding.actions.finalize')" :loading="busy" @click="onFinalize" />
|
|
299
|
+
</div>
|
|
300
|
+
</template>
|
|
301
|
+
</UModal>
|
|
302
|
+
</div>
|
|
303
|
+
</template>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import HrInfoSection from "../../shared/section.vue"
|
|
3
|
+
import HrTabTimeline from "./timeline.vue"
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{ personId: string, readonly?: boolean }>()
|
|
6
|
+
|
|
7
|
+
const api = useHrApi()
|
|
8
|
+
|
|
9
|
+
const notes = ref<HrNote[]>([])
|
|
10
|
+
const loading = ref(true)
|
|
11
|
+
const draft = ref("")
|
|
12
|
+
const adding = ref(false)
|
|
13
|
+
|
|
14
|
+
onMounted(async () => {
|
|
15
|
+
try {
|
|
16
|
+
notes.value = await api.listNotes(props.personId)
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
loading.value = false
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
async function addNote() {
|
|
24
|
+
const body = draft.value.trim()
|
|
25
|
+
if (!body) return
|
|
26
|
+
adding.value = true
|
|
27
|
+
try {
|
|
28
|
+
const note = await api.createNote(props.personId, body)
|
|
29
|
+
notes.value.unshift(note)
|
|
30
|
+
draft.value = ""
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
adding.value = false
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<div class="p-6 max-w-4xl space-y-6">
|
|
40
|
+
<HrInfoSection
|
|
41
|
+
v-if="!readonly"
|
|
42
|
+
title="Add Note"
|
|
43
|
+
icon="i-ph-note-pencil-light"
|
|
44
|
+
>
|
|
45
|
+
<div class="p-4 space-y-2">
|
|
46
|
+
<UTextarea
|
|
47
|
+
v-model="draft"
|
|
48
|
+
placeholder="Write a note about this person…"
|
|
49
|
+
:rows="3"
|
|
50
|
+
/>
|
|
51
|
+
<div class="flex justify-end">
|
|
52
|
+
<UButton
|
|
53
|
+
:disabled="!draft.trim()"
|
|
54
|
+
:loading="adding"
|
|
55
|
+
icon="i-ph-paper-plane-tilt-light"
|
|
56
|
+
@click="addNote"
|
|
57
|
+
>
|
|
58
|
+
Post note
|
|
59
|
+
</UButton>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</HrInfoSection>
|
|
63
|
+
<HrTabTimeline :person-id="personId" :notes="notes" :loading="loading" />
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|