@happyvertical/smrt-human-resources 0.0.0-stage → 0.53.5

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 (64) hide show
  1. package/AGENTS.md +297 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +291 -2
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/dates.d.ts +25 -0
  8. package/dist/dates.d.ts.map +1 -0
  9. package/dist/employment/index.d.ts +3 -0
  10. package/dist/employment/index.d.ts.map +1 -0
  11. package/dist/employment/models.d.ts +73 -0
  12. package/dist/employment/models.d.ts.map +1 -0
  13. package/dist/employment/queries.d.ts +12 -0
  14. package/dist/employment/queries.d.ts.map +1 -0
  15. package/dist/employment/service.d.ts +277 -0
  16. package/dist/employment/service.d.ts.map +1 -0
  17. package/dist/index.d.ts +18 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +2068 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/manifest.json +2424 -0
  22. package/dist/qualifications/index.d.ts +3 -0
  23. package/dist/qualifications/index.d.ts.map +1 -0
  24. package/dist/qualifications/internal.d.ts +24 -0
  25. package/dist/qualifications/internal.d.ts.map +1 -0
  26. package/dist/qualifications/models.d.ts +77 -0
  27. package/dist/qualifications/models.d.ts.map +1 -0
  28. package/dist/qualifications/service.d.ts +381 -0
  29. package/dist/qualifications/service.d.ts.map +1 -0
  30. package/dist/service-base.d.ts +47 -0
  31. package/dist/service-base.d.ts.map +1 -0
  32. package/dist/smrt-knowledge.json +902 -0
  33. package/dist/svelte/components/EmployeeForm.svelte +369 -0
  34. package/dist/svelte/components/EmployeeForm.svelte.d.ts +28 -0
  35. package/dist/svelte/components/EmployeeForm.svelte.d.ts.map +1 -0
  36. package/dist/svelte/components/EmployeeList.svelte +158 -0
  37. package/dist/svelte/components/EmployeeList.svelte.d.ts +13 -0
  38. package/dist/svelte/components/EmployeeList.svelte.d.ts.map +1 -0
  39. package/dist/svelte/components/ExpiringQualificationsList.svelte +126 -0
  40. package/dist/svelte/components/ExpiringQualificationsList.svelte.d.ts +13 -0
  41. package/dist/svelte/components/ExpiringQualificationsList.svelte.d.ts.map +1 -0
  42. package/dist/svelte/components/PersonQualifications.svelte +156 -0
  43. package/dist/svelte/components/PersonQualifications.svelte.d.ts +15 -0
  44. package/dist/svelte/components/PersonQualifications.svelte.d.ts.map +1 -0
  45. package/dist/svelte/i18n.d.ts +67 -0
  46. package/dist/svelte/i18n.d.ts.map +1 -0
  47. package/dist/svelte/i18n.js +67 -0
  48. package/dist/svelte/index.d.ts +23 -0
  49. package/dist/svelte/index.d.ts.map +1 -0
  50. package/dist/svelte/index.js +25 -0
  51. package/dist/svelte/types.d.ts +225 -0
  52. package/dist/svelte/types.d.ts.map +1 -0
  53. package/dist/svelte/types.js +257 -0
  54. package/dist/types.d.ts +119 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/types.js +23 -0
  57. package/dist/types.js.map +1 -0
  58. package/dist/ui.d.ts +17 -0
  59. package/dist/ui.d.ts.map +1 -0
  60. package/dist/ui.js +65 -0
  61. package/dist/ui.js.map +1 -0
  62. package/dist/write.d.ts +22 -0
  63. package/dist/write.d.ts.map +1 -0
  64. 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"}