@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,238 @@
1
+ <template>
2
+ <div class="p-6 max-w-4xl space-y-6">
3
+ <template v-if="employee">
4
+ <!-- Status Banner -->
5
+ <div
6
+ class="flex items-center gap-4 p-4 rounded-lg border"
7
+ :class="employee.stage === 'active'
8
+ ? 'bg-success/5 border-success/20'
9
+ : employee.stage === 'probation'
10
+ ? 'bg-warning/5 border-warning/20'
11
+ : 'bg-neutral/5 border-default'
12
+ "
13
+ >
14
+ <UAvatar
15
+ icon="i-ph-identification-badge-light"
16
+ size="lg"
17
+ class="bg-primary/10 text-primary"
18
+ />
19
+ <div class="flex-1 min-w-0">
20
+ <div class="flex items-center gap-2">
21
+ <h3 class="font-semibold text-default">
22
+ {{ employee.employee_id }}
23
+ </h3>
24
+ <UBadge
25
+ :label="$t(`hr.stages.${employee.stage}`)"
26
+ :color="employee.stage === 'active' ? 'success' : employee.stage === 'probation' ? 'warning' : 'neutral'"
27
+ variant="subtle"
28
+ size="md"
29
+ />
30
+ </div>
31
+ <p class="text-sm text-muted mt-0.5">
32
+ {{ employee.department?.name }} &middot; {{ employee.job_title }} &middot; {{ employee.employment_type }}
33
+ </p>
34
+ <p class="text-xs text-muted mt-1">
35
+ {{ $t('contacts.hr.start_date') }}: {{ employee.start_date }}
36
+ </p>
37
+ </div>
38
+ <UButton
39
+ icon="i-ph-arrow-square-out-light"
40
+ :label="$t('contacts.hr.full_profile')"
41
+ variant="outline"
42
+ size="sm"
43
+ @click="navigateTo(`/hr/employees/${employee.id}`)"
44
+ />
45
+ </div>
46
+
47
+ <!-- Summary grid -->
48
+ <div
49
+ v-if="isEmployee"
50
+ class="grid grid-cols-1 lg:grid-cols-3 gap-4"
51
+ >
52
+ <!-- Contract summary -->
53
+ <div class="border border-default rounded-lg p-4">
54
+ <div class="flex items-center gap-2 mb-3">
55
+ <UIcon
56
+ name="i-ph-file-text-light"
57
+ class="size-4 text-muted"
58
+ />
59
+ <span class="text-sm font-medium text-default">{{ $t('contacts.hr.contract') }}</span>
60
+ </div>
61
+ <div class="space-y-2 text-sm">
62
+ <div class="flex justify-between">
63
+ <span class="text-muted">{{ $t('hr.contract.fields.contract_type') }}</span>
64
+ <span class="text-default font-medium">{{ contract?.contract_type }}</span>
65
+ </div>
66
+ <div class="flex justify-between">
67
+ <span class="text-muted">{{ $t('contacts.hr.expires') }}</span>
68
+ <span class="text-default">{{ contract?.end_date || 'Indefinite' }}</span>
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Compensation summary -->
74
+ <div class="border border-default rounded-lg p-4">
75
+ <div class="flex items-center gap-2 mb-3">
76
+ <UIcon
77
+ name="i-ph-money-light"
78
+ class="size-4 text-muted"
79
+ />
80
+ <span class="text-sm font-medium text-default">{{ $t('hr.compensation.title') }}</span>
81
+ </div>
82
+ <div class="space-y-2 text-sm">
83
+ <div class="flex justify-between">
84
+ <span class="text-muted">{{ $t('hr.compensation.fields.base_salary') }}</span>
85
+ <span class="text-default font-semibold">{{ compensation?.base_salary }}</span>
86
+ </div>
87
+ <div class="flex justify-between">
88
+ <span class="text-muted">{{ $t('hr.compensation.fields.bonus_target') }}</span>
89
+ <span class="text-default">{{ compensation?.bonus_target }}</span>
90
+ </div>
91
+ </div>
92
+ </div>
93
+
94
+ <!-- Insurance summary -->
95
+ <div class="border border-default rounded-lg p-4">
96
+ <div class="flex items-center gap-2 mb-3">
97
+ <UIcon
98
+ name="i-ph-shield-check-light"
99
+ class="size-4 text-muted"
100
+ />
101
+ <span class="text-sm font-medium text-default">{{ $t('hr.insurance.title') }}</span>
102
+ </div>
103
+ <div class="space-y-2 text-sm">
104
+ <div class="flex justify-between">
105
+ <span class="text-muted">{{ $t('hr.insurance.social') }}</span>
106
+ <span class="text-default font-mono">{{ insurance?.social_insurance_number }}</span>
107
+ </div>
108
+ <div class="flex justify-between">
109
+ <span class="text-muted">{{ $t('hr.insurance.fields.tax_code') }}</span>
110
+ <span class="text-default font-mono">{{ insurance?.tax_code }}</span>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+
116
+ <!-- Leave Balance compact (employee only) -->
117
+ <div
118
+ v-if="isEmployee && leaveBalances && leaveBalances.length"
119
+ class="border border-default rounded-lg"
120
+ >
121
+ <div class="px-4 py-3 border-b border-default bg-elevated/30">
122
+ <h3 class="text-sm font-medium flex items-center gap-2">
123
+ <UIcon
124
+ name="i-ph-calendar-check-light"
125
+ class="size-4 text-muted"
126
+ />
127
+ {{ $t('contacts.hr.leave_balance') }}
128
+ </h3>
129
+ </div>
130
+ <div class="grid grid-cols-2 sm:grid-cols-4 divide-x divide-default">
131
+ <div
132
+ v-for="leave in leaveBalances"
133
+ :key="leave.leave_type_id"
134
+ class="p-3 text-center"
135
+ >
136
+ <div
137
+ class="text-xl font-bold"
138
+ :class="(leave.allocated - leave.used) <= 2 ? 'text-error' : 'text-default'"
139
+ >
140
+ {{ leave.allocated - leave.used }}
141
+ </div>
142
+ <div class="text-xs text-muted mt-0.5">
143
+ {{ leave.leave_type?.name }}
144
+ </div>
145
+ <div class="w-full bg-elevated rounded-full h-1 mt-1.5">
146
+ <div
147
+ class="h-1 rounded-full"
148
+ :class="(leave.allocated - leave.used) <= 2 ? 'bg-error' : 'bg-primary'"
149
+ :style="{ width: `${leave.allocated > 0 ? (leave.used / leave.allocated) * 100 : 0}%` }"
150
+ />
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ <div class="text-xs text-muted italic border-t border-default pt-4">
157
+ {{ $t('contacts.hr.full_profile') }} &middot;
158
+ <NuxtLink
159
+ :to="`/hr/employees/${employee.id}`"
160
+ class="text-primary hover:underline"
161
+ >
162
+ {{ $t('contacts.hr.full_profile') }}
163
+ </NuxtLink>
164
+ </div>
165
+ </template>
166
+
167
+ <!-- No HR record -->
168
+ <div
169
+ v-else-if="!pending"
170
+ class="flex flex-col items-center justify-center py-12 text-center"
171
+ >
172
+ <UIcon
173
+ name="i-ph-identification-badge-light"
174
+ class="size-10 text-muted opacity-30 mb-3"
175
+ />
176
+ <p class="text-sm text-muted">
177
+ {{ $t('contacts.hr.not_linked') }}
178
+ </p>
179
+ </div>
180
+
181
+ <!-- Loading -->
182
+ <div
183
+ v-else
184
+ class="flex justify-center py-12"
185
+ >
186
+ <UIcon
187
+ name="i-ph-spinner-light"
188
+ class="size-8 text-muted animate-spin"
189
+ />
190
+ </div>
191
+ </div>
192
+ </template>
193
+
194
+ <script lang="ts" setup>
195
+ const props = defineProps<{
196
+ contact: Record<string, any>
197
+ }>()
198
+
199
+ const api = useHrApi()
200
+
201
+ const personId = computed(() => props.contact?.hr_person_id ?? null)
202
+ const isEmployee = computed(() => ['probation', 'active', 'resigned'].includes(employee.value?.stage ?? ''))
203
+
204
+ const employee = useState<Person | null>(`hr:contact-tab:person:${props.contact?.id}`, () => null)
205
+ const contract = useState<any | null>(`hr:contact-tab:contract:${props.contact?.id}`, () => null)
206
+ const compensation = useState<any | null>(`hr:contact-tab:compensation:${props.contact?.id}`, () => null)
207
+ const insurance = useState<any | null>(`hr:contact-tab:insurance:${props.contact?.id}`, () => null)
208
+ const leaveBalances = useState<any[] | null>(`hr:contact-tab:leave:${props.contact?.id}`, () => null)
209
+ const pending = ref(false)
210
+
211
+ async function loadData() {
212
+ if (!personId.value) return
213
+ pending.value = true
214
+ try {
215
+ employee.value = await api.getPerson(personId.value)
216
+ if (isEmployee.value) {
217
+ const [c, comp, ins, lb] = await Promise.allSettled([
218
+ api.getContract(personId.value),
219
+ api.getCompensation(personId.value),
220
+ api.getInsurance(personId.value),
221
+ $api<any>(`/hr/people/${personId.value}/leave-balances`),
222
+ ])
223
+ if (c.status === 'fulfilled') contract.value = c.value
224
+ if (comp.status === 'fulfilled') compensation.value = comp.value
225
+ if (ins.status === 'fulfilled') insurance.value = ins.value
226
+ if (lb.status === 'fulfilled') {
227
+ const raw = lb.value
228
+ leaveBalances.value = Array.isArray(raw) ? raw : (raw?.data ?? [])
229
+ }
230
+ }
231
+ }
232
+ finally {
233
+ pending.value = false
234
+ }
235
+ }
236
+
237
+ onMounted(loadData)
238
+ </script>
@@ -0,0 +1,141 @@
1
+ <template>
2
+ <div class="border border-default rounded-lg hover:border-primary/30 transition-colors overflow-hidden">
3
+ <!-- Colored top border accent -->
4
+ <div
5
+ class="h-1 w-full"
6
+ :style="{ backgroundColor: department.color || '#6366f1' }"
7
+ />
8
+
9
+ <div class="p-4 border-b border-default">
10
+ <div class="flex items-center justify-between gap-3">
11
+ <div class="flex items-center gap-3 min-w-0">
12
+ <div
13
+ class="p-2 rounded-lg shrink-0"
14
+ :style="{ backgroundColor: (department.color || '#6366f1') + '15', color: department.color || '#6366f1' }"
15
+ >
16
+ <UIcon
17
+ :name="department.icon || 'i-heroicons-building-office'"
18
+ class="size-5"
19
+ />
20
+ </div>
21
+ <div class="min-w-0">
22
+ <h3 class="font-semibold text-default truncate">
23
+ {{ department.name }}
24
+ </h3>
25
+ <p
26
+ v-if="department.description"
27
+ class="text-xs text-muted truncate"
28
+ >
29
+ {{ department.description }}
30
+ </p>
31
+ </div>
32
+ </div>
33
+ <UDropdownMenu
34
+ :items="menuItems"
35
+ >
36
+ <UButton
37
+ icon="i-ph-dots-three-vertical"
38
+ color="neutral"
39
+ variant="ghost"
40
+ size="xs"
41
+ />
42
+ </UDropdownMenu>
43
+ </div>
44
+ </div>
45
+
46
+ <div class="grid grid-cols-2 divide-x divide-default">
47
+ <div class="p-3 text-center">
48
+ <div class="text-lg font-bold text-default">
49
+ {{ department.member_count ?? 0 }}
50
+ </div>
51
+ <div class="text-xs text-muted">
52
+ {{ $t('departments.stats.members', { count: '' }).trim() }}
53
+ </div>
54
+ </div>
55
+ <div class="p-3 text-center">
56
+ <div class="text-lg font-bold text-default">
57
+ {{ formatTenure(department.avg_tenure_months ?? 0) }}
58
+ </div>
59
+ <div class="text-xs text-muted">
60
+ {{ $t('departments.stats.avg_tenure') }}
61
+ </div>
62
+ </div>
63
+ </div>
64
+
65
+ <div class="p-4 border-t border-default">
66
+ <div class="text-xs text-muted mb-2">
67
+ {{ $t('departments.head') }}
68
+ </div>
69
+ <div
70
+ v-if="department.head"
71
+ class="flex items-center gap-2"
72
+ >
73
+ <UAvatar
74
+ :src="department.head.avatar"
75
+ icon="i-ph-user-light"
76
+ size="xs"
77
+ class="bg-primary/10 text-primary"
78
+ />
79
+ <NuxtLink
80
+ :to="`/hr/employees/${department.head.id}`"
81
+ class="text-sm font-medium text-default hover:text-primary"
82
+ >
83
+ {{ department.head.full_name }}
84
+ </NuxtLink>
85
+ </div>
86
+ <div
87
+ v-else
88
+ class="text-sm text-muted"
89
+ >
90
+ {{ $t('departments.no_head') }}
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </template>
95
+
96
+ <script lang="ts" setup>
97
+ const { t } = useI18n()
98
+
99
+ const props = defineProps<{
100
+ department: {
101
+ id: number
102
+ name: string
103
+ description?: string
104
+ color?: string
105
+ icon?: string
106
+ member_count?: number
107
+ avg_tenure_months?: number
108
+ head?: { id: number, full_name: string, avatar?: string } | null
109
+ }
110
+ }>()
111
+
112
+ const emit = defineEmits<{
113
+ edit: []
114
+ delete: []
115
+ }>()
116
+
117
+ const menuItems = computed(() => [
118
+ [
119
+ {
120
+ label: t('common.edit'),
121
+ icon: 'i-ph-pencil-light',
122
+ onSelect: () => emit('edit'),
123
+ },
124
+ {
125
+ label: t('common.delete'),
126
+ icon: 'i-ph-trash-light',
127
+ color: 'error' as const,
128
+ onSelect: () => emit('delete'),
129
+ },
130
+ ],
131
+ ])
132
+
133
+ function formatTenure(months: number): string {
134
+ if (months < 1) return t('departments.tenure.less_than_month')
135
+ const years = Math.floor(months / 12)
136
+ const rem = months % 12
137
+ if (years === 0) return t('departments.tenure.months', { count: months })
138
+ if (rem === 0) return t('departments.tenure.years', { count: years })
139
+ return t('departments.tenure.years_months', { years, months: rem })
140
+ }
141
+ </script>
@@ -0,0 +1,90 @@
1
+ <script lang="ts" setup>
2
+ const { t } = useI18n()
3
+ const toast = useToast()
4
+ const { saveDepartment } = useDepartments()
5
+
6
+ const props = defineProps<{
7
+ department?: any | null
8
+ }>()
9
+
10
+ const emit = defineEmits<{
11
+ close: []
12
+ saved: []
13
+ }>()
14
+
15
+ const DEPT_FIELD_NAMES = ['name', 'code', 'description', 'head_person_id', 'parent_department_id']
16
+ const { pickFields, fetchAll, isReady } = useHrFieldRegistry(['hr_departments'])
17
+
18
+ const formFields = computed(() => pickFields('hr_departments', DEPT_FIELD_NAMES))
19
+
20
+ const form = ref<Record<string, any>>({})
21
+ const saving = ref(false)
22
+
23
+ const isEdit = computed(() => !!props.department)
24
+ const canSave = computed(() => !!form.value.name?.trim())
25
+
26
+ watch(() => props.department, (dept) => {
27
+ form.value = dept
28
+ ? { name: dept.name, code: dept.code, description: dept.description, head_person_id: dept.head_person_id, parent_department_id: dept.parent_department_id }
29
+ : {}
30
+ }, { immediate: true })
31
+
32
+ onMounted(fetchAll)
33
+
34
+ async function handleSave() {
35
+ if (!canSave.value) return
36
+ saving.value = true
37
+ try {
38
+ await saveDepartment(props.department?.id ?? null, { ...form.value })
39
+ toast.add({
40
+ title: isEdit.value ? t('departments.toast.updated') : t('departments.toast.created'),
41
+ color: 'success',
42
+ })
43
+ emit('saved')
44
+ }
45
+ catch (err: any) {
46
+ toast.add({ title: err?.message ?? t('hr.toast.error'), color: 'error' })
47
+ }
48
+ finally {
49
+ saving.value = false
50
+ }
51
+ }
52
+ </script>
53
+
54
+ <template>
55
+ <UModal
56
+ :open="true"
57
+ :title="isEdit ? $t('departments.edit') : $t('departments.create')"
58
+ @close="$emit('close')"
59
+ >
60
+ <template #body>
61
+ <div v-if="!isReady('hr_departments')" class="py-6 text-sm text-muted text-center">
62
+ {{ $t('common.loading') }}
63
+ </div>
64
+ <FormRoot
65
+ v-else-if="formFields.length"
66
+ v-model="form"
67
+ :fields="formFields"
68
+ :initial-values="department ?? {}"
69
+ />
70
+ </template>
71
+
72
+ <template #footer>
73
+ <div class="flex justify-end gap-2">
74
+ <UButton
75
+ color="neutral"
76
+ variant="outline"
77
+ :label="$t('common.cancel')"
78
+ @click="$emit('close')"
79
+ />
80
+ <UButton
81
+ color="primary"
82
+ :label="isEdit ? $t('common.save') : $t('departments.create')"
83
+ :loading="saving"
84
+ :disabled="!canSave"
85
+ @click="handleSave"
86
+ />
87
+ </div>
88
+ </template>
89
+ </UModal>
90
+ </template>