@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,231 @@
1
+ <script lang="ts" setup>
2
+ import HrInfoSection from "../../shared/section.vue";
3
+ import HrInfoRow from "../../shared/row.vue";
4
+ import HrPersonStageBadge from "../../shared/stage-badge.vue";
5
+
6
+ const props = defineProps<{ person: Person; readonly?: boolean }>();
7
+ const emit = defineEmits<{ updated: [person: Person] }>();
8
+
9
+ const api = useHrApi();
10
+ const toast = useToast();
11
+ const { t } = useI18n();
12
+
13
+ const editing = ref(false);
14
+ const saving = ref(false);
15
+ const edits = ref<Record<string, any>>({});
16
+
17
+ const initials = computed(() => {
18
+ const parts = props.person.display_name.trim().split(/\s+/);
19
+ if (parts.length === 1) return (parts[0] ?? "?").slice(0, 2).toUpperCase();
20
+ return (
21
+ (parts[0]?.[0] ?? "") + (parts[parts.length - 1]?.[0] ?? "")
22
+ ).toUpperCase();
23
+ });
24
+
25
+ const tenure = computed(() => {
26
+ if (!props.person.start_date) return null;
27
+ const start = new Date(props.person.start_date).getTime();
28
+ if (Number.isNaN(start)) return null;
29
+ const years = (Date.now() - start) / (365.25 * 86400000);
30
+ if (years < 1) {
31
+ const months = Math.round(years * 12);
32
+ return `${months} month${months === 1 ? "" : "s"}`;
33
+ }
34
+ return `${years.toFixed(1)} years`;
35
+ });
36
+
37
+ function startEdit() {
38
+ edits.value = { ...props.person };
39
+ editing.value = true;
40
+ }
41
+
42
+ function cancelEdit() {
43
+ editing.value = false;
44
+ edits.value = {};
45
+ }
46
+
47
+ async function save() {
48
+ saving.value = true;
49
+ try {
50
+ const updated = await api.updatePerson(
51
+ String(props.person.id),
52
+ edits.value,
53
+ );
54
+ emit("updated", updated);
55
+ toast.add({ title: t("hr.toast.saved"), color: "success" });
56
+ editing.value = false;
57
+ } catch {
58
+ toast.add({ title: t("hr.toast.error"), color: "error" });
59
+ } finally {
60
+ saving.value = false;
61
+ }
62
+ }
63
+
64
+ const PROFILE_FIELD_NAMES = [
65
+ "first_name",
66
+ "last_name",
67
+ "email",
68
+ "phone",
69
+ "date_of_birth",
70
+ "gender",
71
+ "nationality",
72
+ "id_number",
73
+ "desired_position",
74
+ "source",
75
+ "department_id",
76
+ "employment_type_id",
77
+ "job_title",
78
+ "employee_id",
79
+ "start_date",
80
+ ];
81
+ const { pickFields, fetchAll, isReady } = useHrFieldRegistry(["hr_people"]);
82
+
83
+ const profileFields = computed(() =>
84
+ pickFields("hr_people", PROFILE_FIELD_NAMES),
85
+ );
86
+
87
+ onMounted(fetchAll);
88
+ </script>
89
+
90
+ <template>
91
+ <div class="space-y-6">
92
+ <!-- Header -->
93
+ <div class="flex items-start gap-6 flex-wrap">
94
+ <UAvatar
95
+ :text="initials"
96
+ size="xl"
97
+ class="bg-primary/10 text-primary font-semibold"
98
+ />
99
+ <div class="flex-1 min-w-0">
100
+ <h2 class="text-xl font-semibold text-default">
101
+ {{ person.display_name }}
102
+ </h2>
103
+ <p
104
+ v-if="person.job_title || person.desired_position"
105
+ class="text-sm text-muted mt-0.5"
106
+ >
107
+ {{ person.job_title ?? person.desired_position }}
108
+ </p>
109
+ <div class="flex items-center gap-2 mt-2 flex-wrap">
110
+ <HrPersonStageBadge :stage="person.stage" />
111
+ </div>
112
+ </div>
113
+ <UButton
114
+ v-if="!readonly && !editing"
115
+ size="xs"
116
+ variant="soft"
117
+ icon="i-ph-pencil-simple-light"
118
+ @click="startEdit"
119
+ >
120
+ {{ $t("common.edit") }}
121
+ </UButton>
122
+ </div>
123
+
124
+ <!-- Edit mode: FormRoot -->
125
+ <template v-if="editing">
126
+ <div
127
+ v-if="!isReady('hr_people')"
128
+ class="px-4 py-6 text-sm text-muted"
129
+ >
130
+ {{ $t("common.loading") }}
131
+ </div>
132
+ <FormRoot
133
+ v-else-if="profileFields.length"
134
+ v-model="edits"
135
+ :fields="profileFields"
136
+ :initial-values="person"
137
+ />
138
+ <div class="flex justify-end gap-2">
139
+ <UButton
140
+ color="neutral"
141
+ variant="outline"
142
+ :label="$t('common.cancel')"
143
+ @click="cancelEdit"
144
+ />
145
+ <UButton
146
+ :loading="saving"
147
+ :label="$t('common.save')"
148
+ @click="save"
149
+ />
150
+ </div>
151
+ </template>
152
+
153
+ <!-- Read-only mode: InfoSections -->
154
+ <template v-else>
155
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
156
+ <!-- Contact -->
157
+ <HrInfoSection title="Contact" icon="i-ph-address-book-light">
158
+ <HrInfoRow
159
+ icon="i-ph-envelope-light"
160
+ label="Email"
161
+ :value="person.email"
162
+ />
163
+ <HrInfoRow
164
+ icon="i-ph-phone-light"
165
+ label="Phone"
166
+ :value="person.phone"
167
+ />
168
+ <HrInfoRow
169
+ icon="i-ph-cake-light"
170
+ label="Date of birth"
171
+ :value="person.date_of_birth"
172
+ />
173
+ </HrInfoSection>
174
+
175
+ <!-- Recruitment (talents) / Employment (employees) -->
176
+ <HrInfoSection
177
+ v-if="
178
+ ['talent', 'interviewing', 'offer'].includes(
179
+ person.stage,
180
+ )
181
+ "
182
+ title="Recruitment"
183
+ icon="i-ph-magnet-straight-light"
184
+ >
185
+ <HrInfoRow
186
+ icon="i-ph-briefcase-light"
187
+ label="Desired position"
188
+ :value="person.desired_position"
189
+ />
190
+ <HrInfoRow
191
+ icon="i-ph-flag-light"
192
+ label="Source"
193
+ :value="person.source"
194
+ />
195
+ </HrInfoSection>
196
+
197
+ <HrInfoSection
198
+ v-else
199
+ title="Employment"
200
+ icon="i-ph-identification-badge-light"
201
+ >
202
+ <HrInfoRow
203
+ icon="i-ph-hash-light"
204
+ label="Employee ID"
205
+ :value="person.employee_id"
206
+ />
207
+ <HrInfoRow
208
+ icon="i-ph-briefcase-light"
209
+ label="Job title"
210
+ :value="person.job_title"
211
+ />
212
+ <HrInfoRow
213
+ v-if="person.start_date"
214
+ icon="i-ph-calendar-light"
215
+ label="Start date"
216
+ >
217
+ {{ person.start_date }}
218
+ <span v-if="tenure" class="text-muted text-xs ml-1">
219
+ · {{ tenure }}
220
+ </span>
221
+ </HrInfoRow>
222
+ </HrInfoSection>
223
+ </div>
224
+
225
+ <!-- Meta -->
226
+ <div class="text-xs text-muted pt-4 border-t border-default">
227
+ ID: {{ person.id }}
228
+ </div>
229
+ </template>
230
+ </div>
231
+ </template>
@@ -0,0 +1,113 @@
1
+ <script lang="ts" setup>
2
+ import type { DocumentTriggerEvent } from "../../../../shared/types"
3
+
4
+ const { triggers, setTrigger } = useTriggers()
5
+ const { templates } = useTemplates()
6
+
7
+ const eventMeta: Record<DocumentTriggerEvent, { label: string, icon: string, description: string, color: "primary" | "info" | "warning" | "success" | "neutral" }> = {
8
+ on_offer_sent: { label: "Offer Sent", icon: "i-ph-envelope-simple-light", description: "Fires when an offer is sent to a candidate.", color: "info" },
9
+ on_hire: { label: "Hire", icon: "i-ph-rocket-launch-light", description: "Fires when a candidate is hired (Offer → Probation).", color: "primary" },
10
+ on_probation_end: { label: "Probation End", icon: "i-ph-hourglass-medium-light", description: "Fires when an employee is confirmed (Probation → Active).", color: "success" },
11
+ on_resign: { label: "Resign", icon: "i-ph-sign-out-light", description: "Fires when an employee resigns (Active → Resigned).", color: "warning" },
12
+ on_asset_issued: { label: "Asset Issued", icon: "i-ph-laptop-light", description: "Fires when an asset is issued to an employee.", color: "neutral" },
13
+ on_asset_returned: { label: "Asset Returned", icon: "i-ph-laptop-light", description: "Fires when an issued asset is returned.", color: "neutral" },
14
+ }
15
+
16
+ const activeTemplates = computed(() => templates.value.filter(t => !t.is_archived))
17
+
18
+ function toggleTemplate(event: DocumentTriggerEvent, templateId: string, currentIds: string[], autoGenerate: boolean) {
19
+ const next = currentIds.includes(templateId)
20
+ ? currentIds.filter(id => id !== templateId)
21
+ : [...currentIds, templateId]
22
+ setTrigger(event, next, autoGenerate)
23
+ }
24
+ </script>
25
+
26
+ <template>
27
+ <div class="space-y-5">
28
+ <p class="text-sm text-muted">
29
+ {{ $t('triggers.description') }}
30
+ </p>
31
+
32
+ <ul class="space-y-3">
33
+ <li
34
+ v-for="t in triggers"
35
+ :key="t.event"
36
+ class="rounded-lg border border-default bg-default overflow-hidden"
37
+ >
38
+ <header class="flex items-center justify-between gap-3 px-4 py-3 border-b border-default bg-elevated/30">
39
+ <div class="flex items-center gap-3 min-w-0">
40
+ <div
41
+ class="p-2 rounded-lg shrink-0 bg-default"
42
+ :class="`text-${eventMeta[t.event].color}`"
43
+ >
44
+ <UIcon
45
+ :name="eventMeta[t.event].icon"
46
+ class="size-4"
47
+ />
48
+ </div>
49
+ <div class="min-w-0">
50
+ <div class="text-sm font-medium text-default">
51
+ {{ eventMeta[t.event].label }}
52
+ </div>
53
+ <p class="text-xs text-muted">
54
+ {{ eventMeta[t.event].description }}
55
+ </p>
56
+ </div>
57
+ </div>
58
+ <div class="flex items-center gap-2 shrink-0">
59
+ <UBadge
60
+ v-if="t.template_ids.length === 0"
61
+ :label="$t('triggers.no_templates')"
62
+ color="neutral"
63
+ variant="subtle"
64
+ size="md"
65
+ />
66
+ <UBadge
67
+ v-else
68
+ :label="`${t.template_ids.length} template${t.template_ids.length === 1 ? '' : 's'}`"
69
+ color="primary"
70
+ variant="subtle"
71
+ size="md"
72
+ />
73
+ <USwitch
74
+ :model-value="t.auto_generate"
75
+ :label="$t('triggers.auto')"
76
+ @update:model-value="(v) => setTrigger(t.event, t.template_ids, !!v)"
77
+ />
78
+ </div>
79
+ </header>
80
+
81
+ <div class="p-3">
82
+ <div
83
+ v-if="activeTemplates.length === 0"
84
+ class="text-xs text-muted italic"
85
+ >
86
+ {{ $t('triggers.no_active_templates') }}
87
+ </div>
88
+ <div
89
+ v-else
90
+ class="flex flex-wrap gap-2"
91
+ >
92
+ <button
93
+ v-for="tpl in activeTemplates"
94
+ :key="tpl.id"
95
+ type="button"
96
+ class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs transition-colors border"
97
+ :class="t.template_ids.includes(tpl.id)
98
+ ? 'border-primary bg-primary/10 text-primary font-medium'
99
+ : 'border-default text-muted hover:bg-elevated/50 hover:text-default'"
100
+ @click="toggleTemplate(t.event, tpl.id, t.template_ids, t.auto_generate)"
101
+ >
102
+ <UIcon
103
+ :name="t.template_ids.includes(tpl.id) ? 'i-ph-check-light' : 'i-ph-plus-light'"
104
+ class="size-3"
105
+ />
106
+ {{ tpl.name }}
107
+ </button>
108
+ </div>
109
+ </div>
110
+ </li>
111
+ </ul>
112
+ </div>
113
+ </template>
@@ -0,0 +1,200 @@
1
+ <script lang="ts" setup>
2
+ import type { DocumentTemplate } from "../../../../shared/types"
3
+ import HrTemplateEditDrawer from "../person/document/template-edit-drawer.vue"
4
+
5
+ const { templates, createTemplate } = useTemplates()
6
+
7
+ const drawerOpen = ref(false)
8
+ const editingId = ref<string | null>(null)
9
+
10
+ const CATEGORY_COLOR: Record<DocumentTemplate["category"], "primary" | "success" | "warning" | "info" | "neutral"> = {
11
+ Onboarding: "primary",
12
+ Probation: "info",
13
+ Resignation: "warning",
14
+ Asset: "success",
15
+ Misc: "neutral",
16
+ Other: "neutral",
17
+ }
18
+
19
+ const CATEGORY_ICON: Record<DocumentTemplate["category"], string> = {
20
+ Onboarding: "i-ph-rocket-launch-light",
21
+ Probation: "i-ph-hourglass-medium-light",
22
+ Resignation: "i-ph-sign-out-light",
23
+ Asset: "i-ph-laptop-light",
24
+ Misc: "i-ph-file-text-light",
25
+ Other: "i-ph-file-text-light",
26
+ }
27
+
28
+ function openTemplate(id: string) {
29
+ editingId.value = id
30
+ drawerOpen.value = true
31
+ }
32
+
33
+ async function newTemplate() {
34
+ const created = await createTemplate({
35
+ name: "Untitled Template",
36
+ category: "Misc",
37
+ body_markdown: "# {{full_name}}\n\nWrite your template here.",
38
+ available_placeholders: ["full_name"],
39
+ })
40
+ openTemplate(created.id)
41
+ }
42
+
43
+ function bodySnippet(body: string): string {
44
+ return body
45
+ .replace(/^#+\s+/gm, "")
46
+ .replace(/\{\{[^}]+\}\}/g, "…")
47
+ .replace(/\s+/g, " ")
48
+ .trim()
49
+ .slice(0, 140)
50
+ }
51
+
52
+ const groupedTemplates = computed(() => {
53
+ const groups = new Map<DocumentTemplate["category"], DocumentTemplate[]>()
54
+ for (const t of templates.value) {
55
+ if (t.is_archived) continue
56
+ const list = groups.get(t.category) ?? []
57
+ list.push(t)
58
+ groups.set(t.category, list)
59
+ }
60
+ return Array.from(groups.entries())
61
+ })
62
+
63
+ const archivedTemplates = computed(() => templates.value.filter(t => t.is_archived))
64
+ </script>
65
+
66
+ <template>
67
+ <div class="space-y-5">
68
+ <div class="flex items-start justify-between gap-4">
69
+ <p class="text-sm text-muted">
70
+ {{ $t('templates.description') }}
71
+ </p>
72
+ <UButton
73
+ color="primary"
74
+ size="sm"
75
+ icon="i-ph-plus-light"
76
+ :label="$t('templates.new')"
77
+ @click="newTemplate"
78
+ />
79
+ </div>
80
+
81
+ <div
82
+ v-if="groupedTemplates.length === 0"
83
+ class="rounded-xl border border-dashed border-default p-12 text-center"
84
+ >
85
+ <UIcon
86
+ name="i-ph-file-doc-light"
87
+ class="size-10 mx-auto mb-3 opacity-40"
88
+ />
89
+ <p class="text-sm font-medium text-default mb-1">
90
+ {{ $t('templates.empty') }}
91
+ </p>
92
+ <p class="text-xs text-muted mb-4">
93
+ {{ $t('templates.empty_hint') }}
94
+ </p>
95
+ <UButton
96
+ color="primary"
97
+ size="sm"
98
+ icon="i-ph-plus-light"
99
+ :label="$t('templates.new')"
100
+ @click="newTemplate"
101
+ />
102
+ </div>
103
+
104
+ <section
105
+ v-for="[category, list] in groupedTemplates"
106
+ :key="category"
107
+ class="space-y-2"
108
+ >
109
+ <div class="flex items-center gap-2 px-1">
110
+ <UIcon
111
+ :name="CATEGORY_ICON[category]"
112
+ class="size-4 text-muted"
113
+ />
114
+ <h3 class="text-xs font-semibold uppercase tracking-wider text-muted">
115
+ {{ category }}
116
+ </h3>
117
+ <span class="text-xs text-muted">· {{ list.length }}</span>
118
+ </div>
119
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
120
+ <button
121
+ v-for="t in list"
122
+ :key="t.id"
123
+ type="button"
124
+ class="text-left rounded-lg border border-default bg-default p-4 hover:border-primary/40 hover:bg-elevated/30 transition-colors group"
125
+ @click="openTemplate(t.id)"
126
+ >
127
+ <div class="flex items-start gap-3">
128
+ <div class="p-2 rounded-lg shrink-0 bg-elevated">
129
+ <UIcon
130
+ :name="CATEGORY_ICON[t.category]"
131
+ class="size-4"
132
+ :class="`text-${CATEGORY_COLOR[t.category]}`"
133
+ />
134
+ </div>
135
+ <div class="flex-1 min-w-0">
136
+ <div class="flex items-center gap-2 mb-1">
137
+ <div class="font-medium text-sm text-default truncate group-hover:text-primary transition-colors">
138
+ {{ t.name }}
139
+ </div>
140
+ <UBadge
141
+ :color="CATEGORY_COLOR[t.category]"
142
+ variant="subtle"
143
+ size="md"
144
+ >
145
+ {{ t.category }}
146
+ </UBadge>
147
+ </div>
148
+ <p class="text-xs text-muted line-clamp-2">
149
+ {{ bodySnippet(t.body_markdown) }}
150
+ </p>
151
+ <div class="mt-2 flex items-center gap-1.5 text-xs text-muted">
152
+ <UIcon
153
+ name="i-ph-brackets-curly-light"
154
+ class="size-3"
155
+ />
156
+ {{ t.available_placeholders.length }} placeholder{{ t.available_placeholders.length === 1 ? '' : 's' }}
157
+ </div>
158
+ </div>
159
+ <UIcon
160
+ name="i-ph-pencil-simple-line-light"
161
+ class="size-4 text-muted opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
162
+ />
163
+ </div>
164
+ </button>
165
+ </div>
166
+ </section>
167
+
168
+ <section
169
+ v-if="archivedTemplates.length > 0"
170
+ class="space-y-2 pt-4 border-t border-default"
171
+ >
172
+ <details>
173
+ <summary class="cursor-pointer text-xs text-muted hover:text-default">
174
+ {{ $t('templates.archived', { count: archivedTemplates.length }) }}
175
+ </summary>
176
+ <div class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-3">
177
+ <button
178
+ v-for="t in archivedTemplates"
179
+ :key="t.id"
180
+ type="button"
181
+ class="text-left rounded-lg border border-default bg-elevated/30 p-3 opacity-60 hover:opacity-100 transition-opacity"
182
+ @click="openTemplate(t.id)"
183
+ >
184
+ <div class="font-medium text-sm text-default">
185
+ {{ t.name }}
186
+ </div>
187
+ <div class="text-xs text-muted">
188
+ {{ t.category }}
189
+ </div>
190
+ </button>
191
+ </div>
192
+ </details>
193
+ </section>
194
+
195
+ <HrTemplateEditDrawer
196
+ v-model:open="drawerOpen"
197
+ :template-id="editingId"
198
+ />
199
+ </div>
200
+ </template>
@@ -0,0 +1,87 @@
1
+ <script lang="ts" setup>
2
+ import { LIFECYCLE_STAGES } from "../../../../shared/types/person"
3
+
4
+ const { general, lifecycleLabels, setStageLabel, save, isPending } = useHrSettings()
5
+ </script>
6
+
7
+ <template>
8
+ <div class="space-y-5">
9
+ <div class="rounded-xl border border-default bg-default p-4 space-y-4">
10
+ <div>
11
+ <div class="text-sm font-semibold text-default">
12
+ {{ $t('settings.general.company_defaults_title') }}
13
+ </div>
14
+ <p class="mt-0.5 text-xs text-muted">
15
+ {{ $t('settings.general.company_defaults_desc') }}
16
+ </p>
17
+ </div>
18
+ <div class="space-y-4">
19
+ <UFormField :label="$t('settings.general.company_name')">
20
+ <UInput
21
+ v-model="general.company_name"
22
+ size="sm"
23
+ class="w-full max-w-md"
24
+ />
25
+ </UFormField>
26
+ <UFormField
27
+ :label="$t('settings.general.default_currency')"
28
+ :help="$t('settings.general.default_currency_help')"
29
+ >
30
+ <UInput
31
+ v-model="general.default_currency"
32
+ size="sm"
33
+ class="w-full max-w-xs"
34
+ />
35
+ </UFormField>
36
+ <UFormField
37
+ :label="$t('settings.general.fiscal_year_start_month')"
38
+ :help="$t('settings.general.fiscal_year_start_month_help')"
39
+ >
40
+ <UInputNumber
41
+ v-model="general.fiscal_year_start_month"
42
+ :min="1"
43
+ :max="12"
44
+ size="sm"
45
+ class="w-full max-w-xs"
46
+ />
47
+ </UFormField>
48
+ </div>
49
+ <div class="pt-2">
50
+ <UButton
51
+ color="primary"
52
+ size="sm"
53
+ :loading="isPending"
54
+ @click="save(general)"
55
+ >
56
+ {{ $t('common.save') }}
57
+ </UButton>
58
+ </div>
59
+ </div>
60
+
61
+ <div class="rounded-xl border border-default bg-default p-4 space-y-3">
62
+ <div>
63
+ <div class="text-sm font-semibold text-default">
64
+ {{ $t('settings.general.stage_labels') }}
65
+ </div>
66
+ <p class="mt-0.5 text-xs text-muted">
67
+ {{ $t('settings.general.stage_labels_desc') }}
68
+ </p>
69
+ </div>
70
+ <ul class="space-y-2">
71
+ <li
72
+ v-for="stage in LIFECYCLE_STAGES"
73
+ :key="stage"
74
+ class="flex items-center gap-3"
75
+ >
76
+ <code class="text-xs text-muted w-28 shrink-0">{{ stage }}</code>
77
+ <UInput
78
+ :model-value="lifecycleLabels[stage]"
79
+ size="sm"
80
+ class="flex-1 max-w-md"
81
+ @update:model-value="(v) => setStageLabel(stage, String(v))"
82
+ />
83
+ </li>
84
+ </ul>
85
+ </div>
86
+ </div>
87
+ </template>