@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.
- 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,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializable view types and pure helpers for the HR components.
|
|
3
|
+
*
|
|
4
|
+
* Components take plain view objects (not model instances) so hosts can pass
|
|
5
|
+
* data across the server/client boundary. HR holds no identity data, so every
|
|
6
|
+
* name on a view comes from the host. Dates are `YYYY-MM-DD` calendar dates:
|
|
7
|
+
* all arithmetic here is UTC, never local time. Explicit interfaces (not
|
|
8
|
+
* inline intersections) keep Svelte 5 prop type evaluation cheap.
|
|
9
|
+
*/
|
|
10
|
+
import type { Employment } from '../employment/models.js';
|
|
11
|
+
import type { ProfileQualification } from '../qualifications/service.js';
|
|
12
|
+
import type { EmploymentStatus, HeldQualificationStatus, IsoDate, QualificationKind } from '../types.js';
|
|
13
|
+
/** Status of a held qualification on one date (mirrors the service's type). */
|
|
14
|
+
export type QualificationStatusOn = HeldQualificationStatus | 'not-yet-issued';
|
|
15
|
+
/** One row of `EmployeeList`. */
|
|
16
|
+
export interface EmployeeView {
|
|
17
|
+
/** The `Employment` id. */
|
|
18
|
+
id: string;
|
|
19
|
+
/** The person's name, supplied by the host (for example from smrt-profiles). */
|
|
20
|
+
displayName: string;
|
|
21
|
+
employeeNumber: string;
|
|
22
|
+
position: string | null;
|
|
23
|
+
workerType: string;
|
|
24
|
+
status: EmploymentStatus;
|
|
25
|
+
/** First day of the current (or latest) term; null when the host has none. */
|
|
26
|
+
startedOn: IsoDate | null;
|
|
27
|
+
/**
|
|
28
|
+
* The last day employed when an end has been recorded (the latest term's
|
|
29
|
+
* `endedOn`); null or omitted otherwise. `EmployeeList` shows it next to the
|
|
30
|
+
* status and never compares it with today.
|
|
31
|
+
*/
|
|
32
|
+
endsOn?: IsoDate | null;
|
|
33
|
+
}
|
|
34
|
+
/** The employment fields `toEmployeeView` reads; an `Employment` satisfies it. */
|
|
35
|
+
export interface EmploymentLike {
|
|
36
|
+
id?: string | null;
|
|
37
|
+
employeeNumber: string;
|
|
38
|
+
position: string | null;
|
|
39
|
+
workerType: string;
|
|
40
|
+
status: EmploymentStatus;
|
|
41
|
+
userId?: string | null;
|
|
42
|
+
}
|
|
43
|
+
/** Existing values passed to `EmployeeForm` when editing. */
|
|
44
|
+
export interface EmployeeFormInitial {
|
|
45
|
+
employeeNumber: string;
|
|
46
|
+
workerType: string;
|
|
47
|
+
position: string | null;
|
|
48
|
+
/** The linked login (`smrt-users:User` id), if any. */
|
|
49
|
+
userId: string | null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* What `EmployeeForm` hands to `onsubmit`. Values are trimmed; a blank
|
|
53
|
+
* position or login is null. The host maps them onto `EmploymentService`:
|
|
54
|
+
* a new hire onto `hire` (with the profile it chose), an edit onto
|
|
55
|
+
* `changePosition` / `changeWorkerType` / `linkLogin` / `unlinkLogin` for each
|
|
56
|
+
* value that differs from the stored employment, each dated `effectiveOn`.
|
|
57
|
+
* A login link or unlink takes effect at once whatever the date, so a
|
|
58
|
+
* future-dated edit that also clears the login unlinks it immediately.
|
|
59
|
+
*/
|
|
60
|
+
export interface EmployeeFormValues {
|
|
61
|
+
employeeNumber: string;
|
|
62
|
+
workerType: string;
|
|
63
|
+
position: string | null;
|
|
64
|
+
userId: string | null;
|
|
65
|
+
/** First day employed. Set for a new hire, null for an edit. */
|
|
66
|
+
startedOn: IsoDate | null;
|
|
67
|
+
/** The day an edit's changes take effect. Set for an edit, null for a new hire. */
|
|
68
|
+
effectiveOn: IsoDate | null;
|
|
69
|
+
}
|
|
70
|
+
/** A login the host offers in `EmployeeForm`'s login field. */
|
|
71
|
+
export interface EmployeeLoginOption {
|
|
72
|
+
/** The `smrt-users:User` id. */
|
|
73
|
+
value: string;
|
|
74
|
+
/** What the person sees, for example an email address. */
|
|
75
|
+
label: string;
|
|
76
|
+
}
|
|
77
|
+
/** One row of `PersonQualifications`. */
|
|
78
|
+
export interface PersonQualificationView {
|
|
79
|
+
/** The `HeldQualification` id. */
|
|
80
|
+
id: string;
|
|
81
|
+
name: string;
|
|
82
|
+
kind: QualificationKind;
|
|
83
|
+
issuingBody: string;
|
|
84
|
+
certificateNumber: string;
|
|
85
|
+
issuedOn: IsoDate;
|
|
86
|
+
/** Last day valid, inclusive; null never expires. */
|
|
87
|
+
expiresOn: IsoDate | null;
|
|
88
|
+
/** Status as of the host's "today". */
|
|
89
|
+
status: QualificationStatusOn;
|
|
90
|
+
/** Whole days from today to `expiresOn` (negative once past); null never expires. */
|
|
91
|
+
daysUntilExpiry: number | null;
|
|
92
|
+
}
|
|
93
|
+
/** The held-qualification fields the adapters read. */
|
|
94
|
+
export interface HeldQualificationLike {
|
|
95
|
+
id?: string | null;
|
|
96
|
+
issuedOn: IsoDate;
|
|
97
|
+
expiresOn: IsoDate | null;
|
|
98
|
+
certificateNumber: string;
|
|
99
|
+
}
|
|
100
|
+
/** The definition fields the adapters read. */
|
|
101
|
+
export interface QualificationLike {
|
|
102
|
+
name: string;
|
|
103
|
+
kind: QualificationKind;
|
|
104
|
+
issuingBody: string;
|
|
105
|
+
}
|
|
106
|
+
/** A `QualificationService.listForProfile` row, structurally. */
|
|
107
|
+
export interface ProfileQualificationLike {
|
|
108
|
+
held: HeldQualificationLike;
|
|
109
|
+
qualification: QualificationLike;
|
|
110
|
+
status: QualificationStatusOn;
|
|
111
|
+
}
|
|
112
|
+
/** Resolves to `Row`, and fails to compile unless `Row` is assignable to `Like`. */
|
|
113
|
+
type Satisfies<Like, Row extends Like> = Row;
|
|
114
|
+
/**
|
|
115
|
+
* Compile-time proof that what the services return fits the structural
|
|
116
|
+
* inputs of the adapters below: an `Employment` is an {@link EmploymentLike}
|
|
117
|
+
* and a `listForProfile` row is a {@link ProfileQualificationLike}. Typecheck
|
|
118
|
+
* fails here if a model and its view input drift apart.
|
|
119
|
+
*/
|
|
120
|
+
export type HrViewInputs = [
|
|
121
|
+
Satisfies<EmploymentLike, Employment>,
|
|
122
|
+
Satisfies<ProfileQualificationLike, ProfileQualification>
|
|
123
|
+
];
|
|
124
|
+
/** One row of `ExpiringQualificationsList`. */
|
|
125
|
+
export interface ExpiringQualificationView {
|
|
126
|
+
/** The `HeldQualification` id. */
|
|
127
|
+
id: string;
|
|
128
|
+
/** The holder's name, supplied by the host. */
|
|
129
|
+
displayName: string;
|
|
130
|
+
qualificationName: string;
|
|
131
|
+
expiresOn: IsoDate;
|
|
132
|
+
daysUntilExpiry: number;
|
|
133
|
+
}
|
|
134
|
+
/** How a qualification is shown: its status, with "expiring soon" split out. */
|
|
135
|
+
export type QualificationExpiryState = 'valid' | 'expiring-soon' | 'expired' | 'suspended' | 'revoked' | 'not-yet-issued';
|
|
136
|
+
/** Default "expiring soon" window, in days. */
|
|
137
|
+
export declare const DEFAULT_EXPIRING_SOON_DAYS = 30;
|
|
138
|
+
/** Whether `value` is a real calendar date written `YYYY-MM-DD`. */
|
|
139
|
+
export declare function isCalendarDate(value: unknown): value is IsoDate;
|
|
140
|
+
/**
|
|
141
|
+
* Whole calendar days from `today` to `date` (negative when `date` is past).
|
|
142
|
+
* Both are `YYYY-MM-DD`; the arithmetic is UTC, so no timezone or daylight
|
|
143
|
+
* saving shift can move the answer.
|
|
144
|
+
*
|
|
145
|
+
* @throws RangeError when either value is not a real calendar date
|
|
146
|
+
*/
|
|
147
|
+
export declare function daysUntil(today: IsoDate, date: IsoDate): number;
|
|
148
|
+
/** Whether a worker type is lowercase kebab-case, as the service requires. */
|
|
149
|
+
export declare function isWorkerType(value: string): boolean;
|
|
150
|
+
/** Message key for an employment status label. */
|
|
151
|
+
export declare function employmentStatusLabelKey(status: EmploymentStatus): 'human_resources.employee_list.status_active' | 'human_resources.employee_list.status_on_leave' | 'human_resources.employee_list.status_ended';
|
|
152
|
+
/** Map an employment status onto `StatusBadge` default color-scheme keys. */
|
|
153
|
+
export declare function employmentStatusBadgeKey(status: EmploymentStatus): string;
|
|
154
|
+
/**
|
|
155
|
+
* How to show a qualification: its status, with a valid one whose last day is
|
|
156
|
+
* within `expiringSoonDays` (inclusive, counting today) reported as
|
|
157
|
+
* `expiring-soon`. A `valid` row whose last day has passed shows as expired.
|
|
158
|
+
*/
|
|
159
|
+
export declare function qualificationExpiryState(view: Pick<PersonQualificationView, 'status' | 'daysUntilExpiry'>, expiringSoonDays?: number): QualificationExpiryState;
|
|
160
|
+
/** Message key for a qualification expiry state label. */
|
|
161
|
+
export declare function qualificationStateLabelKey(state: QualificationExpiryState): 'human_resources.person_qualifications.state_valid' | 'human_resources.person_qualifications.state_expiring_soon' | 'human_resources.person_qualifications.state_expired' | 'human_resources.person_qualifications.state_suspended' | 'human_resources.person_qualifications.state_revoked' | 'human_resources.person_qualifications.state_not_yet_issued';
|
|
162
|
+
/** Map a qualification expiry state onto `StatusBadge` default color-scheme keys. */
|
|
163
|
+
export declare function qualificationStateBadgeKey(state: QualificationExpiryState): string;
|
|
164
|
+
/** Message key for a qualification kind label. */
|
|
165
|
+
export declare function qualificationKindLabelKey(kind: QualificationKind): 'human_resources.person_qualifications.kind_ticket' | 'human_resources.person_qualifications.kind_certification' | 'human_resources.person_qualifications.kind_authorization' | 'human_resources.person_qualifications.kind_training' | 'human_resources.person_qualifications.kind_restriction';
|
|
166
|
+
/**
|
|
167
|
+
* Adapt an employment to an `EmployeeList` row. The host supplies the name
|
|
168
|
+
* and the start date (from `EmploymentService.terms`), and may pass the
|
|
169
|
+
* latest term's `endedOn` as `endsOn` so the list shows the last day employed
|
|
170
|
+
* beside an `ended` status during a notice period.
|
|
171
|
+
*/
|
|
172
|
+
export declare function toEmployeeView(employment: EmploymentLike, displayName: string, startedOn: IsoDate | null, endsOn?: IsoDate | null): EmployeeView;
|
|
173
|
+
/** Adapt an employment to `EmployeeForm`'s `employee` prop. */
|
|
174
|
+
export declare function toEmployeeFormInitial(employment: EmploymentLike): EmployeeFormInitial;
|
|
175
|
+
/**
|
|
176
|
+
* Adapt a `QualificationService.listForProfile(profileId, today)` row. Pass
|
|
177
|
+
* the same `today` given to the service, so the status and the day count agree.
|
|
178
|
+
*/
|
|
179
|
+
export declare function toPersonQualificationView(row: ProfileQualificationLike, today: IsoDate): PersonQualificationView;
|
|
180
|
+
/**
|
|
181
|
+
* Adapt a `QualificationService.expiringWithin(days, today)` row. The host
|
|
182
|
+
* supplies the holder's name and the qualification's name.
|
|
183
|
+
*
|
|
184
|
+
* @throws RangeError when the held qualification never expires
|
|
185
|
+
*/
|
|
186
|
+
export declare function toExpiringQualificationView(held: HeldQualificationLike, displayName: string, qualificationName: string, today: IsoDate): ExpiringQualificationView;
|
|
187
|
+
/** A copy sorted by soonest expiry, then holder, then qualification name. */
|
|
188
|
+
export declare function sortBySoonestExpiry(items: readonly ExpiringQualificationView[]): ExpiringQualificationView[];
|
|
189
|
+
/**
|
|
190
|
+
* Message key and day count describing how far away an expiry is. `days` is
|
|
191
|
+
* always zero or more, for the `{days}` placeholder.
|
|
192
|
+
*/
|
|
193
|
+
export declare function expiryDistance(daysUntilExpiry: number): {
|
|
194
|
+
key: 'human_resources.expiry.today' | 'human_resources.expiry.in_one_day' | 'human_resources.expiry.in_days' | 'human_resources.expiry.one_day_ago' | 'human_resources.expiry.days_ago';
|
|
195
|
+
days: number;
|
|
196
|
+
};
|
|
197
|
+
/** A field `validateEmployeeForm` can reject. */
|
|
198
|
+
export type EmployeeFormField = 'employeeNumber' | 'workerType' | 'startedOn' | 'effectiveOn';
|
|
199
|
+
/** Result of {@link validateEmployeeForm}. */
|
|
200
|
+
export type EmployeeFormValidation = {
|
|
201
|
+
ok: true;
|
|
202
|
+
values: EmployeeFormValues;
|
|
203
|
+
} | {
|
|
204
|
+
ok: false;
|
|
205
|
+
invalid: EmployeeFormField[];
|
|
206
|
+
};
|
|
207
|
+
/** The raw text `EmployeeForm` collects, before trimming and checks. */
|
|
208
|
+
export interface EmployeeFormDraft {
|
|
209
|
+
employeeNumber: string;
|
|
210
|
+
workerType: string;
|
|
211
|
+
position: string;
|
|
212
|
+
userId: string;
|
|
213
|
+
startedOn: string;
|
|
214
|
+
effectiveOn: string;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Normalize and check an `EmployeeForm` draft against the same field rules
|
|
218
|
+
* `EmploymentService` enforces: employee number required, worker type
|
|
219
|
+
* lowercase kebab-case, a real calendar start date for a new hire and a real
|
|
220
|
+
* calendar effective date for an edit. The values are not a service input by
|
|
221
|
+
* themselves; see {@link EmployeeFormValues} for how a host maps them.
|
|
222
|
+
*/
|
|
223
|
+
export declare function validateEmployeeForm(draft: Readonly<Partial<EmployeeFormDraft>>, isNew: boolean): EmployeeFormValidation;
|
|
224
|
+
export {};
|
|
225
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/svelte/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializable view types and pure helpers for the HR components.
|
|
3
|
+
*
|
|
4
|
+
* Components take plain view objects (not model instances) so hosts can pass
|
|
5
|
+
* data across the server/client boundary. HR holds no identity data, so every
|
|
6
|
+
* name on a view comes from the host. Dates are `YYYY-MM-DD` calendar dates:
|
|
7
|
+
* all arithmetic here is UTC, never local time. Explicit interfaces (not
|
|
8
|
+
* inline intersections) keep Svelte 5 prop type evaluation cheap.
|
|
9
|
+
*/
|
|
10
|
+
/** Default "expiring soon" window, in days. */
|
|
11
|
+
export const DEFAULT_EXPIRING_SOON_DAYS = 30;
|
|
12
|
+
const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
13
|
+
const DAY_MS = 86_400_000;
|
|
14
|
+
const WORKER_TYPE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
15
|
+
/** UTC midnight of a `YYYY-MM-DD` date in milliseconds, or null when it is not a real date. */
|
|
16
|
+
function utcDay(value) {
|
|
17
|
+
if (typeof value !== 'string')
|
|
18
|
+
return null;
|
|
19
|
+
const match = ISO_DATE.exec(value);
|
|
20
|
+
if (!match)
|
|
21
|
+
return null;
|
|
22
|
+
const [year, month, day] = match.slice(1).map(Number);
|
|
23
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
24
|
+
if (date.getUTCFullYear() !== year ||
|
|
25
|
+
date.getUTCMonth() !== month - 1 ||
|
|
26
|
+
date.getUTCDate() !== day)
|
|
27
|
+
return null;
|
|
28
|
+
return date.getTime();
|
|
29
|
+
}
|
|
30
|
+
/** Whether `value` is a real calendar date written `YYYY-MM-DD`. */
|
|
31
|
+
export function isCalendarDate(value) {
|
|
32
|
+
return utcDay(value) !== null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whole calendar days from `today` to `date` (negative when `date` is past).
|
|
36
|
+
* Both are `YYYY-MM-DD`; the arithmetic is UTC, so no timezone or daylight
|
|
37
|
+
* saving shift can move the answer.
|
|
38
|
+
*
|
|
39
|
+
* @throws RangeError when either value is not a real calendar date
|
|
40
|
+
*/
|
|
41
|
+
export function daysUntil(today, date) {
|
|
42
|
+
const from = utcDay(today);
|
|
43
|
+
const to = utcDay(date);
|
|
44
|
+
if (from === null || to === null)
|
|
45
|
+
throw new RangeError(`daysUntil needs YYYY-MM-DD calendar dates, got '${String(today)}' and '${String(date)}'.`);
|
|
46
|
+
return Math.round((to - from) / DAY_MS);
|
|
47
|
+
}
|
|
48
|
+
/** Whether a worker type is lowercase kebab-case, as the service requires. */
|
|
49
|
+
export function isWorkerType(value) {
|
|
50
|
+
return WORKER_TYPE.test(value);
|
|
51
|
+
}
|
|
52
|
+
/** Message key for an employment status label. */
|
|
53
|
+
export function employmentStatusLabelKey(status) {
|
|
54
|
+
switch (status) {
|
|
55
|
+
case 'on-leave':
|
|
56
|
+
return 'human_resources.employee_list.status_on_leave';
|
|
57
|
+
case 'ended':
|
|
58
|
+
return 'human_resources.employee_list.status_ended';
|
|
59
|
+
default:
|
|
60
|
+
return 'human_resources.employee_list.status_active';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Map an employment status onto `StatusBadge` default color-scheme keys. */
|
|
64
|
+
export function employmentStatusBadgeKey(status) {
|
|
65
|
+
switch (status) {
|
|
66
|
+
case 'on-leave':
|
|
67
|
+
return 'pending';
|
|
68
|
+
case 'ended':
|
|
69
|
+
return 'inactive';
|
|
70
|
+
default:
|
|
71
|
+
return 'active';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* How to show a qualification: its status, with a valid one whose last day is
|
|
76
|
+
* within `expiringSoonDays` (inclusive, counting today) reported as
|
|
77
|
+
* `expiring-soon`. A `valid` row whose last day has passed shows as expired.
|
|
78
|
+
*/
|
|
79
|
+
export function qualificationExpiryState(view, expiringSoonDays = DEFAULT_EXPIRING_SOON_DAYS) {
|
|
80
|
+
if (view.status !== 'valid')
|
|
81
|
+
return view.status;
|
|
82
|
+
if (view.daysUntilExpiry === null)
|
|
83
|
+
return 'valid';
|
|
84
|
+
if (view.daysUntilExpiry < 0)
|
|
85
|
+
return 'expired';
|
|
86
|
+
return view.daysUntilExpiry <= expiringSoonDays ? 'expiring-soon' : 'valid';
|
|
87
|
+
}
|
|
88
|
+
/** Message key for a qualification expiry state label. */
|
|
89
|
+
export function qualificationStateLabelKey(state) {
|
|
90
|
+
switch (state) {
|
|
91
|
+
case 'expiring-soon':
|
|
92
|
+
return 'human_resources.person_qualifications.state_expiring_soon';
|
|
93
|
+
case 'expired':
|
|
94
|
+
return 'human_resources.person_qualifications.state_expired';
|
|
95
|
+
case 'suspended':
|
|
96
|
+
return 'human_resources.person_qualifications.state_suspended';
|
|
97
|
+
case 'revoked':
|
|
98
|
+
return 'human_resources.person_qualifications.state_revoked';
|
|
99
|
+
case 'not-yet-issued':
|
|
100
|
+
return 'human_resources.person_qualifications.state_not_yet_issued';
|
|
101
|
+
default:
|
|
102
|
+
return 'human_resources.person_qualifications.state_valid';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Map a qualification expiry state onto `StatusBadge` default color-scheme keys. */
|
|
106
|
+
export function qualificationStateBadgeKey(state) {
|
|
107
|
+
switch (state) {
|
|
108
|
+
case 'valid':
|
|
109
|
+
return 'success';
|
|
110
|
+
case 'expiring-soon':
|
|
111
|
+
return 'warning';
|
|
112
|
+
case 'expired':
|
|
113
|
+
case 'revoked':
|
|
114
|
+
return 'error';
|
|
115
|
+
case 'suspended':
|
|
116
|
+
return 'pending';
|
|
117
|
+
default:
|
|
118
|
+
return 'inactive';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Message key for a qualification kind label. */
|
|
122
|
+
export function qualificationKindLabelKey(kind) {
|
|
123
|
+
switch (kind) {
|
|
124
|
+
case 'ticket':
|
|
125
|
+
return 'human_resources.person_qualifications.kind_ticket';
|
|
126
|
+
case 'authorization':
|
|
127
|
+
return 'human_resources.person_qualifications.kind_authorization';
|
|
128
|
+
case 'training':
|
|
129
|
+
return 'human_resources.person_qualifications.kind_training';
|
|
130
|
+
case 'restriction':
|
|
131
|
+
return 'human_resources.person_qualifications.kind_restriction';
|
|
132
|
+
default:
|
|
133
|
+
return 'human_resources.person_qualifications.kind_certification';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Adapt an employment to an `EmployeeList` row. The host supplies the name
|
|
138
|
+
* and the start date (from `EmploymentService.terms`), and may pass the
|
|
139
|
+
* latest term's `endedOn` as `endsOn` so the list shows the last day employed
|
|
140
|
+
* beside an `ended` status during a notice period.
|
|
141
|
+
*/
|
|
142
|
+
export function toEmployeeView(employment, displayName, startedOn, endsOn = null) {
|
|
143
|
+
return {
|
|
144
|
+
id: String(employment.id ?? ''),
|
|
145
|
+
displayName,
|
|
146
|
+
employeeNumber: employment.employeeNumber,
|
|
147
|
+
position: employment.position,
|
|
148
|
+
workerType: employment.workerType,
|
|
149
|
+
status: employment.status,
|
|
150
|
+
startedOn,
|
|
151
|
+
endsOn,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** Adapt an employment to `EmployeeForm`'s `employee` prop. */
|
|
155
|
+
export function toEmployeeFormInitial(employment) {
|
|
156
|
+
return {
|
|
157
|
+
employeeNumber: employment.employeeNumber,
|
|
158
|
+
workerType: employment.workerType,
|
|
159
|
+
position: employment.position,
|
|
160
|
+
userId: employment.userId ?? null,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Adapt a `QualificationService.listForProfile(profileId, today)` row. Pass
|
|
165
|
+
* the same `today` given to the service, so the status and the day count agree.
|
|
166
|
+
*/
|
|
167
|
+
export function toPersonQualificationView(row, today) {
|
|
168
|
+
return {
|
|
169
|
+
id: String(row.held.id ?? ''),
|
|
170
|
+
name: row.qualification.name,
|
|
171
|
+
kind: row.qualification.kind,
|
|
172
|
+
issuingBody: row.qualification.issuingBody,
|
|
173
|
+
certificateNumber: row.held.certificateNumber,
|
|
174
|
+
issuedOn: row.held.issuedOn,
|
|
175
|
+
expiresOn: row.held.expiresOn,
|
|
176
|
+
status: row.status,
|
|
177
|
+
daysUntilExpiry: row.held.expiresOn === null ? null : daysUntil(today, row.held.expiresOn),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Adapt a `QualificationService.expiringWithin(days, today)` row. The host
|
|
182
|
+
* supplies the holder's name and the qualification's name.
|
|
183
|
+
*
|
|
184
|
+
* @throws RangeError when the held qualification never expires
|
|
185
|
+
*/
|
|
186
|
+
export function toExpiringQualificationView(held, displayName, qualificationName, today) {
|
|
187
|
+
if (held.expiresOn === null)
|
|
188
|
+
throw new RangeError('A qualification that never expires is not expiring.');
|
|
189
|
+
return {
|
|
190
|
+
id: String(held.id ?? ''),
|
|
191
|
+
displayName,
|
|
192
|
+
qualificationName,
|
|
193
|
+
expiresOn: held.expiresOn,
|
|
194
|
+
daysUntilExpiry: daysUntil(today, held.expiresOn),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/** A copy sorted by soonest expiry, then holder, then qualification name. */
|
|
198
|
+
export function sortBySoonestExpiry(items) {
|
|
199
|
+
return [...items].sort((a, b) => a.daysUntilExpiry - b.daysUntilExpiry ||
|
|
200
|
+
a.expiresOn.localeCompare(b.expiresOn) ||
|
|
201
|
+
a.displayName.localeCompare(b.displayName) ||
|
|
202
|
+
a.qualificationName.localeCompare(b.qualificationName));
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Message key and day count describing how far away an expiry is. `days` is
|
|
206
|
+
* always zero or more, for the `{days}` placeholder.
|
|
207
|
+
*/
|
|
208
|
+
export function expiryDistance(daysUntilExpiry) {
|
|
209
|
+
const days = Math.abs(daysUntilExpiry);
|
|
210
|
+
if (daysUntilExpiry === 0)
|
|
211
|
+
return { key: 'human_resources.expiry.today', days };
|
|
212
|
+
if (daysUntilExpiry === 1)
|
|
213
|
+
return { key: 'human_resources.expiry.in_one_day', days };
|
|
214
|
+
if (daysUntilExpiry > 1)
|
|
215
|
+
return { key: 'human_resources.expiry.in_days', days };
|
|
216
|
+
if (daysUntilExpiry === -1)
|
|
217
|
+
return { key: 'human_resources.expiry.one_day_ago', days };
|
|
218
|
+
return { key: 'human_resources.expiry.days_ago', days };
|
|
219
|
+
}
|
|
220
|
+
function textOf(value) {
|
|
221
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Normalize and check an `EmployeeForm` draft against the same field rules
|
|
225
|
+
* `EmploymentService` enforces: employee number required, worker type
|
|
226
|
+
* lowercase kebab-case, a real calendar start date for a new hire and a real
|
|
227
|
+
* calendar effective date for an edit. The values are not a service input by
|
|
228
|
+
* themselves; see {@link EmployeeFormValues} for how a host maps them.
|
|
229
|
+
*/
|
|
230
|
+
export function validateEmployeeForm(draft, isNew) {
|
|
231
|
+
const employeeNumber = textOf(draft.employeeNumber);
|
|
232
|
+
const workerType = textOf(draft.workerType);
|
|
233
|
+
const startedOn = textOf(draft.startedOn);
|
|
234
|
+
const effectiveOn = textOf(draft.effectiveOn);
|
|
235
|
+
const invalid = [];
|
|
236
|
+
if (!employeeNumber)
|
|
237
|
+
invalid.push('employeeNumber');
|
|
238
|
+
if (!isWorkerType(workerType))
|
|
239
|
+
invalid.push('workerType');
|
|
240
|
+
if (isNew && !isCalendarDate(startedOn))
|
|
241
|
+
invalid.push('startedOn');
|
|
242
|
+
if (!isNew && !isCalendarDate(effectiveOn))
|
|
243
|
+
invalid.push('effectiveOn');
|
|
244
|
+
if (invalid.length > 0)
|
|
245
|
+
return { ok: false, invalid };
|
|
246
|
+
return {
|
|
247
|
+
ok: true,
|
|
248
|
+
values: {
|
|
249
|
+
employeeNumber,
|
|
250
|
+
workerType,
|
|
251
|
+
position: textOf(draft.position) || null,
|
|
252
|
+
userId: textOf(draft.userId) || null,
|
|
253
|
+
startedOn: isNew ? startedOn : null,
|
|
254
|
+
effectiveOn: isNew ? null : effectiveOn,
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared vocabulary, actor, event and error types for the HR package.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import type { Employment } from './employment/models.js';
|
|
7
|
+
import type { HeldQualification } from './qualifications/models.js';
|
|
8
|
+
/**
|
|
9
|
+
* Server-side, trusted scope. The application authorizes the acting person
|
|
10
|
+
* before constructing a service; `profileId` is recorded as the actor on every
|
|
11
|
+
* dated change.
|
|
12
|
+
*/
|
|
13
|
+
export interface HrActor {
|
|
14
|
+
/** The employing tenant. */
|
|
15
|
+
tenantId: string;
|
|
16
|
+
/** The profile of the person performing the action. */
|
|
17
|
+
profileId: string;
|
|
18
|
+
}
|
|
19
|
+
/** A calendar date written `YYYY-MM-DD`; never an instant. */
|
|
20
|
+
export type IsoDate = string;
|
|
21
|
+
/**
|
|
22
|
+
* Stored employment status. `ended` means an end has been recorded, not that
|
|
23
|
+
* the person is no longer employed today: the last day may still be ahead (a
|
|
24
|
+
* notice period). "Employed on a date" is computed from terms; use
|
|
25
|
+
* `EmploymentService.check`, `employedOn`, `asOf` or `findByUser(userId, on)`.
|
|
26
|
+
*/
|
|
27
|
+
export type EmploymentStatus = 'active' | 'on-leave' | 'ended';
|
|
28
|
+
/**
|
|
29
|
+
* Allowed changes of the stored status; `ended → active` is a rehire. Leave
|
|
30
|
+
* recorded inside the final term of an ended employment leaves the status
|
|
31
|
+
* `ended`.
|
|
32
|
+
*/
|
|
33
|
+
export declare const EMPLOYMENT_STATUS_TRANSITIONS: Record<EmploymentStatus, readonly EmploymentStatus[]>;
|
|
34
|
+
/** Worker types shipped as suggestions; any lowercase kebab-case value works. */
|
|
35
|
+
export declare const SUGGESTED_WORKER_TYPES: readonly ["employee", "apprentice", "contractor"];
|
|
36
|
+
/** An open vocabulary: the suggestions, or an application's own value. */
|
|
37
|
+
export type WorkerType = (typeof SUGGESTED_WORKER_TYPES)[number] | (string & {});
|
|
38
|
+
/** Kinds of dated change kept in the employment history. */
|
|
39
|
+
export type EmploymentChangeKind = 'hired' | 'ended' | 'rehired' | 'position-changed' | 'worker-type-changed' | 'leave-started' | 'leave-ended' | 'login-linked' | 'login-unlinked';
|
|
40
|
+
/**
|
|
41
|
+
* What a qualification is. A `restriction` (for example "youth worker") limits
|
|
42
|
+
* what its holder may do instead of permitting something.
|
|
43
|
+
*/
|
|
44
|
+
export type QualificationKind = 'ticket' | 'certification' | 'authorization' | 'training' | 'restriction';
|
|
45
|
+
/**
|
|
46
|
+
* Who a held qualification belongs to: the `person` (it survives a change of
|
|
47
|
+
* employment) or the `employment` (it ends with it).
|
|
48
|
+
*/
|
|
49
|
+
export type QualificationScope = 'person' | 'employment';
|
|
50
|
+
/** Stored held-qualification status. Expiry on a date is computed. */
|
|
51
|
+
export type HeldQualificationStatus = 'valid' | 'expired' | 'suspended' | 'revoked';
|
|
52
|
+
/** Kinds of dated change kept in a held qualification's history. */
|
|
53
|
+
export type HeldQualificationChangeKind = 'granted' | 'renewed' | 'suspended' | 'reinstated' | 'revoked' | 'expired';
|
|
54
|
+
/** Result of asking whether a profile is employed on a date. */
|
|
55
|
+
export type EmploymentCheck = {
|
|
56
|
+
ok: true;
|
|
57
|
+
employmentId: string;
|
|
58
|
+
onLeave: boolean;
|
|
59
|
+
} | {
|
|
60
|
+
ok: false;
|
|
61
|
+
reason: 'not-employed' | 'not-started' | 'ended';
|
|
62
|
+
};
|
|
63
|
+
/** Result of asking whether a profile holds a qualification on a date. */
|
|
64
|
+
export type QualificationCheck = {
|
|
65
|
+
ok: true;
|
|
66
|
+
heldQualificationId: string;
|
|
67
|
+
expiresOn: IsoDate | null;
|
|
68
|
+
} | {
|
|
69
|
+
ok: false;
|
|
70
|
+
reason: 'not-held' | 'not-yet-issued' | 'expired' | 'suspended' | 'revoked';
|
|
71
|
+
};
|
|
72
|
+
/** Lifecycle events for an employment. */
|
|
73
|
+
export type EmploymentEventType = 'employment.hired' | 'employment.ended' | 'employment.rehired' | 'employment.position-changed' | 'employment.worker-type-changed' | 'employment.leave-started' | 'employment.leave-ended';
|
|
74
|
+
/** Lifecycle events for a held qualification. */
|
|
75
|
+
export type HeldQualificationEventType = 'held-qualification.granted' | 'held-qualification.renewed' | 'held-qualification.suspended' | 'held-qualification.reinstated' | 'held-qualification.revoked' | 'held-qualification.expired';
|
|
76
|
+
/** Payload delivered to an {@link HrEventHandler}. */
|
|
77
|
+
export type HrEvent = {
|
|
78
|
+
type: EmploymentEventType;
|
|
79
|
+
employment: Employment;
|
|
80
|
+
/** The calendar date the change takes effect. */
|
|
81
|
+
effectiveOn: IsoDate;
|
|
82
|
+
at: Date;
|
|
83
|
+
byProfileId: string;
|
|
84
|
+
} | {
|
|
85
|
+
type: HeldQualificationEventType;
|
|
86
|
+
heldQualification: HeldQualification;
|
|
87
|
+
effectiveOn: IsoDate;
|
|
88
|
+
at: Date;
|
|
89
|
+
byProfileId: string;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Event hook an application provides to react to HR changes (reminders are the
|
|
93
|
+
* application's business). Called after the service's own transaction
|
|
94
|
+
* commits. Delivery is best-effort: a throwing handler is logged and
|
|
95
|
+
* swallowed, so it never rolls back a persisted change.
|
|
96
|
+
*
|
|
97
|
+
* "After commit" holds because a service only mutates through the root
|
|
98
|
+
* database handle. A service constructed with a handle that is already inside
|
|
99
|
+
* a transaction refuses every mutation (`HR_TRANSACTION_UNSUPPORTED`), so an
|
|
100
|
+
* event is never delivered for rows an outer rollback could still remove.
|
|
101
|
+
*/
|
|
102
|
+
export type HrEventHandler = (event: HrEvent) => void | Promise<void>;
|
|
103
|
+
/** Options shared by the HR services. */
|
|
104
|
+
export interface HrServiceOptions {
|
|
105
|
+
/**
|
|
106
|
+
* Optional lifecycle event hook, called after each mutation commits. The
|
|
107
|
+
* service must be constructed with the root database handle; see
|
|
108
|
+
* {@link HrEventHandler}.
|
|
109
|
+
*/
|
|
110
|
+
onEvent?: HrEventHandler;
|
|
111
|
+
}
|
|
112
|
+
/** Stable error codes raised by this package. */
|
|
113
|
+
export type HrErrorCode = 'HR_INVALID' | 'HR_NOT_FOUND' | 'HR_ACTOR_INVALID' | 'HR_TENANT_MISMATCH' | 'HR_WRITE_FORBIDDEN' | 'HR_HISTORY_IMMUTABLE' | 'HR_STATUS_TRANSITION' | 'HR_EMPLOYEE_NUMBER_TAKEN' | 'HR_ALREADY_EMPLOYED' | 'HR_TERM_OVERLAP' | 'HR_QUALIFICATION_KEY_TAKEN' | 'HR_ALREADY_HELD' | 'HR_QUALIFICATION_SCOPE' | 'HR_TRANSACTION_UNSUPPORTED';
|
|
114
|
+
/** Base error for this package. `code` is stable and safe to branch on. */
|
|
115
|
+
export declare class HrError extends Error {
|
|
116
|
+
readonly code: HrErrorCode;
|
|
117
|
+
constructor(code: HrErrorCode, message: string, options?: ErrorOptions);
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/types.ts
|
|
2
|
+
var EMPLOYMENT_STATUS_TRANSITIONS = {
|
|
3
|
+
active: ["on-leave", "ended"],
|
|
4
|
+
"on-leave": ["active", "ended"],
|
|
5
|
+
ended: ["active"]
|
|
6
|
+
};
|
|
7
|
+
var SUGGESTED_WORKER_TYPES = [
|
|
8
|
+
"employee",
|
|
9
|
+
"apprentice",
|
|
10
|
+
"contractor"
|
|
11
|
+
];
|
|
12
|
+
var HrError = class extends Error {
|
|
13
|
+
code;
|
|
14
|
+
constructor(code, message, options) {
|
|
15
|
+
super(message, options);
|
|
16
|
+
this.name = "HrError";
|
|
17
|
+
this.code = code;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
//#endregion
|
|
21
|
+
export { EMPLOYMENT_STATUS_TRANSITIONS, HrError, SUGGESTED_WORKER_TYPES };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","names":[],"sources":["../src/types.ts"],"sourcesContent":["/**\n * Shared vocabulary, actor, event and error types for the HR package.\n *\n * @packageDocumentation\n */\n\nimport type { Employment } from './employment/models.js';\nimport type { HeldQualification } from './qualifications/models.js';\n\n/**\n * Server-side, trusted scope. The application authorizes the acting person\n * before constructing a service; `profileId` is recorded as the actor on every\n * dated change.\n */\nexport interface HrActor {\n /** The employing tenant. */\n tenantId: string;\n /** The profile of the person performing the action. */\n profileId: string;\n}\n\n/** A calendar date written `YYYY-MM-DD`; never an instant. */\nexport type IsoDate = string;\n\n/**\n * Stored employment status. `ended` means an end has been recorded, not that\n * the person is no longer employed today: the last day may still be ahead (a\n * notice period). \"Employed on a date\" is computed from terms; use\n * `EmploymentService.check`, `employedOn`, `asOf` or `findByUser(userId, on)`.\n */\nexport type EmploymentStatus = 'active' | 'on-leave' | 'ended';\n\n/**\n * Allowed changes of the stored status; `ended → active` is a rehire. Leave\n * recorded inside the final term of an ended employment leaves the status\n * `ended`.\n */\nexport const EMPLOYMENT_STATUS_TRANSITIONS: Record<\n EmploymentStatus,\n readonly EmploymentStatus[]\n> = {\n active: ['on-leave', 'ended'],\n 'on-leave': ['active', 'ended'],\n ended: ['active'],\n};\n\n/** Worker types shipped as suggestions; any lowercase kebab-case value works. */\nexport const SUGGESTED_WORKER_TYPES = [\n 'employee',\n 'apprentice',\n 'contractor',\n] as const;\n/** An open vocabulary: the suggestions, or an application's own value. */\nexport type WorkerType =\n | (typeof SUGGESTED_WORKER_TYPES)[number]\n | (string & {});\n\n/** Kinds of dated change kept in the employment history. */\nexport type EmploymentChangeKind =\n | 'hired'\n | 'ended'\n | 'rehired'\n | 'position-changed'\n | 'worker-type-changed'\n | 'leave-started'\n | 'leave-ended'\n | 'login-linked'\n | 'login-unlinked';\n\n/**\n * What a qualification is. A `restriction` (for example \"youth worker\") limits\n * what its holder may do instead of permitting something.\n */\nexport type QualificationKind =\n | 'ticket'\n | 'certification'\n | 'authorization'\n | 'training'\n | 'restriction';\n\n/**\n * Who a held qualification belongs to: the `person` (it survives a change of\n * employment) or the `employment` (it ends with it).\n */\nexport type QualificationScope = 'person' | 'employment';\n\n/** Stored held-qualification status. Expiry on a date is computed. */\nexport type HeldQualificationStatus =\n | 'valid'\n | 'expired'\n | 'suspended'\n | 'revoked';\n\n/** Kinds of dated change kept in a held qualification's history. */\nexport type HeldQualificationChangeKind =\n | 'granted'\n | 'renewed'\n | 'suspended'\n | 'reinstated'\n | 'revoked'\n | 'expired';\n\n/** Result of asking whether a profile is employed on a date. */\nexport type EmploymentCheck =\n | { ok: true; employmentId: string; onLeave: boolean }\n | { ok: false; reason: 'not-employed' | 'not-started' | 'ended' };\n\n/** Result of asking whether a profile holds a qualification on a date. */\nexport type QualificationCheck =\n | { ok: true; heldQualificationId: string; expiresOn: IsoDate | null }\n | {\n ok: false;\n reason:\n | 'not-held'\n | 'not-yet-issued'\n | 'expired'\n | 'suspended'\n | 'revoked';\n };\n\n/** Lifecycle events for an employment. */\nexport type EmploymentEventType =\n | 'employment.hired'\n | 'employment.ended'\n | 'employment.rehired'\n | 'employment.position-changed'\n | 'employment.worker-type-changed'\n | 'employment.leave-started'\n | 'employment.leave-ended';\n\n/** Lifecycle events for a held qualification. */\nexport type HeldQualificationEventType =\n | 'held-qualification.granted'\n | 'held-qualification.renewed'\n | 'held-qualification.suspended'\n | 'held-qualification.reinstated'\n | 'held-qualification.revoked'\n | 'held-qualification.expired';\n\n/** Payload delivered to an {@link HrEventHandler}. */\nexport type HrEvent =\n | {\n type: EmploymentEventType;\n employment: Employment;\n /** The calendar date the change takes effect. */\n effectiveOn: IsoDate;\n at: Date;\n byProfileId: string;\n }\n | {\n type: HeldQualificationEventType;\n heldQualification: HeldQualification;\n effectiveOn: IsoDate;\n at: Date;\n byProfileId: string;\n };\n\n/**\n * Event hook an application provides to react to HR changes (reminders are the\n * application's business). Called after the service's own transaction\n * commits. Delivery is best-effort: a throwing handler is logged and\n * swallowed, so it never rolls back a persisted change.\n *\n * \"After commit\" holds because a service only mutates through the root\n * database handle. A service constructed with a handle that is already inside\n * a transaction refuses every mutation (`HR_TRANSACTION_UNSUPPORTED`), so an\n * event is never delivered for rows an outer rollback could still remove.\n */\nexport type HrEventHandler = (event: HrEvent) => void | Promise<void>;\n\n/** Options shared by the HR services. */\nexport interface HrServiceOptions {\n /**\n * Optional lifecycle event hook, called after each mutation commits. The\n * service must be constructed with the root database handle; see\n * {@link HrEventHandler}.\n */\n onEvent?: HrEventHandler;\n}\n\n/** Stable error codes raised by this package. */\nexport type HrErrorCode =\n | 'HR_INVALID'\n | 'HR_NOT_FOUND'\n | 'HR_ACTOR_INVALID'\n | 'HR_TENANT_MISMATCH'\n | 'HR_WRITE_FORBIDDEN'\n | 'HR_HISTORY_IMMUTABLE'\n | 'HR_STATUS_TRANSITION'\n | 'HR_EMPLOYEE_NUMBER_TAKEN'\n | 'HR_ALREADY_EMPLOYED'\n | 'HR_TERM_OVERLAP'\n | 'HR_QUALIFICATION_KEY_TAKEN'\n | 'HR_ALREADY_HELD'\n | 'HR_QUALIFICATION_SCOPE'\n | 'HR_TRANSACTION_UNSUPPORTED';\n\n/** Base error for this package. `code` is stable and safe to branch on. */\nexport class HrError extends Error {\n readonly code: HrErrorCode;\n\n constructor(code: HrErrorCode, message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = 'HrError';\n this.code = code;\n }\n}\n"],"mappings":";AAqCO,IAAM,gCAGT;CACF,QAAQ,CAAC,YAAY,OAAO;CAC5B,YAAY,CAAC,UAAU,OAAO;CAC9B,OAAO,CAAC,QAAQ;AAClB;AAGO,IAAM,yBAAyB;CACpC;CACA;CACA;AACF;AAmJO,IAAM,UAAN,cAAsB,MAAM;CACxB;CAET,YAAY,MAAmB,SAAiB,SAAwB;EACtE,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACF"}
|