@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,215 @@
1
+ <script lang="ts" setup>
2
+ import { renderTemplateHtml } from '../../../../../shared/utils/template-render'
3
+ import { PLACEHOLDER_HINTS_PERSON } from '../../../../../shared/utils/template-render'
4
+
5
+ const props = defineProps<{ open: boolean, templateId: string | null }>()
6
+ const emit = defineEmits<{ "update:open": [v: boolean], "archived": [] }>()
7
+
8
+ const { t } = useI18n()
9
+ const toast = useToast()
10
+ const { templates, fetchTemplates, saveTemplate, archiveTemplate } = useTemplates()
11
+
12
+ const template = computed<any | undefined>(() =>
13
+ props.templateId ? templates.value.find((tpl: any) => tpl.id === props.templateId) : undefined,
14
+ )
15
+
16
+ const SAMPLE_CONTEXT = {
17
+ person: {
18
+ full_name: 'Nguyen Van A',
19
+ employee_id: 'EMP-2026-001',
20
+ job_title: 'Software Engineer',
21
+ department: 'Engineering',
22
+ start_date: '2026-01-15',
23
+ probation_end: '2026-04-15',
24
+ email: 'nguyen.van.a@example.com',
25
+ },
26
+ today: new Date().toISOString().slice(0, 10),
27
+ company: { name: 'Your Company' },
28
+ }
29
+
30
+ const previewHtml = computed(() =>
31
+ template.value ? renderTemplateHtml(template.value.body_markdown ?? '', SAMPLE_CONTEXT) : '',
32
+ )
33
+
34
+ const CATEGORIES = [
35
+ { label: t('hr.templates.categories.general'), value: 'general' },
36
+ { label: t('hr.templates.categories.recruitment'), value: 'recruitment' },
37
+ { label: t('hr.templates.categories.onboarding'), value: 'onboarding' },
38
+ { label: t('hr.templates.categories.employment'), value: 'employment' },
39
+ { label: t('hr.templates.categories.offboarding'), value: 'offboarding' },
40
+ { label: t('hr.templates.categories.assets'), value: 'assets' },
41
+ ]
42
+
43
+ const form = ref<{ name: string, category: string, body_markdown: string }>({
44
+ name: '',
45
+ category: 'general',
46
+ body_markdown: '',
47
+ })
48
+
49
+ watch(() => template.value, (tpl) => {
50
+ if (tpl) {
51
+ form.value = {
52
+ name: tpl.name ?? '',
53
+ category: tpl.category ?? 'general',
54
+ body_markdown: tpl.body_markdown ?? '',
55
+ }
56
+ }
57
+ }, { immediate: true })
58
+
59
+ const saving = ref(false)
60
+
61
+ async function onSave() {
62
+ if (!template.value) return
63
+ saving.value = true
64
+ try {
65
+ await saveTemplate(template.value.id, form.value)
66
+ await fetchTemplates()
67
+ toast.add({ title: t('hr.templates.toast.saved'), color: 'success' })
68
+ }
69
+ catch (err: any) {
70
+ toast.add({ title: err.message ?? t('hr.toast.error'), color: 'error' })
71
+ }
72
+ finally {
73
+ saving.value = false
74
+ }
75
+ }
76
+
77
+ async function onArchive() {
78
+ if (!template.value) return
79
+ try {
80
+ await archiveTemplate(template.value.id)
81
+ await fetchTemplates()
82
+ toast.add({ title: t('hr.templates.toast.archived'), color: 'success' })
83
+ emit('archived')
84
+ emit('update:open', false)
85
+ }
86
+ catch (err: any) {
87
+ toast.add({ title: err.message ?? t('hr.toast.error'), color: 'error' })
88
+ }
89
+ }
90
+ </script>
91
+
92
+ <template>
93
+ <USlideover
94
+ :open="open"
95
+ :ui="{ content: 'w-full sm:max-w-[1000px]' }"
96
+ @update:open="(v) => emit('update:open', v)"
97
+ >
98
+ <template #header>
99
+ <div class="flex items-center justify-between w-full">
100
+ <div class="flex items-center gap-2">
101
+ <UIcon
102
+ name="i-ph-file-doc-light"
103
+ class="size-5 text-muted"
104
+ />
105
+ <h2 class="text-base font-semibold">
106
+ {{ template?.name ?? t('hr.templates.edit') }}
107
+ </h2>
108
+ <UBadge
109
+ v-if="template"
110
+ :label="template.category"
111
+ variant="subtle"
112
+ color="neutral"
113
+ size="md"
114
+ />
115
+ <UBadge
116
+ v-if="template?.is_archived"
117
+ :label="t('hr.documents.status.archived')"
118
+ color="warning"
119
+ variant="subtle"
120
+ size="md"
121
+ />
122
+ </div>
123
+ </div>
124
+ </template>
125
+
126
+ <template #body>
127
+ <div
128
+ v-if="template"
129
+ class="grid grid-cols-2 gap-4 h-full"
130
+ >
131
+ <!-- Editor side -->
132
+ <div class="space-y-3 overflow-y-auto pr-1">
133
+ <UFormField :label="t('hr.templates.fields.name')">
134
+ <UInput
135
+ v-model="form.name"
136
+ size="sm"
137
+ />
138
+ </UFormField>
139
+ <UFormField :label="t('hr.templates.fields.category')">
140
+ <USelect
141
+ v-model="form.category"
142
+ :items="CATEGORIES"
143
+ value-key="value"
144
+ label-key="label"
145
+ size="sm"
146
+ />
147
+ </UFormField>
148
+ <UFormField :label="t('hr.templates.fields.body')">
149
+ <UTextarea
150
+ v-model="form.body_markdown"
151
+ :rows="22"
152
+ class="font-mono text-xs"
153
+ />
154
+ </UFormField>
155
+ <details class="text-xs">
156
+ <summary class="cursor-pointer text-muted hover:text-default">
157
+ {{ t('hr.templates.placeholder_hint') }}
158
+ </summary>
159
+ <div class="mt-2 flex flex-wrap gap-1.5">
160
+ <code
161
+ v-for="ph in PLACEHOLDER_HINTS_PERSON"
162
+ :key="ph"
163
+ class="bg-elevated px-1.5 py-0.5 rounded text-xs"
164
+ >{{ ph }}</code>
165
+ </div>
166
+ </details>
167
+ </div>
168
+
169
+ <!-- Preview side -->
170
+ <div class="space-y-2 overflow-y-auto">
171
+ <div class="text-xs text-muted">
172
+ {{ t('hr.templates.preview') }} · {{ SAMPLE_CONTEXT.person.full_name }}
173
+ </div>
174
+ <div
175
+ class="rounded-lg border border-default bg-default p-4 prose prose-sm max-w-none"
176
+ v-html="previewHtml"
177
+ />
178
+ </div>
179
+ </div>
180
+ <div
181
+ v-else
182
+ class="p-8 text-center text-muted text-sm"
183
+ >
184
+ {{ t('common.no_data') }}
185
+ </div>
186
+ </template>
187
+
188
+ <template #footer>
189
+ <div class="flex items-center justify-between w-full">
190
+ <UButton
191
+ v-if="template && !template.is_archived"
192
+ size="sm"
193
+ color="neutral"
194
+ variant="soft"
195
+ icon="i-ph-archive-light"
196
+ :label="t('common.delete')"
197
+ @click="onArchive"
198
+ />
199
+ <div class="flex-1" />
200
+ <UButton
201
+ size="sm"
202
+ variant="outline"
203
+ :label="t('common.close')"
204
+ @click="emit('update:open', false)"
205
+ />
206
+ <UButton
207
+ size="sm"
208
+ :label="t('common.save')"
209
+ :loading="saving"
210
+ @click="onSave"
211
+ />
212
+ </div>
213
+ </template>
214
+ </USlideover>
215
+ </template>
@@ -0,0 +1,39 @@
1
+ <script lang="ts" setup>
2
+ import { renderTemplateHtml } from '../../../../../shared/utils/template-render'
3
+
4
+ const props = defineProps<{
5
+ template: any
6
+ selected: boolean
7
+ }>()
8
+ const emit = defineEmits<{ "update:selected": [v: boolean] }>()
9
+
10
+ const previewRef = ref<HTMLElement | null>(null)
11
+
12
+ const html = computed(() => renderTemplateHtml(props.template.body_markdown ?? '', {}))
13
+
14
+ defineExpose({ getEl: () => previewRef.value })
15
+ </script>
16
+
17
+ <template>
18
+ <div class="border border-default rounded">
19
+ <div class="flex items-center justify-between px-3 py-2 border-b border-default bg-elevated">
20
+ <label class="flex items-center gap-2 text-sm">
21
+ <UCheckbox
22
+ :model-value="selected"
23
+ @update:model-value="(v) => emit('update:selected', !!v)"
24
+ />
25
+ <span class="font-medium">{{ template.name }}</span>
26
+ <UBadge
27
+ variant="subtle"
28
+ color="neutral"
29
+ size="md"
30
+ >{{ template.category }}</UBadge>
31
+ </label>
32
+ </div>
33
+ <div
34
+ ref="previewRef"
35
+ class="p-4 prose prose-sm max-w-none"
36
+ v-html="html"
37
+ />
38
+ </div>
39
+ </template>
@@ -0,0 +1,121 @@
1
+ <script lang="ts" setup>
2
+ import HrTemplatePreviewCard from "./template-preview-card.vue"
3
+
4
+ const props = defineProps<{
5
+ open: boolean
6
+ event: string
7
+ personId: string
8
+ }>()
9
+ const emit = defineEmits<{ close: [] }>()
10
+
11
+ const { t } = useI18n()
12
+ const toast = useToast()
13
+ const api = useHrApi()
14
+
15
+ const { triggers, fetchTriggers } = useTriggers()
16
+ const { templates, fetchTemplates } = useTemplates()
17
+
18
+ const trigger = computed(() => triggers.value.find(tr => tr.event_key === props.event))
19
+ const templateList = computed(() =>
20
+ (trigger.value?.template_ids ?? [])
21
+ .map((id: string) => templates.value.find((tpl: any) => tpl.id === id))
22
+ .filter((t: any): t is NonNullable<typeof t> => !!t),
23
+ )
24
+
25
+ const selected = ref<Record<string, boolean>>({})
26
+ const generating = ref(false)
27
+
28
+ watch(() => props.open, (v) => {
29
+ if (v) {
30
+ fetchTriggers()
31
+ fetchTemplates()
32
+ selected.value = Object.fromEntries(templateList.value.map((t: any) => [t.id, true]))
33
+ }
34
+ })
35
+
36
+ watch(templateList, (list) => {
37
+ selected.value = Object.fromEntries(list.map((t: any) => [t.id, true]))
38
+ })
39
+
40
+ const eventLabel: Record<string, string> = {
41
+ on_offer_sent: t('hr.triggers.events.on_offer_sent'),
42
+ on_hire: t('hr.triggers.events.on_hire'),
43
+ on_probation_end: t('hr.triggers.events.on_probation_end'),
44
+ on_resign: t('hr.triggers.events.on_resign'),
45
+ on_asset_issued: t('hr.triggers.events.on_asset_issued'),
46
+ on_asset_returned: t('hr.triggers.events.on_asset_returned'),
47
+ }
48
+
49
+ const selectedCount = computed(() => Object.values(selected.value).filter(v => v).length)
50
+
51
+ async function generateAll() {
52
+ generating.value = true
53
+ try {
54
+ for (const tpl of templateList.value) {
55
+ if (!selected.value[tpl.id]) continue
56
+ await api.generateDocument(props.personId, tpl.id)
57
+ }
58
+ toast.add({ title: t('hr.documents.toast.generated'), color: 'success' })
59
+ emit('close')
60
+ }
61
+ catch (err: any) {
62
+ toast.add({ title: err.message ?? t('hr.toast.error'), color: 'error' })
63
+ }
64
+ finally {
65
+ generating.value = false
66
+ }
67
+ }
68
+ </script>
69
+
70
+ <template>
71
+ <UModal
72
+ :open="open"
73
+ :ui="{ content: 'sm:max-w-4xl' }"
74
+ @update:open="(v) => !v && emit('close')"
75
+ >
76
+ <template #content>
77
+ <UCard>
78
+ <div class="space-y-3 text-sm">
79
+ <h3 class="font-semibold">
80
+ {{ t('hr.documents.trigger_modal.title') }} — {{ eventLabel[event] ?? event }}
81
+ </h3>
82
+ <p
83
+ v-if="!templateList.length"
84
+ class="text-muted"
85
+ >
86
+ {{ t('hr.documents.trigger_modal.hint') }}
87
+ </p>
88
+ <div
89
+ v-else
90
+ class="space-y-3 max-h-[60vh] overflow-y-auto pr-2"
91
+ >
92
+ <HrTemplatePreviewCard
93
+ v-for="tpl in templateList"
94
+ :key="tpl.id"
95
+ :template="tpl"
96
+ :selected="!!selected[tpl.id]"
97
+ @update:selected="(v) => (selected[tpl.id] = v)"
98
+ />
99
+ </div>
100
+ <div class="flex justify-end gap-2 pt-2">
101
+ <UButton
102
+ variant="soft"
103
+ color="neutral"
104
+ @click="emit('close')"
105
+ >
106
+ {{ t('common.cancel') }}
107
+ </UButton>
108
+ <UButton
109
+ :disabled="!templateList.length || selectedCount === 0"
110
+ :loading="generating"
111
+ icon="i-ph-file-pdf-light"
112
+ @click="generateAll"
113
+ >
114
+ {{ t('hr.documents.trigger_modal.generate', { count: selectedCount }) }}
115
+ </UButton>
116
+ </div>
117
+ </div>
118
+ </UCard>
119
+ </template>
120
+ </UModal>
121
+ </template>
@@ -0,0 +1,78 @@
1
+ <script lang="ts" setup>
2
+ const open = defineModel<boolean>('open', { default: false })
3
+ const emit = defineEmits<{ created: [person: Person] }>()
4
+
5
+ const { t } = useI18n()
6
+ const toast = useToast()
7
+ const { createPerson } = usePeople()
8
+
9
+ const form = ref<Record<string, any>>({})
10
+ const saving = ref(false)
11
+
12
+ const EMPLOYEE_FIELD_NAMES = [
13
+ 'first_name', 'last_name', 'email', 'phone',
14
+ 'job_title', 'department_id', 'employment_type_id', 'start_date',
15
+ ]
16
+ const { pickFields, fetchAll, isReady } = useHrFieldRegistry(['hr_people'])
17
+
18
+ const formFields = computed(() => pickFields('hr_people', EMPLOYEE_FIELD_NAMES))
19
+
20
+ watch(open, (v) => {
21
+ if (v) {
22
+ form.value = {}
23
+ fetchAll()
24
+ }
25
+ })
26
+
27
+ const canSave = computed(() => !!form.value.first_name?.trim())
28
+
29
+ async function save() {
30
+ if (!canSave.value) return
31
+ saving.value = true
32
+ try {
33
+ const person = await createPerson({ ...form.value, stage: 'probation' })
34
+ toast.add({ title: t('hr.toast.saved'), color: 'success' })
35
+ open.value = false
36
+ emit('created', person)
37
+ }
38
+ catch {
39
+ toast.add({ title: t('hr.toast.error'), color: 'error' })
40
+ }
41
+ finally {
42
+ saving.value = false
43
+ }
44
+ }
45
+ </script>
46
+
47
+ <template>
48
+ <UModal v-model:open="open">
49
+ <template #content>
50
+ <div class="p-6 space-y-4">
51
+ <h3 class="text-base font-semibold">
52
+ {{ $t('hr.employees.actions.add') }}
53
+ </h3>
54
+
55
+ <div v-if="!isReady('hr_people')" class="py-6 text-sm text-muted text-center">
56
+ {{ $t('common.loading') }}
57
+ </div>
58
+ <FormRoot
59
+ v-else-if="formFields.length"
60
+ v-model="form"
61
+ :fields="formFields"
62
+ :initial-values="{}"
63
+ />
64
+
65
+ <div class="flex justify-end gap-2 pt-2">
66
+ <UButton :label="$t('common.cancel')" variant="ghost" @click="open = false" />
67
+ <UButton
68
+ :label="$t('common.save')"
69
+ color="primary"
70
+ :disabled="!canSave"
71
+ :loading="saving"
72
+ @click="save"
73
+ />
74
+ </div>
75
+ </div>
76
+ </template>
77
+ </UModal>
78
+ </template>
@@ -0,0 +1,78 @@
1
+ <script lang="ts" setup>
2
+ const open = defineModel<boolean>('open', { default: false })
3
+ const emit = defineEmits<{ saved: [person: Person] }>()
4
+
5
+ const { t } = useI18n()
6
+ const toast = useToast()
7
+ const { createPerson } = usePeople()
8
+
9
+ const form = ref<Record<string, any>>({})
10
+ const saving = ref(false)
11
+
12
+ const TALENT_FIELD_NAMES = [
13
+ 'first_name', 'last_name', 'email', 'phone',
14
+ 'desired_position', 'source',
15
+ ]
16
+ const { pickFields, fetchAll, isReady } = useHrFieldRegistry(['hr_people'])
17
+
18
+ const formFields = computed(() => pickFields('hr_people', TALENT_FIELD_NAMES))
19
+
20
+ watch(open, (v) => {
21
+ if (v) {
22
+ form.value = {}
23
+ fetchAll()
24
+ }
25
+ })
26
+
27
+ const canSave = computed(() => !!form.value.first_name?.trim())
28
+
29
+ async function save() {
30
+ if (!canSave.value) return
31
+ saving.value = true
32
+ try {
33
+ const person = await createPerson({ ...form.value, stage: 'talent' })
34
+ toast.add({ title: t('hr.toast.saved'), color: 'success' })
35
+ open.value = false
36
+ emit('saved', person)
37
+ }
38
+ catch {
39
+ toast.add({ title: t('hr.toast.error'), color: 'error' })
40
+ }
41
+ finally {
42
+ saving.value = false
43
+ }
44
+ }
45
+ </script>
46
+
47
+ <template>
48
+ <UModal v-model:open="open">
49
+ <template #content>
50
+ <div class="p-6 space-y-4">
51
+ <h3 class="text-base font-semibold">
52
+ {{ $t('hr.talents.actions.add') }}
53
+ </h3>
54
+
55
+ <div v-if="!isReady('hr_people')" class="py-6 text-sm text-muted text-center">
56
+ {{ $t('common.loading') }}
57
+ </div>
58
+ <FormRoot
59
+ v-else-if="formFields.length"
60
+ v-model="form"
61
+ :fields="formFields"
62
+ :initial-values="{}"
63
+ />
64
+
65
+ <div class="flex justify-end gap-2 pt-2">
66
+ <UButton :label="$t('common.cancel')" variant="ghost" @click="open = false" />
67
+ <UButton
68
+ :label="$t('common.save')"
69
+ color="primary"
70
+ :disabled="!canSave"
71
+ :loading="saving"
72
+ @click="save"
73
+ />
74
+ </div>
75
+ </div>
76
+ </template>
77
+ </UModal>
78
+ </template>
@@ -0,0 +1,40 @@
1
+ <script lang="ts" setup>
2
+ import HrPersonStageBadge from "../shared/stage-badge.vue"
3
+
4
+ defineProps<{
5
+ person: Person
6
+ columns: ("source" | "desired_position" | "employee_id" | "job_title" | "start_date")[]
7
+ }>()
8
+ </script>
9
+
10
+ <template>
11
+ <NuxtLink
12
+ :to="['probation', 'active', 'resigned'].includes(person.stage) ? `/hr/employees/${person.id}` : `/hr/talents/${person.id}`"
13
+ class="grid grid-cols-12 gap-2 px-3 py-2 hover:bg-elevated rounded items-center text-sm"
14
+ >
15
+ <div class="col-span-3 font-medium truncate">{{ person.display_name }}</div>
16
+ <div
17
+ v-if="columns.includes('source')"
18
+ class="col-span-2 text-muted truncate"
19
+ >{{ person.source ?? '—' }}</div>
20
+ <div
21
+ v-if="columns.includes('desired_position')"
22
+ class="col-span-3 truncate"
23
+ >{{ person.desired_position ?? '—' }}</div>
24
+ <div
25
+ v-if="columns.includes('employee_id')"
26
+ class="col-span-2 text-muted truncate"
27
+ >{{ person.employee_id ?? '—' }}</div>
28
+ <div
29
+ v-if="columns.includes('job_title')"
30
+ class="col-span-2 truncate"
31
+ >{{ person.job_title ?? '—' }}</div>
32
+ <div
33
+ v-if="columns.includes('start_date')"
34
+ class="col-span-2 text-muted"
35
+ >{{ person.start_date ?? '—' }}</div>
36
+ <div class="col-span-2 flex justify-end">
37
+ <HrPersonStageBadge :stage="person.stage" />
38
+ </div>
39
+ </NuxtLink>
40
+ </template>