@happyvertical/smrt-human-resources 0.0.0-stage → 0.53.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +297 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +291 -2
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/dates.d.ts +25 -0
- package/dist/dates.d.ts.map +1 -0
- package/dist/employment/index.d.ts +3 -0
- package/dist/employment/index.d.ts.map +1 -0
- package/dist/employment/models.d.ts +73 -0
- package/dist/employment/models.d.ts.map +1 -0
- package/dist/employment/queries.d.ts +12 -0
- package/dist/employment/queries.d.ts.map +1 -0
- package/dist/employment/service.d.ts +277 -0
- package/dist/employment/service.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2068 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.json +2424 -0
- package/dist/qualifications/index.d.ts +3 -0
- package/dist/qualifications/index.d.ts.map +1 -0
- package/dist/qualifications/internal.d.ts +24 -0
- package/dist/qualifications/internal.d.ts.map +1 -0
- package/dist/qualifications/models.d.ts +77 -0
- package/dist/qualifications/models.d.ts.map +1 -0
- package/dist/qualifications/service.d.ts +381 -0
- package/dist/qualifications/service.d.ts.map +1 -0
- package/dist/service-base.d.ts +47 -0
- package/dist/service-base.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +902 -0
- package/dist/svelte/components/EmployeeForm.svelte +369 -0
- package/dist/svelte/components/EmployeeForm.svelte.d.ts +28 -0
- package/dist/svelte/components/EmployeeForm.svelte.d.ts.map +1 -0
- package/dist/svelte/components/EmployeeList.svelte +158 -0
- package/dist/svelte/components/EmployeeList.svelte.d.ts +13 -0
- package/dist/svelte/components/EmployeeList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ExpiringQualificationsList.svelte +126 -0
- package/dist/svelte/components/ExpiringQualificationsList.svelte.d.ts +13 -0
- package/dist/svelte/components/ExpiringQualificationsList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PersonQualifications.svelte +156 -0
- package/dist/svelte/components/PersonQualifications.svelte.d.ts +15 -0
- package/dist/svelte/components/PersonQualifications.svelte.d.ts.map +1 -0
- package/dist/svelte/i18n.d.ts +67 -0
- package/dist/svelte/i18n.d.ts.map +1 -0
- package/dist/svelte/i18n.js +67 -0
- package/dist/svelte/index.d.ts +23 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +25 -0
- package/dist/svelte/types.d.ts +225 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +257 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +23 -0
- package/dist/types.js.map +1 -0
- package/dist/ui.d.ts +17 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +65 -0
- package/dist/ui.js.map +1 -0
- package/dist/write.d.ts +22 -0
- package/dist/write.d.ts.map +1 -0
- package/package.json +86 -3
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* EmployeeForm — the fields a host edits when hiring or editing an employee:
|
|
4
|
+
* employee number, worker type, position, start date (new hire only),
|
|
5
|
+
* effective date (edit only) and an optional login link. Presentational: it
|
|
6
|
+
* never persists. The host receives trimmed, checked values in `onsubmit` and
|
|
7
|
+
* calls `EmploymentService`: `hire` for a new hire, or, for an edit,
|
|
8
|
+
* `changePosition` / `changeWorkerType` / `linkLogin` / `unlinkLogin` for
|
|
9
|
+
* each value that changed, dated with the returned `effectiveOn`. A login
|
|
10
|
+
* link or unlink takes effect at once whatever that date is.
|
|
11
|
+
*
|
|
12
|
+
* The person is chosen by the host before the form opens: HR holds no
|
|
13
|
+
* identity data, so there is no name field here.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Button } from '@happyvertical/smrt-ui';
|
|
17
|
+
import { Form, Input, Select } from '@happyvertical/smrt-ui/forms';
|
|
18
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
19
|
+
import { type IsoDate, SUGGESTED_WORKER_TYPES } from '../../types.js';
|
|
20
|
+
import { M } from '../i18n.js';
|
|
21
|
+
import {
|
|
22
|
+
type EmployeeFormDraft,
|
|
23
|
+
type EmployeeFormField,
|
|
24
|
+
type EmployeeFormInitial,
|
|
25
|
+
type EmployeeFormValues,
|
|
26
|
+
type EmployeeLoginOption,
|
|
27
|
+
validateEmployeeForm,
|
|
28
|
+
} from '../types.js';
|
|
29
|
+
|
|
30
|
+
const { t } = useI18n();
|
|
31
|
+
|
|
32
|
+
export interface EmployeeFormProps {
|
|
33
|
+
/** The employment being edited; omit or pass null to hire. */
|
|
34
|
+
employee?: EmployeeFormInitial | null;
|
|
35
|
+
/** Invoked with trimmed, checked values. The form never persists. */
|
|
36
|
+
onsubmit: (values: EmployeeFormValues) => void;
|
|
37
|
+
/** Invoked when the form is canceled; omit to hide the cancel button. */
|
|
38
|
+
oncancel?: () => void;
|
|
39
|
+
/** Blocks input and submission while the host saves. */
|
|
40
|
+
loading?: boolean;
|
|
41
|
+
/** Worker types offered as suggestions; other values are still accepted. */
|
|
42
|
+
workerTypes?: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* Logins the host offers. With options the login field is a choice;
|
|
45
|
+
* without, it takes a `smrt-users:User` id.
|
|
46
|
+
*/
|
|
47
|
+
logins?: readonly EmployeeLoginOption[];
|
|
48
|
+
/**
|
|
49
|
+
* The host's local date, prefilled as a new hire's start date and as an
|
|
50
|
+
* edit's effective date.
|
|
51
|
+
*/
|
|
52
|
+
today?: IsoDate;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const {
|
|
56
|
+
employee = null,
|
|
57
|
+
onsubmit,
|
|
58
|
+
oncancel,
|
|
59
|
+
loading = false,
|
|
60
|
+
workerTypes = SUGGESTED_WORKER_TYPES,
|
|
61
|
+
logins,
|
|
62
|
+
today,
|
|
63
|
+
}: EmployeeFormProps = $props();
|
|
64
|
+
|
|
65
|
+
const uid = $props.id();
|
|
66
|
+
const isNew = $derived(employee === null);
|
|
67
|
+
|
|
68
|
+
function draftFor(from: EmployeeFormInitial | null): EmployeeFormDraft {
|
|
69
|
+
return {
|
|
70
|
+
employeeNumber: from?.employeeNumber ?? '',
|
|
71
|
+
workerType: from?.workerType ?? 'employee',
|
|
72
|
+
position: from?.position ?? '',
|
|
73
|
+
userId: from?.userId ?? '',
|
|
74
|
+
startedOn: from ? '' : (today ?? ''),
|
|
75
|
+
effectiveOn: from ? (today ?? '') : '',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getInitialFormState() {
|
|
80
|
+
return { employee, draft: draftFor(employee) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const initialFormState = getInitialFormState();
|
|
84
|
+
let employeeNumber = $state(initialFormState.draft.employeeNumber);
|
|
85
|
+
let workerType = $state(initialFormState.draft.workerType);
|
|
86
|
+
let position = $state(initialFormState.draft.position);
|
|
87
|
+
let userId = $state(initialFormState.draft.userId);
|
|
88
|
+
let startedOn = $state(initialFormState.draft.startedOn);
|
|
89
|
+
let effectiveOn = $state(initialFormState.draft.effectiveOn);
|
|
90
|
+
let invalid = $state<EmployeeFormField[]>([]);
|
|
91
|
+
let appliedEmployee: EmployeeFormInitial | null = initialFormState.employee;
|
|
92
|
+
|
|
93
|
+
// Follow the host when it swaps the employment being edited.
|
|
94
|
+
$effect(() => {
|
|
95
|
+
if (appliedEmployee === employee) return;
|
|
96
|
+
appliedEmployee = employee;
|
|
97
|
+
const draft = draftFor(employee);
|
|
98
|
+
employeeNumber = draft.employeeNumber;
|
|
99
|
+
workerType = draft.workerType;
|
|
100
|
+
position = draft.position;
|
|
101
|
+
userId = draft.userId;
|
|
102
|
+
startedOn = draft.startedOn;
|
|
103
|
+
effectiveOn = draft.effectiveOn;
|
|
104
|
+
invalid = [];
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
function errorMessage(field: EmployeeFormField): string {
|
|
108
|
+
switch (field) {
|
|
109
|
+
case 'employeeNumber':
|
|
110
|
+
return t(M['human_resources.employee_form.error_employee_number']);
|
|
111
|
+
case 'workerType':
|
|
112
|
+
return t(M['human_resources.employee_form.error_worker_type']);
|
|
113
|
+
case 'effectiveOn':
|
|
114
|
+
return t(M['human_resources.employee_form.error_effective_on']);
|
|
115
|
+
default:
|
|
116
|
+
return t(M['human_resources.employee_form.error_started_on']);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const errors = $derived(
|
|
121
|
+
invalid.map((field) => ({ field, message: errorMessage(field) })),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
/** The id of a field's validation message, as rendered in the error list. */
|
|
125
|
+
function errorId(field: EmployeeFormField): string {
|
|
126
|
+
return `${uid}-error-${field}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* `aria-describedby` for a field: its hint, plus its validation message
|
|
131
|
+
* while it is invalid.
|
|
132
|
+
*/
|
|
133
|
+
function describedBy(
|
|
134
|
+
field: EmployeeFormField,
|
|
135
|
+
hintId?: string,
|
|
136
|
+
): string | undefined {
|
|
137
|
+
const ids = [hintId, invalid.includes(field) ? errorId(field) : undefined];
|
|
138
|
+
return ids.filter(Boolean).join(' ') || undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The employee's current login stays selectable even when the host's options
|
|
142
|
+
// leave it out, so submitting an unrelated edit never unlinks it.
|
|
143
|
+
const loginOptions = $derived.by((): EmployeeLoginOption[] | undefined => {
|
|
144
|
+
if (!logins) return undefined;
|
|
145
|
+
const current = employee?.userId;
|
|
146
|
+
if (!current || logins.some((login) => login.value === current))
|
|
147
|
+
return [...logins];
|
|
148
|
+
return [
|
|
149
|
+
{
|
|
150
|
+
value: current,
|
|
151
|
+
label: t(M['human_resources.employee_form.login_current'], {
|
|
152
|
+
id: current,
|
|
153
|
+
}),
|
|
154
|
+
},
|
|
155
|
+
...logins,
|
|
156
|
+
];
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
function handleSubmit() {
|
|
160
|
+
const result = validateEmployeeForm(
|
|
161
|
+
{ employeeNumber, workerType, position, userId, startedOn, effectiveOn },
|
|
162
|
+
isNew,
|
|
163
|
+
);
|
|
164
|
+
if (!result.ok) {
|
|
165
|
+
invalid = result.invalid;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
invalid = [];
|
|
169
|
+
onsubmit(result.values);
|
|
170
|
+
}
|
|
171
|
+
</script>
|
|
172
|
+
|
|
173
|
+
<div class="employee-form-shell">
|
|
174
|
+
<Form class="employee-form" onsubmit={handleSubmit}>
|
|
175
|
+
<div class="field">
|
|
176
|
+
<label for="{uid}-employee-number">
|
|
177
|
+
{t(M['human_resources.employee_form.employee_number'])}<span class="required" aria-hidden="true">*</span>
|
|
178
|
+
</label>
|
|
179
|
+
<Input
|
|
180
|
+
id="{uid}-employee-number"
|
|
181
|
+
name="employeeNumber"
|
|
182
|
+
type="text"
|
|
183
|
+
bind:value={employeeNumber}
|
|
184
|
+
required
|
|
185
|
+
readonly={!isNew}
|
|
186
|
+
disabled={loading}
|
|
187
|
+
aria-invalid={invalid.includes('employeeNumber') ? 'true' : undefined}
|
|
188
|
+
aria-describedby={describedBy(
|
|
189
|
+
'employeeNumber',
|
|
190
|
+
isNew ? undefined : `${uid}-employee-number-hint`,
|
|
191
|
+
)}
|
|
192
|
+
/>
|
|
193
|
+
{#if !isNew}
|
|
194
|
+
<span class="hint" id="{uid}-employee-number-hint">
|
|
195
|
+
{t(M['human_resources.employee_form.employee_number_edit_help'])}
|
|
196
|
+
</span>
|
|
197
|
+
{/if}
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div class="field">
|
|
201
|
+
<label for="{uid}-worker-type">
|
|
202
|
+
{t(M['human_resources.employee_form.worker_type'])}<span class="required" aria-hidden="true">*</span>
|
|
203
|
+
</label>
|
|
204
|
+
<Input
|
|
205
|
+
id="{uid}-worker-type"
|
|
206
|
+
name="workerType"
|
|
207
|
+
type="text"
|
|
208
|
+
list="{uid}-worker-types"
|
|
209
|
+
autocomplete="off"
|
|
210
|
+
bind:value={workerType}
|
|
211
|
+
required
|
|
212
|
+
disabled={loading}
|
|
213
|
+
aria-invalid={invalid.includes('workerType') ? 'true' : undefined}
|
|
214
|
+
aria-describedby={describedBy('workerType', `${uid}-worker-type-hint`)}
|
|
215
|
+
/>
|
|
216
|
+
<datalist id="{uid}-worker-types">
|
|
217
|
+
{#each workerTypes as suggestion (suggestion)}
|
|
218
|
+
<option value={suggestion}></option>
|
|
219
|
+
{/each}
|
|
220
|
+
</datalist>
|
|
221
|
+
<span class="hint" id="{uid}-worker-type-hint">
|
|
222
|
+
{t(M['human_resources.employee_form.worker_type_help'])}
|
|
223
|
+
</span>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div class="field">
|
|
227
|
+
<label for="{uid}-position">{t(M['human_resources.employee_form.position'])}</label>
|
|
228
|
+
<Input id="{uid}-position" name="position" type="text" bind:value={position} disabled={loading} />
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
{#if isNew}
|
|
232
|
+
<div class="field">
|
|
233
|
+
<label for="{uid}-started-on">
|
|
234
|
+
{t(M['human_resources.employee_form.started_on'])}<span class="required" aria-hidden="true">*</span>
|
|
235
|
+
</label>
|
|
236
|
+
<Input
|
|
237
|
+
id="{uid}-started-on"
|
|
238
|
+
name="startedOn"
|
|
239
|
+
type="date"
|
|
240
|
+
bind:value={startedOn}
|
|
241
|
+
required
|
|
242
|
+
disabled={loading}
|
|
243
|
+
aria-invalid={invalid.includes('startedOn') ? 'true' : undefined}
|
|
244
|
+
aria-describedby={describedBy('startedOn')}
|
|
245
|
+
/>
|
|
246
|
+
</div>
|
|
247
|
+
{:else}
|
|
248
|
+
<div class="field">
|
|
249
|
+
<label for="{uid}-effective-on">
|
|
250
|
+
{t(M['human_resources.employee_form.effective_on'])}<span class="required" aria-hidden="true">*</span>
|
|
251
|
+
</label>
|
|
252
|
+
<Input
|
|
253
|
+
id="{uid}-effective-on"
|
|
254
|
+
name="effectiveOn"
|
|
255
|
+
type="date"
|
|
256
|
+
bind:value={effectiveOn}
|
|
257
|
+
required
|
|
258
|
+
disabled={loading}
|
|
259
|
+
aria-invalid={invalid.includes('effectiveOn') ? 'true' : undefined}
|
|
260
|
+
aria-describedby={describedBy('effectiveOn', `${uid}-effective-on-hint`)}
|
|
261
|
+
/>
|
|
262
|
+
<span class="hint" id="{uid}-effective-on-hint">
|
|
263
|
+
{t(M['human_resources.employee_form.effective_on_help'])}
|
|
264
|
+
</span>
|
|
265
|
+
</div>
|
|
266
|
+
{/if}
|
|
267
|
+
|
|
268
|
+
<div class="field">
|
|
269
|
+
<label for="{uid}-login">{t(M['human_resources.employee_form.login'])}</label>
|
|
270
|
+
{#if loginOptions}
|
|
271
|
+
<Select
|
|
272
|
+
id="{uid}-login"
|
|
273
|
+
name="userId"
|
|
274
|
+
bind:value={userId}
|
|
275
|
+
disabled={loading}
|
|
276
|
+
aria-describedby="{uid}-login-hint"
|
|
277
|
+
>
|
|
278
|
+
<option value="">{t(M['human_resources.employee_form.login_none'])}</option>
|
|
279
|
+
{#each loginOptions as login (login.value)}
|
|
280
|
+
<option value={login.value}>{login.label}</option>
|
|
281
|
+
{/each}
|
|
282
|
+
</Select>
|
|
283
|
+
{:else}
|
|
284
|
+
<Input
|
|
285
|
+
id="{uid}-login"
|
|
286
|
+
name="userId"
|
|
287
|
+
type="text"
|
|
288
|
+
bind:value={userId}
|
|
289
|
+
disabled={loading}
|
|
290
|
+
aria-describedby="{uid}-login-hint"
|
|
291
|
+
/>
|
|
292
|
+
{/if}
|
|
293
|
+
<span class="hint" id="{uid}-login-hint">
|
|
294
|
+
{t(M['human_resources.employee_form.login_help'])}
|
|
295
|
+
</span>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
{#if errors.length > 0}
|
|
299
|
+
<div class="errors" role="alert">
|
|
300
|
+
<ul>
|
|
301
|
+
{#each errors as error (error.field)}
|
|
302
|
+
<li id={errorId(error.field)}>{error.message}</li>
|
|
303
|
+
{/each}
|
|
304
|
+
</ul>
|
|
305
|
+
</div>
|
|
306
|
+
{/if}
|
|
307
|
+
|
|
308
|
+
<div class="actions">
|
|
309
|
+
{#if oncancel}
|
|
310
|
+
<Button variant="secondary" type="button" onclick={oncancel} disabled={loading}>
|
|
311
|
+
{t(M['human_resources.employee_form.cancel'])}
|
|
312
|
+
</Button>
|
|
313
|
+
{/if}
|
|
314
|
+
<Button variant="primary" type="submit" disabled={loading}>
|
|
315
|
+
{#if loading}
|
|
316
|
+
{t(M['human_resources.employee_form.saving'])}
|
|
317
|
+
{:else if isNew}
|
|
318
|
+
{t(M['human_resources.employee_form.submit_hire'])}
|
|
319
|
+
{:else}
|
|
320
|
+
{t(M['human_resources.employee_form.submit_edit'])}
|
|
321
|
+
{/if}
|
|
322
|
+
</Button>
|
|
323
|
+
</div>
|
|
324
|
+
</Form>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<style>
|
|
328
|
+
.employee-form-shell :global(.employee-form) {
|
|
329
|
+
display: flex;
|
|
330
|
+
flex-direction: column;
|
|
331
|
+
gap: var(--smrt-spacing-md, 1rem);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.field {
|
|
335
|
+
display: flex;
|
|
336
|
+
flex-direction: column;
|
|
337
|
+
gap: 0.25rem;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
label {
|
|
341
|
+
font: var(--smrt-typography-body-medium-font, 500 0.875rem / 1.25 sans-serif);
|
|
342
|
+
color: var(--smrt-color-on-surface-variant, inherit);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.required {
|
|
346
|
+
color: var(--smrt-color-error, inherit);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.hint {
|
|
350
|
+
font: var(--smrt-typography-body-small-font, 0.75rem / 1.25 sans-serif);
|
|
351
|
+
color: var(--smrt-color-on-surface-variant, inherit);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.errors {
|
|
355
|
+
color: var(--smrt-color-error, inherit);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.errors ul {
|
|
359
|
+
margin: 0;
|
|
360
|
+
padding-left: 1.25rem;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.actions {
|
|
364
|
+
display: flex;
|
|
365
|
+
justify-content: flex-end;
|
|
366
|
+
gap: var(--smrt-spacing-sm, 0.5rem);
|
|
367
|
+
margin-top: var(--smrt-spacing-sm, 0.5rem);
|
|
368
|
+
}
|
|
369
|
+
</style>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type IsoDate } from '../../types.js';
|
|
2
|
+
import { type EmployeeFormInitial, type EmployeeFormValues, type EmployeeLoginOption } from '../types.js';
|
|
3
|
+
export interface EmployeeFormProps {
|
|
4
|
+
/** The employment being edited; omit or pass null to hire. */
|
|
5
|
+
employee?: EmployeeFormInitial | null;
|
|
6
|
+
/** Invoked with trimmed, checked values. The form never persists. */
|
|
7
|
+
onsubmit: (values: EmployeeFormValues) => void;
|
|
8
|
+
/** Invoked when the form is canceled; omit to hide the cancel button. */
|
|
9
|
+
oncancel?: () => void;
|
|
10
|
+
/** Blocks input and submission while the host saves. */
|
|
11
|
+
loading?: boolean;
|
|
12
|
+
/** Worker types offered as suggestions; other values are still accepted. */
|
|
13
|
+
workerTypes?: readonly string[];
|
|
14
|
+
/**
|
|
15
|
+
* Logins the host offers. With options the login field is a choice;
|
|
16
|
+
* without, it takes a `smrt-users:User` id.
|
|
17
|
+
*/
|
|
18
|
+
logins?: readonly EmployeeLoginOption[];
|
|
19
|
+
/**
|
|
20
|
+
* The host's local date, prefilled as a new hire's start date and as an
|
|
21
|
+
* edit's effective date.
|
|
22
|
+
*/
|
|
23
|
+
today?: IsoDate;
|
|
24
|
+
}
|
|
25
|
+
declare const EmployeeForm: import("svelte").Component<EmployeeFormProps, {}, "">;
|
|
26
|
+
type EmployeeForm = ReturnType<typeof EmployeeForm>;
|
|
27
|
+
export default EmployeeForm;
|
|
28
|
+
//# sourceMappingURL=EmployeeForm.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EmployeeForm.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/EmployeeForm.svelte.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAE,KAAK,OAAO,EAA0B,MAAM,gBAAgB,CAAC;AAEtE,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EAEzB,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,iBAAiB;IAChC,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACtC,qEAAqE;IACrE,QAAQ,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC/C,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,wDAAwD;IACxD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACxC;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AA8OD,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* EmployeeList — a table of employees: name, employee number, position,
|
|
4
|
+
* worker type, status (with the last day employed once an end is recorded)
|
|
5
|
+
* and start date. Presentational: the host loads
|
|
6
|
+
* employments through `EmploymentService`, adds each person's name (HR holds
|
|
7
|
+
* no identity data) and adapts rows with `toEmployeeView`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Button, StatusBadge } from '@happyvertical/smrt-ui';
|
|
11
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
12
|
+
import { M } from '../i18n.js';
|
|
13
|
+
import {
|
|
14
|
+
type EmployeeView,
|
|
15
|
+
employmentStatusBadgeKey,
|
|
16
|
+
employmentStatusLabelKey,
|
|
17
|
+
} from '../types.js';
|
|
18
|
+
|
|
19
|
+
const { t } = useI18n();
|
|
20
|
+
|
|
21
|
+
export interface EmployeeListProps {
|
|
22
|
+
/** Employees to show, in the host's order. */
|
|
23
|
+
employees: EmployeeView[];
|
|
24
|
+
/** Invoked with the employment id when a name is activated. */
|
|
25
|
+
onselect?: (id: string) => void;
|
|
26
|
+
/** Message shown when there are no employees. */
|
|
27
|
+
emptyMessage?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { employees, onselect, emptyMessage }: EmployeeListProps = $props();
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
{#if employees.length === 0}
|
|
34
|
+
<p class="employee-list-empty">
|
|
35
|
+
{emptyMessage ?? t(M['human_resources.employee_list.empty'])}
|
|
36
|
+
</p>
|
|
37
|
+
{:else}
|
|
38
|
+
<div class="employee-list-scroll">
|
|
39
|
+
<table class="employee-list">
|
|
40
|
+
<caption class="employee-list-caption">{t(M['human_resources.employee_list.caption'])}</caption>
|
|
41
|
+
<thead>
|
|
42
|
+
<tr>
|
|
43
|
+
<th scope="col">{t(M['human_resources.employee_list.name'])}</th>
|
|
44
|
+
<th scope="col">{t(M['human_resources.employee_list.employee_number'])}</th>
|
|
45
|
+
<th scope="col">{t(M['human_resources.employee_list.position'])}</th>
|
|
46
|
+
<th scope="col">{t(M['human_resources.employee_list.worker_type'])}</th>
|
|
47
|
+
<th scope="col">{t(M['human_resources.employee_list.status'])}</th>
|
|
48
|
+
<th scope="col">{t(M['human_resources.employee_list.started_on'])}</th>
|
|
49
|
+
</tr>
|
|
50
|
+
</thead>
|
|
51
|
+
<tbody>
|
|
52
|
+
{#each employees as employee (employee.id)}
|
|
53
|
+
<tr>
|
|
54
|
+
<th scope="row" class="employee-list-name">
|
|
55
|
+
{#if onselect}
|
|
56
|
+
<Button
|
|
57
|
+
variant="ghost"
|
|
58
|
+
size="sm"
|
|
59
|
+
onclick={() => onselect(employee.id)}
|
|
60
|
+
aria-label={t(M['human_resources.employee_list.select_aria'], { name: employee.displayName })}
|
|
61
|
+
>
|
|
62
|
+
{employee.displayName}
|
|
63
|
+
</Button>
|
|
64
|
+
{:else}
|
|
65
|
+
{employee.displayName}
|
|
66
|
+
{/if}
|
|
67
|
+
</th>
|
|
68
|
+
<td class="employee-list-number">{employee.employeeNumber}</td>
|
|
69
|
+
<td>
|
|
70
|
+
{#if employee.position}
|
|
71
|
+
{employee.position}
|
|
72
|
+
{:else}
|
|
73
|
+
<span class="employee-list-none">{t(M['human_resources.employee_list.none'])}</span>
|
|
74
|
+
{/if}
|
|
75
|
+
</td>
|
|
76
|
+
<td>{employee.workerType}</td>
|
|
77
|
+
<td>
|
|
78
|
+
<StatusBadge
|
|
79
|
+
status={employmentStatusBadgeKey(employee.status)}
|
|
80
|
+
label={t(employmentStatusLabelKey(employee.status))}
|
|
81
|
+
size="sm"
|
|
82
|
+
/>
|
|
83
|
+
{#if employee.endsOn}
|
|
84
|
+
<span class="employee-list-last-day">
|
|
85
|
+
{t(M['human_resources.employee_list.last_day'], { date: employee.endsOn })}
|
|
86
|
+
</span>
|
|
87
|
+
{/if}
|
|
88
|
+
</td>
|
|
89
|
+
<td>
|
|
90
|
+
{#if employee.startedOn}
|
|
91
|
+
<time datetime={employee.startedOn}>{employee.startedOn}</time>
|
|
92
|
+
{:else}
|
|
93
|
+
<span class="employee-list-none">{t(M['human_resources.employee_list.none'])}</span>
|
|
94
|
+
{/if}
|
|
95
|
+
</td>
|
|
96
|
+
</tr>
|
|
97
|
+
{/each}
|
|
98
|
+
</tbody>
|
|
99
|
+
</table>
|
|
100
|
+
</div>
|
|
101
|
+
{/if}
|
|
102
|
+
|
|
103
|
+
<style>
|
|
104
|
+
.employee-list-scroll {
|
|
105
|
+
overflow-x: auto;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.employee-list {
|
|
109
|
+
width: 100%;
|
|
110
|
+
border-collapse: collapse;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.employee-list-caption {
|
|
114
|
+
position: absolute;
|
|
115
|
+
width: 1px;
|
|
116
|
+
height: 1px;
|
|
117
|
+
overflow: hidden;
|
|
118
|
+
clip-path: inset(50%);
|
|
119
|
+
white-space: nowrap;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.employee-list th,
|
|
123
|
+
.employee-list td {
|
|
124
|
+
padding: 0.5rem 0.75rem;
|
|
125
|
+
text-align: left;
|
|
126
|
+
border-bottom: 1px solid var(--smrt-color-outline-variant, transparent);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.employee-list thead th {
|
|
130
|
+
font-size: 0.75rem;
|
|
131
|
+
font-weight: 500;
|
|
132
|
+
color: var(--smrt-color-on-surface-variant, inherit);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.employee-list-name {
|
|
136
|
+
font-weight: 500;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.employee-list-number {
|
|
140
|
+
font-variant-numeric: tabular-nums;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.employee-list-last-day {
|
|
144
|
+
margin-left: 0.5rem;
|
|
145
|
+
font-size: 0.75rem;
|
|
146
|
+
white-space: nowrap;
|
|
147
|
+
color: var(--smrt-color-on-surface-variant, inherit);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.employee-list-none,
|
|
151
|
+
.employee-list-empty {
|
|
152
|
+
color: var(--smrt-color-on-surface-variant, inherit);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.employee-list-empty {
|
|
156
|
+
padding: 1rem 0;
|
|
157
|
+
}
|
|
158
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type EmployeeView } from '../types.js';
|
|
2
|
+
export interface EmployeeListProps {
|
|
3
|
+
/** Employees to show, in the host's order. */
|
|
4
|
+
employees: EmployeeView[];
|
|
5
|
+
/** Invoked with the employment id when a name is activated. */
|
|
6
|
+
onselect?: (id: string) => void;
|
|
7
|
+
/** Message shown when there are no employees. */
|
|
8
|
+
emptyMessage?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const EmployeeList: import("svelte").Component<EmployeeListProps, {}, "">;
|
|
11
|
+
type EmployeeList = ReturnType<typeof EmployeeList>;
|
|
12
|
+
export default EmployeeList;
|
|
13
|
+
//# sourceMappingURL=EmployeeList.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EmployeeList.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/EmployeeList.svelte.ts"],"names":[],"mappings":"AAaA,OAAO,EACL,KAAK,YAAY,EAGlB,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,iBAAiB;IAChC,8CAA8C;IAC9C,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAgFD,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|