@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,119 @@
1
+ <script lang="ts" setup>
2
+ const { $api } = useNuxtApp()
3
+ import HrInfoSection from "../../shared/section.vue"
4
+
5
+ const props = defineProps<{
6
+ personId: string
7
+ notes?: HrNote[]
8
+ loading?: boolean
9
+ }>()
10
+
11
+ const transitions = ref<StageTransition[]>([])
12
+ const transitionsLoading = ref(true)
13
+
14
+ onMounted(async () => {
15
+ try {
16
+ const res = await $api<StageTransition[] | { data: StageTransition[] }>(`/hr/people/${props.personId}/transitions`)
17
+ transitions.value = Array.isArray(res) ? res : (res.data ?? [])
18
+ }
19
+ catch {
20
+ transitions.value = []
21
+ }
22
+ finally {
23
+ transitionsLoading.value = false
24
+ }
25
+ })
26
+
27
+ function userName(user: any): string {
28
+ if (!user) return ''
29
+ if (typeof user === 'object') return user.first_name ? `${user.first_name} ${user.last_name ?? ''}`.trim() : (user.email ?? '')
30
+ return String(user).slice(0, 8)
31
+ }
32
+
33
+ interface TimelineEntry { at: string, title: string, sub?: string, by?: string, icon: string, color: string }
34
+
35
+ const entries = computed<TimelineEntry[]>(() => {
36
+ const out: TimelineEntry[] = []
37
+ for (const tr of transitions.value) {
38
+ out.push({
39
+ at: tr.date_created,
40
+ title: tr.from_stage ? `${tr.from_stage} → ${tr.to_stage}` : `Created at ${tr.to_stage}`,
41
+ sub: tr.reason,
42
+ by: userName(tr.performed_by),
43
+ icon: "i-ph-flow-arrow-light",
44
+ color: "text-primary",
45
+ })
46
+ }
47
+ for (const n of (props.notes ?? [])) {
48
+ out.push({
49
+ at: n.date_created,
50
+ title: "Note",
51
+ sub: n.body,
52
+ by: userName((n as any).author_id ?? n.author),
53
+ icon: "i-ph-note-light",
54
+ color: "text-muted",
55
+ })
56
+ }
57
+ return out.sort((a, b) => b.at.localeCompare(a.at))
58
+ })
59
+
60
+ const isLoading = computed(() => props.loading || transitionsLoading.value)
61
+ </script>
62
+
63
+ <template>
64
+ <div>
65
+ <HrInfoSection
66
+ title="Activity Timeline"
67
+ icon="i-ph-clock-light"
68
+ >
69
+ <div
70
+ v-if="isLoading"
71
+ class="p-6 text-muted text-sm"
72
+ >
73
+ <UIcon
74
+ name="i-ph-spinner"
75
+ class="animate-spin size-5 mx-auto"
76
+ />
77
+ </div>
78
+ <div
79
+ v-else-if="!entries.length"
80
+ class="p-6 text-muted text-sm"
81
+ >
82
+ No activity yet.
83
+ </div>
84
+ <ol
85
+ v-else
86
+ class="divide-y divide-default"
87
+ >
88
+ <li
89
+ v-for="(e, i) in entries"
90
+ :key="i"
91
+ class="flex items-start gap-3 px-4 py-3 hover:bg-elevated/30 transition-colors"
92
+ >
93
+ <UIcon
94
+ :name="e.icon"
95
+ :class="['size-5 shrink-0 mt-0.5', e.color]"
96
+ />
97
+ <div class="flex-1 min-w-0">
98
+ <div class="flex items-baseline gap-2">
99
+ <span class="font-medium text-sm text-default">{{ e.title }}</span>
100
+ <span
101
+ v-if="e.by"
102
+ class="text-xs text-muted"
103
+ >by {{ e.by }}</span>
104
+ </div>
105
+ <div
106
+ v-if="e.sub"
107
+ class="text-muted text-xs mt-0.5"
108
+ >
109
+ {{ e.sub }}
110
+ </div>
111
+ </div>
112
+ <div class="text-muted text-xs whitespace-nowrap shrink-0">
113
+ {{ e.at.slice(0, 10) }}
114
+ </div>
115
+ </li>
116
+ </ol>
117
+ </HrInfoSection>
118
+ </div>
119
+ </template>
@@ -0,0 +1,303 @@
1
+ <script lang="ts" setup>
2
+ import HrTabProfile from "./profile/tab.vue";
3
+ import HrTabRecruitment from "../talents/tab.vue";
4
+ import HrTabActivity from "./activity/tab.vue";
5
+ import HrTabEmployment from "../employees/tab.vue";
6
+ import HrPersonTabAssets from "../employees/assets/tab.vue";
7
+ import HrTabProvisioning from "../employees/provisioning/tab.vue";
8
+ import HrPersonStageBadge from "../shared/stage-badge.vue";
9
+
10
+ const props = defineProps<{
11
+ person: Person;
12
+ backTo: string;
13
+ enabledTabs: string[];
14
+ readonlyTabs?: string[];
15
+ }>();
16
+
17
+ const { t } = useI18n();
18
+ const toast = useToast();
19
+ const { hire } = usePeople();
20
+
21
+ const localPerson = ref<Person>({ ...props.person });
22
+
23
+ const acting = ref(false);
24
+
25
+ const TALENT_PIPELINE = ["talent", "interviewing", "offer"];
26
+ const isTalent = computed(() => TALENT_PIPELINE.includes(localPerson.value.stage));
27
+
28
+ // Step-by-step pipeline advance (talent → interviewing → offer). Hiring is a separate action.
29
+ const nextStageAction = computed<{ to: string; label: string } | null>(() => {
30
+ switch (localPerson.value.stage) {
31
+ case "talent":
32
+ return { to: "interviewing", label: t("hr.actions.move_to_interviewing") };
33
+ case "interviewing":
34
+ return { to: "offer", label: t("hr.actions.send_offer") };
35
+ default:
36
+ return null;
37
+ }
38
+ });
39
+
40
+ async function onStageAction() {
41
+ const action = nextStageAction.value;
42
+ if (!action) return;
43
+ acting.value = true;
44
+ try {
45
+ await $fetch(`/api/hr/people/${localPerson.value.id}/transition`, {
46
+ method: "POST",
47
+ body: { to: action.to, reason: "" },
48
+ });
49
+ localPerson.value = { ...localPerson.value, stage: action.to };
50
+ toast.add({ title: t("hr.toast.stage_changed", { stage: action.to }), color: "success" });
51
+ } catch {
52
+ toast.add({ title: t("hr.toast.error"), color: "error" });
53
+ } finally {
54
+ acting.value = false;
55
+ }
56
+ }
57
+
58
+ // Direct convert to employee (any talent stage → probation), with confirmation
59
+ const showConvert = ref(false);
60
+ const convertForm = ref({ start_date: new Date().toISOString().slice(0, 10), reason: "" });
61
+
62
+ function openConvert() {
63
+ convertForm.value = { start_date: new Date().toISOString().slice(0, 10), reason: "" };
64
+ showConvert.value = true;
65
+ }
66
+
67
+ async function onConvertConfirm() {
68
+ acting.value = true;
69
+ try {
70
+ await hire(String(localPerson.value.id), {
71
+ reason: convertForm.value.reason,
72
+ start_date: convertForm.value.start_date,
73
+ });
74
+ showConvert.value = false;
75
+ } catch {
76
+ toast.add({ title: t("hr.toast.error"), color: "error" });
77
+ } finally {
78
+ acting.value = false;
79
+ }
80
+ }
81
+
82
+ watch(
83
+ () => props.person,
84
+ (p) => {
85
+ localPerson.value = { ...p };
86
+ },
87
+ );
88
+
89
+ const EMPLOYEE_STAGES = ["probation", "active", "resigned"];
90
+
91
+ const TALENT_STAGES = ["talent", "interviewing", "offer"];
92
+
93
+ const BASE_TAB_DEFS: { id: string; label: string; icon: string }[] = [
94
+ { id: "profile", label: t("hr.tabs.profile"), icon: "i-ph-user-light" },
95
+ { id: "activity", label: t("hr.tabs.activity"), icon: "i-ph-clock-light" },
96
+ ];
97
+
98
+ const allTabDefs = computed(() => {
99
+ const tabs = [...BASE_TAB_DEFS];
100
+
101
+ if (TALENT_STAGES.includes(localPerson.value.stage)) {
102
+ tabs.splice(1, 0, {
103
+ id: "recruitment",
104
+ label: t("hr.tabs.recruitment"),
105
+ icon: "i-ph-clipboard-text-light",
106
+ });
107
+ }
108
+
109
+ if (EMPLOYEE_STAGES.includes(localPerson.value.stage)) {
110
+ tabs.splice(1, 0, {
111
+ id: "employment",
112
+ label: t("hr.tabs.employment"),
113
+ icon: "i-ph-briefcase-light",
114
+ });
115
+ const empIdx = tabs.findIndex((tab) => tab.id === "employment");
116
+ tabs.splice(empIdx + 1, 0, {
117
+ id: "assets",
118
+ label: t("assets.tab_label"),
119
+ icon: "i-heroicons-computer-desktop",
120
+ });
121
+ const assetsIdx = tabs.findIndex((tab) => tab.id === "assets");
122
+ tabs.splice(assetsIdx + 1, 0, {
123
+ id: "provisioning",
124
+ label: t("hr.tabs.provisioning"),
125
+ icon: "i-ph-plugs-connected-light",
126
+ });
127
+ }
128
+
129
+ return tabs;
130
+ });
131
+
132
+ const tabItems = computed(() =>
133
+ allTabDefs.value.map((tab) => ({
134
+ label: tab.label,
135
+ icon: tab.icon,
136
+ value: tab.id,
137
+ })),
138
+ );
139
+ const activeTab = ref(props.enabledTabs[0] ?? "profile");
140
+
141
+ watch(
142
+ () => props.enabledTabs,
143
+ (tabs) => {
144
+ if (!tabs.includes(activeTab.value))
145
+ activeTab.value = tabs[0] ?? "profile";
146
+ },
147
+ );
148
+
149
+ const isRO = (tab: string) => props.readonlyTabs?.includes(tab) ?? false;
150
+
151
+ const subtitle = computed(() => {
152
+ const p = localPerson.value;
153
+ const bits: string[] = [];
154
+ if (p.job_title) bits.push(p.job_title);
155
+ if (p.desired_position && !p.job_title) bits.push(p.desired_position);
156
+ if (p.employee_id) bits.push(p.employee_id);
157
+ return bits.join(" · ");
158
+ });
159
+ </script>
160
+
161
+ <template>
162
+ <UDashboardPanel>
163
+ <template #header>
164
+ <UDashboardNavbar
165
+ :title="localPerson.display_name"
166
+ :ui="{ title: 'text-lg font-semibold' }"
167
+ >
168
+ <template #leading>
169
+ <UButton
170
+ icon="i-ph-arrow-left-light"
171
+ variant="ghost"
172
+ size="sm"
173
+ @click="navigateTo(backTo)"
174
+ />
175
+ </template>
176
+
177
+ <template #right>
178
+ <span
179
+ v-if="subtitle"
180
+ class="text-xs text-muted hidden lg:inline"
181
+ >{{ subtitle }}</span
182
+ >
183
+ <UButton
184
+ v-if="nextStageAction"
185
+ size="sm"
186
+ color="neutral"
187
+ variant="soft"
188
+ icon="i-ph-arrow-right-light"
189
+ :label="nextStageAction.label"
190
+ :disabled="acting"
191
+ @click="onStageAction"
192
+ />
193
+ <UButton
194
+ v-if="isTalent"
195
+ size="sm"
196
+ color="primary"
197
+ variant="solid"
198
+ icon="i-ph-user-switch-light"
199
+ :label="$t('hr.convert.action')"
200
+ :disabled="acting"
201
+ @click="openConvert"
202
+ />
203
+ </template>
204
+ </UDashboardNavbar>
205
+ </template>
206
+
207
+ <template #body>
208
+ <div class="flex flex-col h-full">
209
+ <UTabs
210
+ v-model="activeTab"
211
+ :items="tabItems"
212
+ variant="link"
213
+ :content="false"
214
+ />
215
+
216
+ <div class="flex-1 overflow-auto py-6">
217
+ <HrTabProfile
218
+ v-if="activeTab === 'profile'"
219
+ :person="localPerson"
220
+ :readonly="isRO('profile')"
221
+ />
222
+ <HrTabRecruitment
223
+ v-else-if="activeTab === 'recruitment'"
224
+ :person-id="localPerson.id"
225
+ />
226
+ <HrTabEmployment
227
+ v-else-if="activeTab === 'employment'"
228
+ :person="localPerson"
229
+ :readonly="isRO('employment')"
230
+ />
231
+ <HrPersonTabAssets
232
+ v-else-if="activeTab === 'assets'"
233
+ :person="localPerson"
234
+ :readonly="isRO('assets')"
235
+ />
236
+ <HrTabProvisioning
237
+ v-else-if="activeTab === 'provisioning'"
238
+ :person="localPerson"
239
+ :readonly="isRO('provisioning')"
240
+ />
241
+ <HrTabActivity
242
+ v-else-if="activeTab === 'activity'"
243
+ :person-id="localPerson.id"
244
+ :readonly="isRO('activity')"
245
+ />
246
+ </div>
247
+ </div>
248
+ </template>
249
+ </UDashboardPanel>
250
+
251
+ <UModal
252
+ :open="showConvert"
253
+ :title="$t('hr.convert.title')"
254
+ @update:open="showConvert = $event"
255
+ >
256
+ <template #body>
257
+ <div class="space-y-4">
258
+ <p class="text-sm text-muted">
259
+ {{ $t('hr.convert.description', { name: localPerson.display_name }) }}
260
+ </p>
261
+ <div class="flex items-center gap-2 text-sm">
262
+ <HrPersonStageBadge :stage="localPerson.stage" />
263
+ <UIcon name="i-ph-arrow-right-light" class="size-4 text-muted" />
264
+ <HrPersonStageBadge stage="probation" />
265
+ </div>
266
+ <ul class="text-xs text-muted list-disc pl-5 space-y-1">
267
+ <li>{{ $t('hr.convert.note_employee_id') }}</li>
268
+ <li>{{ $t('hr.convert.note_moved') }}</li>
269
+ </ul>
270
+ <UFormField :label="$t('hr.convert.start_date')" required>
271
+ <UInput v-model="convertForm.start_date" type="date" class="w-full" />
272
+ </UFormField>
273
+ <UFormField :label="$t('hr.convert.reason')">
274
+ <UTextarea
275
+ v-model="convertForm.reason"
276
+ :rows="2"
277
+ :placeholder="$t('hr.stages.reason_placeholder')"
278
+ class="w-full"
279
+ />
280
+ </UFormField>
281
+ </div>
282
+ </template>
283
+ <template #footer>
284
+ <div class="flex justify-end gap-2">
285
+ <UButton
286
+ color="neutral"
287
+ variant="outline"
288
+ :label="$t('common.cancel')"
289
+ :disabled="acting"
290
+ @click="showConvert = false"
291
+ />
292
+ <UButton
293
+ color="primary"
294
+ icon="i-ph-user-switch-light"
295
+ :label="$t('hr.convert.action')"
296
+ :loading="acting"
297
+ :disabled="!convertForm.start_date"
298
+ @click="onConvertConfirm"
299
+ />
300
+ </div>
301
+ </template>
302
+ </UModal>
303
+ </template>
@@ -0,0 +1,120 @@
1
+ <script lang="ts" setup>
2
+ import HrInfoSection from "../../shared/section.vue"
3
+
4
+ const props = defineProps<{ personId: string, readonly?: boolean }>()
5
+
6
+ const { t } = useI18n()
7
+ const toast = useToast()
8
+ const api = useHrApi()
9
+
10
+ const documents = ref<any[]>([])
11
+ const loading = ref(false)
12
+
13
+ const STATUS_COLOR: Record<string, "success" | "warning" | "error" | "neutral"> = {
14
+ generated: "success",
15
+ signed: "success",
16
+ pending: "warning",
17
+ expired: "error",
18
+ archived: "neutral",
19
+ }
20
+
21
+ async function fetchDocuments() {
22
+ loading.value = true
23
+ try {
24
+ documents.value = await api.listDocuments(props.personId)
25
+ }
26
+ catch { /* noop */ }
27
+ finally {
28
+ loading.value = false
29
+ }
30
+ }
31
+
32
+ async function deleteDocument(docId: string) {
33
+ try {
34
+ await api.deleteDocument(props.personId, docId)
35
+ documents.value = documents.value.filter(d => d.id !== docId)
36
+ toast.add({ title: t('hr.documents.toast.deleted'), color: 'success' })
37
+ }
38
+ catch (err: any) {
39
+ toast.add({ title: err.message ?? t('hr.toast.error'), color: 'error' })
40
+ }
41
+ }
42
+
43
+ onMounted(fetchDocuments)
44
+ </script>
45
+
46
+ <template>
47
+ <div class="p-6 max-w-4xl space-y-6">
48
+ <HrInfoSection
49
+ :title="t('hr.documents.title')"
50
+ icon="i-ph-folder-light"
51
+ >
52
+ <div
53
+ v-if="loading"
54
+ class="p-6 text-muted text-sm"
55
+ >
56
+ {{ t('common.loading') }}
57
+ </div>
58
+ <div
59
+ v-else-if="!documents.length"
60
+ class="p-6 text-muted text-sm"
61
+ >
62
+ {{ t('hr.documents.empty') }}
63
+ </div>
64
+ <div
65
+ v-else
66
+ class="divide-y divide-default"
67
+ >
68
+ <div
69
+ v-for="d in documents"
70
+ :key="d.id"
71
+ class="flex items-center gap-3 px-4 py-3 hover:bg-elevated/30 transition-colors"
72
+ >
73
+ <UIcon
74
+ name="i-ph-file-text-light"
75
+ class="size-5 text-muted shrink-0"
76
+ />
77
+ <div class="flex-1 min-w-0">
78
+ <div class="font-medium text-default truncate">
79
+ {{ d.name }}
80
+ </div>
81
+ <div class="text-xs text-muted">
82
+ {{ d.category }} · {{ d.date_created }}
83
+ </div>
84
+ </div>
85
+ <UBadge
86
+ :color="STATUS_COLOR[d.status] ?? 'neutral'"
87
+ variant="subtle"
88
+ size="md"
89
+ >
90
+ {{ t(`hr.documents.status.${d.status}`) }}
91
+ </UBadge>
92
+ <UBadge
93
+ v-if="d.status === 'generated'"
94
+ color="info"
95
+ variant="subtle"
96
+ size="md"
97
+ >
98
+ {{ t('hr.documents.status.generated') }}
99
+ </UBadge>
100
+ <UButton
101
+ v-if="d.file?.id"
102
+ :to="`/assets/${d.file.id}`"
103
+ target="_blank"
104
+ size="xs"
105
+ variant="ghost"
106
+ icon="i-ph-download-simple-light"
107
+ />
108
+ <UButton
109
+ v-if="!readonly"
110
+ size="xs"
111
+ variant="ghost"
112
+ color="error"
113
+ icon="i-ph-trash-light"
114
+ @click="deleteDocument(d.id)"
115
+ />
116
+ </div>
117
+ </div>
118
+ </HrInfoSection>
119
+ </div>
120
+ </template>