@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
package/AGENTS.md
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# smrt-human-resources
|
|
2
|
+
|
|
3
|
+
HR records: who works for an organization, as what, and with what standing
|
|
4
|
+
(#3435). It sits between identity (`smrt-profiles`) and pay (payroll, #3292):
|
|
5
|
+
payroll → human-resources → profiles. It holds no pay, no time records, no
|
|
6
|
+
logins or permissions, and no identity or contact details.
|
|
7
|
+
|
|
8
|
+
## Models
|
|
9
|
+
|
|
10
|
+
| Model | Table | Notes |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `Employment` | `employments` | One per tenant and profile. `profileId`, optional `userId` and `employerProfileId`, `employeeNumber` (unique per tenant), `workerType`, `position`, `status` (`active` \| `on-leave` \| `ended`) |
|
|
13
|
+
| `EmploymentTerm` | `employment_terms` | One continuous period: `startedOn`, `endedOn` (last day, inclusive; null is open), `endReason`. A rehire adds a term |
|
|
14
|
+
| `EmploymentChange` | `employment_changes` | Append-only dated change: `kind`, `effectiveOn`, `fromValue`, `toValue`, `actorProfileId` |
|
|
15
|
+
| `Qualification` | `qualifications` | Definition, `key` unique per tenant: `kind`, `issuingBody`, `expires`, `validityMonths`, `scope` (`person` \| `employment`) |
|
|
16
|
+
| `HeldQualification` | `held_qualifications` | A person holds a qualification: `issuedOn`, `expiresOn`, `certificateNumber`, `status`, verifier, optional `documentAssetId`, `renewalOfId` |
|
|
17
|
+
| `HeldQualificationChange` | `held_qualification_changes` | Append-only dated change: `kind`, `effectiveOn`, `reason`, `actorProfileId` |
|
|
18
|
+
|
|
19
|
+
`EmploymentService` and `QualificationService` are the only writers (an
|
|
20
|
+
application subclass may save its own added fields; see "Extending
|
|
21
|
+
Employment"). Both are
|
|
22
|
+
constructed from a trusted `{ tenantId, profileId }` actor the application has
|
|
23
|
+
already authorized, and both take an optional `onEvent` hook. Construct them
|
|
24
|
+
with the root database handle, never one that is already inside a transaction
|
|
25
|
+
(see "Events need the root handle").
|
|
26
|
+
|
|
27
|
+
## Invariants
|
|
28
|
+
|
|
29
|
+
- **Sensitive and closed.** Every model is `@TenantScoped({ mode: 'required' })`,
|
|
30
|
+
`sensitive: true`, with `api` / `mcp` / `cli` set to `{ include: [] }`.
|
|
31
|
+
`save()` throws outside a service (`src/write.ts`) and `delete()` always
|
|
32
|
+
throws. Services insert rows only through `insertHr()` in `src/write.ts`,
|
|
33
|
+
which uses the model's public constructor, `initialize()` and
|
|
34
|
+
`requireInsertOnSave()`; never reach into a collection's protected
|
|
35
|
+
`createUnsaved()`. A subclass must restate the closed surface: `@smrt` exposure is not
|
|
36
|
+
inherited, and an empty `@smrt()` exposes everything.
|
|
37
|
+
- **Collections are closed too.** Collection classes are registered like
|
|
38
|
+
models, so each of the six carries
|
|
39
|
+
`@smrt({ api: { include: [] }, mcp: { include: [] }, cli: false })` as an
|
|
40
|
+
inline literal (the scanner cannot resolve an imported constant). An
|
|
41
|
+
undecorated collection publishes its methods as MCP tools. A collection's
|
|
42
|
+
config merges over its model's, so a model's effective `cli` reads `false`.
|
|
43
|
+
`src/__tests__/closed-surface.test.ts` asserts no generated tool exists.
|
|
44
|
+
- **The tenant is the employer.** `employerProfileId` is informational and
|
|
45
|
+
never part of uniqueness. There is one `Employment` per tenant and profile,
|
|
46
|
+
so (tenant, profile, date) resolves to at most one employment.
|
|
47
|
+
- **The employment id is stable.** Rehire adds a term to the same row; other
|
|
48
|
+
packages and applications key on `Employment.id`.
|
|
49
|
+
- **Dates are calendar dates.** `YYYY-MM-DD` text, compared as strings, with
|
|
50
|
+
no timezone. The application converts an instant to its local date first.
|
|
51
|
+
`verifiedAt` is the one instant.
|
|
52
|
+
- **"Employed on a date" comes from terms**, never from `status`. `on-leave`
|
|
53
|
+
still counts as employed.
|
|
54
|
+
- **`ended` means an end has been recorded**, not "no longer employed today".
|
|
55
|
+
`end` accepts an `endedOn` ahead of the day it is called (a notice period)
|
|
56
|
+
and stores `ended` at once; the package has no clock and never flips a
|
|
57
|
+
status when a date passes. For "on a date" use `check`, `employedOn`, `asOf`
|
|
58
|
+
or `findByUser(userId, on)`.
|
|
59
|
+
- **Dated changes belong to the latest term.** `changePosition`,
|
|
60
|
+
`changeWorkerType`, `placeOnLeave` and `returnFromLeave` are accepted when
|
|
61
|
+
`effectiveOn` is inside the latest term: the open one, or after an end is
|
|
62
|
+
recorded the final one up to and including its `endedOn`. On an ended
|
|
63
|
+
employment the stored status stays `ended`, the row's `position` /
|
|
64
|
+
`workerType` update as usual, leave is validated from the replayed history
|
|
65
|
+
as of `effectiveOn` (not from `status`), and the usual event is raised. A
|
|
66
|
+
date after the last day is `HR_STATUS_TRANSITION`; before the term started,
|
|
67
|
+
`HR_INVALID`.
|
|
68
|
+
- **History is append-only.** Changes are new rows. A term is closed once. A
|
|
69
|
+
renewal is a new `HeldQualification` pointing at the old one through
|
|
70
|
+
`renewalOfId`. A row stored `revoked` is never saved again, except for its
|
|
71
|
+
verification fields (`HeldQualification.save()` allows a save that changes
|
|
72
|
+
only `verifiedByProfileId`, `verifiedAt` and `documentAssetId`); a later
|
|
73
|
+
suspension, reinstatement or earlier revocation of one is a further change
|
|
74
|
+
row, never an edit.
|
|
75
|
+
- **A stored qualification status is "the last change recorded", not
|
|
76
|
+
standing, and no rule decides standing from it.** `revoke` and `suspend`
|
|
77
|
+
store their status at once even when `effectiveOn` is ahead, and `reinstate` stores `valid`
|
|
78
|
+
even when it takes effect later (`expired` if the sweep has recorded the
|
|
79
|
+
row's lapse).
|
|
80
|
+
The one sticky value is `revoked`: once stored it stays, whatever is
|
|
81
|
+
recorded afterwards. Every rule decides from the dated history, as `check`
|
|
82
|
+
does: the one-live-grant rule, renewal validation, `expiringWithin`, the
|
|
83
|
+
employment-end cutoff, and the transitions themselves.
|
|
84
|
+
- **Transitions are validated by replay as of `effectiveOn`.** On the latest
|
|
85
|
+
row of a chain, from the chain's whole history: `suspend` needs the chain
|
|
86
|
+
neither suspended nor revoked on `effectiveOn`; `reinstate` needs it
|
|
87
|
+
suspended (and not revoked) on `effectiveOn`; both are `HR_INVALID` when
|
|
88
|
+
dated before the chain's latest suspension or reinstatement, so those two
|
|
89
|
+
are recorded in date order. `revoke` is exempt from that ordering (it is
|
|
90
|
+
terminal and the earliest one wins in replay) and is refused only when a
|
|
91
|
+
revocation effective on or before `effectiveOn` is already recorded
|
|
92
|
+
(`HR_STATUS_TRANSITION`). So a revocation recorded for a later date (a
|
|
93
|
+
scheduled one, an employment end with a last day ahead, a grant against a
|
|
94
|
+
closed term) freezes nothing: until it takes effect the qualification can
|
|
95
|
+
be suspended, reinstated, renewed, or revoked from an earlier day. A stored
|
|
96
|
+
`expired` blocks none of them.
|
|
97
|
+
- **A term holds its own changes.** `end` rejects an `endedOn` earlier than a
|
|
98
|
+
position, worker-type or leave change recorded in the open term, and
|
|
99
|
+
`rehire` a start before one, so replay (`check().onLeave`, `asOf`) never
|
|
100
|
+
picks up a change dated outside its term. Login links are not replayed and
|
|
101
|
+
are not part of this rule.
|
|
102
|
+
- **Login links are immediate.** `linkLogin` / `unlinkLogin` write the stored
|
|
103
|
+
`userId` at once; `effectiveOn` is only recorded on the change row. `asOf`
|
|
104
|
+
does not replay them and `findByUser(userId, on)` reads the stored `userId`
|
|
105
|
+
(only the terms are dated).
|
|
106
|
+
- **A login is exclusive to the employment that stores it.** `hire`, `rehire`
|
|
107
|
+
and `linkLogin` reject (`HR_INVALID`) a `userId` stored on any other
|
|
108
|
+
employment in the tenant, whatever that employment's status: an ended
|
|
109
|
+
employment keeps its login through its notice period and after, until
|
|
110
|
+
`unlinkLogin` (which works on an ended employment) clears it. So two
|
|
111
|
+
employments never share a login through the service, and neither lookup
|
|
112
|
+
can be ambiguous; `findByUser` still fails closed on an ambiguous match as
|
|
113
|
+
a backstop for rows written some other way. `findByUser(userId)` goes by
|
|
114
|
+
the stored status; `findByUser(userId, on)` goes by terms covering `on`,
|
|
115
|
+
whatever the status, and is what a host that records end dates ahead of
|
|
116
|
+
time must call.
|
|
117
|
+
- **A renewal chain shares its standing.** `check`, `holders` and
|
|
118
|
+
`listForProfile` treat a row as not good on a date when a later row in its
|
|
119
|
+
chain is suspended or revoked on that date. `suspend`, `reinstate` and
|
|
120
|
+
`revoke` act on the latest row only (`HR_INVALID` on a row that was
|
|
121
|
+
renewed), and the change belongs to the chain: `effectiveOn` may be any
|
|
122
|
+
date from the chain's first `issuedOn` onward (see "Transitions are
|
|
123
|
+
validated by replay" for the ordering rule). It may precede the latest
|
|
124
|
+
row's own `issuedOn`. That is how a ticket in force today is withdrawn
|
|
125
|
+
after its renewal was recorded ahead of time: the earlier row stops passing from
|
|
126
|
+
`effectiveOn`, and the renewal does not restore standing when its issue
|
|
127
|
+
date arrives (revoked from issue, or suspended from issue until
|
|
128
|
+
reinstated). `listForProfile` reports the status of the latest row issued
|
|
129
|
+
by the date, so it agrees with `check` instead of answering
|
|
130
|
+
`not-yet-issued`.
|
|
131
|
+
- **A renewal is validated by date.** `renew` rejects an `issuedOn` earlier
|
|
132
|
+
than the row's own, or earlier than its latest suspension or reinstatement
|
|
133
|
+
(`HR_INVALID`: the renewal would answer for dates the history says were
|
|
134
|
+
suspended); a row suspended on `issuedOn` by replay
|
|
135
|
+
(`HR_STATUS_TRANSITION`: reinstate first); a row revoked on or before
|
|
136
|
+
`issuedOn` (`HR_INVALID`); an expiry earlier than the row's own, or any
|
|
137
|
+
expiry on a renewal of a row that has none (`HR_INVALID`: the renewed row
|
|
138
|
+
keeps answering for its own period, so a shorter renewal would lapse while
|
|
139
|
+
`check` still passed; to replace a qualification with a shorter one,
|
|
140
|
+
revoke it from that day and grant a new one); and a person who holds
|
|
141
|
+
another chain that is not
|
|
142
|
+
revoked on or before `issuedOn` and overlaps the renewal
|
|
143
|
+
(`HR_ALREADY_HELD`). A chain that only starts on or after the day the
|
|
144
|
+
renewal is cut off (its carried revocation) does not overlap, so a lapsed
|
|
145
|
+
ticket can be renewed up to its scheduled revocation even when the next
|
|
146
|
+
grant, dated from that day, was recorded first.
|
|
147
|
+
- **A renewal carries a pending cutoff forward.** A row whose revocation
|
|
148
|
+
takes effect after `issuedOn` is still in force and is renewed normally;
|
|
149
|
+
replay only propagates standing from later rows to earlier ones, so the
|
|
150
|
+
renewal records the cutoff on itself: a `revoked` change at the earlier of
|
|
151
|
+
the old row's pending revocation (with its reason) and, inside a closed
|
|
152
|
+
employment term, `endedOn + 1` (`employment-ended`; it wins a tie). The
|
|
153
|
+
renewal is stored `revoked` and raises `renewed` then `revoked`. Each
|
|
154
|
+
remaining latest row therefore holds its chain's revocation, which
|
|
155
|
+
`expiringWithin`, the one-live-grant rule and `renew` rely on.
|
|
156
|
+
- **One live grant, by date.** `grant` is refused (`HR_ALREADY_HELD`) unless
|
|
157
|
+
every chain the person already holds of that qualification is revoked
|
|
158
|
+
effective on or before the new `issuedOn`. A revocation recorded for a
|
|
159
|
+
later date does not free an earlier issue date (the message says to renew
|
|
160
|
+
the existing one), and a lapsed ticket, swept or not, is renewed instead of
|
|
161
|
+
granted again.
|
|
162
|
+
- **Verification is not a status.** `verify` sets `verifiedByProfileId` and
|
|
163
|
+
`verifiedAt` (and optionally the document) on any row, including one stored
|
|
164
|
+
`revoked`; it writes no change row and raises no event.
|
|
165
|
+
- **Ids are validated.** A blank or missing id is `HR_INVALID` in both
|
|
166
|
+
services (`HrService.id`); the `check` methods answer `not-employed` /
|
|
167
|
+
`not-held` instead.
|
|
168
|
+
- **Qualification scope.** A `person`-scoped held qualification survives a
|
|
169
|
+
change of employment. An `employment`-scoped one carries `employmentId` and
|
|
170
|
+
is revoked, with reason `employment-ended`, in the transaction that ends the
|
|
171
|
+
employment. The revocation is effective the day after `endedOn`, so the
|
|
172
|
+
qualification is still good on the last day employed. The cutoff is the
|
|
173
|
+
earliest one, decided by date and per renewal chain
|
|
174
|
+
(`revokeEmploymentQualifications`): the latest row of each chain of the
|
|
175
|
+
employment gets it, with one `held-qualification.revoked` event, unless a
|
|
176
|
+
revocation effective on or before it is already recorded anywhere in the
|
|
177
|
+
chain. Rows that were renewed are not written; replay cuts them off through
|
|
178
|
+
the chain. A latest row already stored `revoked` for a later date gets the
|
|
179
|
+
change row only and the event carries the earlier date; replay takes the
|
|
180
|
+
earliest revocation. A grant or renewal dated inside a term whose end is
|
|
181
|
+
already recorded (granted during notice, or backdated) gets the same
|
|
182
|
+
revocation in its own transaction and is stored `revoked` from the start.
|
|
183
|
+
- **Expiry is computed.** A check on a date reads `expiresOn`; the stored
|
|
184
|
+
`expired` status is written by `QualificationService.sweepExpired()`,
|
|
185
|
+
which the application calls from its own scheduler (and by reinstating a
|
|
186
|
+
row whose lapse the sweep has recorded); it is bookkeeping and
|
|
187
|
+
no rule reads standing from it. The sweep decides which rows lapsed by
|
|
188
|
+
date: every row whose last day is before `today`, that has no `expired`
|
|
189
|
+
change yet and was not revoked on or before the day after `expiresOn`,
|
|
190
|
+
gets an `expired` change and (unless it was renewed) the event. That
|
|
191
|
+
includes a ticket that runs out while suspended, or while a revocation is
|
|
192
|
+
recorded for a later day (a notice period). Only a row stored `valid` has
|
|
193
|
+
its stored status set to `expired`; a stored `suspended` or `revoked`
|
|
194
|
+
stays. Those rows are re-read on every run, so a scheduled sweep should
|
|
195
|
+
pass `{ since }` (a day on or before the last day its previous run
|
|
196
|
+
covered) to bound the work. `expiringWithin(days, today,
|
|
197
|
+
{ employedOnly })` lists rows that are good on `today` by the same dated
|
|
198
|
+
evaluation as `check` (issued by `today`, not suspended or revoked on it,
|
|
199
|
+
not renewed) and can keep only people employed on `today`.
|
|
200
|
+
- **Events need the root handle.** A mutation begins its own transaction
|
|
201
|
+
with `db.beginTransaction()`, commits it, then delivers its events.
|
|
202
|
+
`@happyvertical/sql` has no after-commit hook and no marker for "inside a
|
|
203
|
+
transaction" (happyvertical/sdk#1249), so on a handle that is already
|
|
204
|
+
inside one (the `tx` of `db.transaction()`, a savepoint scope, or a
|
|
205
|
+
`beginTransaction()` handle) the commit would be the caller's and an outer
|
|
206
|
+
rollback could follow a delivered event. `HrService.transact` therefore
|
|
207
|
+
refuses a mutation on such a handle with `HR_TRANSACTION_UNSUPPORTED`
|
|
208
|
+
before writing anything. It recognises one by asking it to begin: a
|
|
209
|
+
transaction-scoped handle either has no `beginTransaction` (libsql SQLite,
|
|
210
|
+
PostgreSQL, DuckDB, JSON) or exposes one that throws
|
|
211
|
+
`NestedTransactionError` without touching the connection (the
|
|
212
|
+
native-capabilities SQLite adapter, opened for `vector` /
|
|
213
|
+
`notifications`). Never go back to `db.transaction()` /
|
|
214
|
+
`withEmbeddedWriteTransaction` here: on a transaction-scoped handle those
|
|
215
|
+
re-enter the caller's transaction under a savepoint, or with
|
|
216
|
+
`reuseUnsupportedNested` run on the caller's handle, and both fail open.
|
|
217
|
+
Reads work on any handle. Tests that isolate with
|
|
218
|
+
`createIsolatedTestDb*()` must hand the services `baseDb`, not the
|
|
219
|
+
transaction handle.
|
|
220
|
+
- **Tested on SQLite and PostgreSQL.** `HrService.identity` normalizes ids
|
|
221
|
+
only on PostgreSQL. On any other adapter (DuckDB included) nothing is
|
|
222
|
+
normalized, so callers must pass canonical lowercase UUIDs.
|
|
223
|
+
- **Not a permission system.** A qualification says what a person may do in
|
|
224
|
+
the real world. Who may use the software stays with `smrt-users`.
|
|
225
|
+
- **People are `smrt-profiles:Profile` ids** in fields named `profileId` or
|
|
226
|
+
`<role>ProfileId`, as cross-package refs. This is the id `smrt-timesheets`
|
|
227
|
+
holds.
|
|
228
|
+
|
|
229
|
+
## Extending Employment
|
|
230
|
+
|
|
231
|
+
Declare a same-named subclass over the `employments` table in the application,
|
|
232
|
+
restating the fields and the closed surface, and add application fields there
|
|
233
|
+
(teamworks-os keeps succession rank and acting-role fields this way). Only one
|
|
234
|
+
inheritance family may share a table; the deepest same-named subclass wins.
|
|
235
|
+
|
|
236
|
+
`Employment.save()` lets such a subclass write its own fields: without the
|
|
237
|
+
service's write capability a save is allowed only for an existing row whose
|
|
238
|
+
package-owned columns (`tenantId`, `profileId`, `userId`, `employerProfileId`,
|
|
239
|
+
`employeeNumber`, `workerType`, `position`, `status`) all still hold their
|
|
240
|
+
stored values. A save that changes one of them, and any insert, throws
|
|
241
|
+
`HR_WRITE_FORBIDDEN`; `delete()` always throws. Add a column to
|
|
242
|
+
`PACKAGE_OWNED_COLUMNS` in `src/employment/models.ts` when the package gains
|
|
243
|
+
one.
|
|
244
|
+
|
|
245
|
+
## Svelte UI
|
|
246
|
+
|
|
247
|
+
`@happyvertical/smrt-human-resources/svelte` ships four components, each
|
|
248
|
+
registered as a slot in `HUMAN_RESOURCES_UI_SLOTS` (`src/ui.ts`):
|
|
249
|
+
|
|
250
|
+
| Component | Slot | Shows |
|
|
251
|
+
|---|---|---|
|
|
252
|
+
| `EmployeeList` | `employee-list` | Name, employee number, position, worker type, status (plus "Last day {date}" when `endsOn` is set), start date |
|
|
253
|
+
| `EmployeeForm` | `employee-form` | Employee number, worker type, position, start date (new hire only), effective date (edit only), optional login |
|
|
254
|
+
| `PersonQualifications` | `person-qualifications` | One person's qualifications with their expiry state |
|
|
255
|
+
| `ExpiringQualificationsList` | `expiring-qualifications-list` | Qualifications expiring soon across people, soonest first |
|
|
256
|
+
|
|
257
|
+
- **Props-driven.** Components never load or save. The host calls the
|
|
258
|
+
services, adapts rows with the helpers in `src/svelte/types.ts`
|
|
259
|
+
(`toEmployeeView`, `toPersonQualificationView`,
|
|
260
|
+
`toExpiringQualificationView`) and handles `onselect` / `onsubmit`.
|
|
261
|
+
- **No clock in components.** `EmployeeList` shows the stored status badge
|
|
262
|
+
and, when the host passes `endsOn` (the latest term's `endedOn`, fourth
|
|
263
|
+
argument of `toEmployeeView`), the text "Last day {date}". It never compares
|
|
264
|
+
a date with today to decide a label.
|
|
265
|
+
- **Names come from the host.** HR holds no identity data, so every view
|
|
266
|
+
carries a `displayName` the host supplies.
|
|
267
|
+
- **`EmployeeForm` is hand-built** from smrt-ui form primitives (owner
|
|
268
|
+
decision); the package does not depend on `smrt-fields` or `smrt-users`.
|
|
269
|
+
- **`EmployeeForm` values are not a service input.** A hire returns
|
|
270
|
+
`startedOn`; an edit returns a required `effectiveOn` (prefilled from
|
|
271
|
+
`today`) that the host passes to `changePosition` / `changeWorkerType` /
|
|
272
|
+
`linkLogin` / `unlinkLogin` for each value that changed. A login link or
|
|
273
|
+
unlink takes effect at once whatever the date, so a future-dated edit that
|
|
274
|
+
also clears the login unlinks it immediately; a host that wants it kept
|
|
275
|
+
until then calls `unlinkLogin` on that day. The employee's
|
|
276
|
+
current login stays an option even when the host's `logins` omit it, so a
|
|
277
|
+
save never unlinks it by accident. Each validation message is tied to its
|
|
278
|
+
field with `aria-describedby`.
|
|
279
|
+
- **Day arithmetic is UTC** (`daysUntil`); never build a local-time `Date`
|
|
280
|
+
from a calendar date.
|
|
281
|
+
- **Expiry state is text plus a badge**, never colour alone. "Expiring soon"
|
|
282
|
+
is a valid qualification whose last day is within `expiringSoonDays`
|
|
283
|
+
(default 30), inclusive.
|
|
284
|
+
- **Strings** go through `useI18n()` and the catalog in `src/svelte/i18n.ts`,
|
|
285
|
+
keyed `human_resources.<component>.<key>`. Only smrt-ui primitives; no raw
|
|
286
|
+
`<input>` / `<button>` / `<select>`.
|
|
287
|
+
|
|
288
|
+
## Validation commands
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
pnpm --filter @happyvertical/smrt-human-resources build
|
|
292
|
+
pnpm --filter @happyvertical/smrt-human-resources test
|
|
293
|
+
pnpm --filter @happyvertical/smrt-human-resources typecheck
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Build before `test`: `src/__tests__/employment-extension.test.ts` reads
|
|
297
|
+
`dist/manifest.json`.
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright <2025> <Happy Vertical Corporation>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,292 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @happyvertical/smrt-human-resources
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
HR records for s-m-r-t: who works for an organization, as what, and
|
|
4
|
+
with what standing.
|
|
5
|
+
|
|
6
|
+
- **Employment**: one record per tenant and person, with an employee number,
|
|
7
|
+
worker type, position, status, dated terms and an append-only change history.
|
|
8
|
+
- **Qualifications**: a managed list of definitions (tickets, certifications,
|
|
9
|
+
authorizations, training, restrictions) and what each person holds, with
|
|
10
|
+
issue and expiry dates.
|
|
11
|
+
|
|
12
|
+
Pay belongs to payroll, time records to `smrt-timesheets`, logins and
|
|
13
|
+
permissions to `smrt-users`, and identity to `smrt-profiles`.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add @happyvertical/smrt-human-resources
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Access
|
|
22
|
+
|
|
23
|
+
Every model is tenant-scoped (required), sensitive, and publishes no generated
|
|
24
|
+
REST, MCP or CLI operation; the collection classes are closed the same way.
|
|
25
|
+
All writes go through `EmploymentService` and
|
|
26
|
+
`QualificationService`. The application authorizes the acting person first,
|
|
27
|
+
then constructs a service from that trusted actor:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
const actor = { tenantId, profileId: managerProfileId };
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`profileId` is a `smrt-profiles` Profile id and is recorded on every change.
|
|
34
|
+
Models reject `save()` and `delete()` outside the services (an application
|
|
35
|
+
subclass of `Employment` may save its own fields; see below).
|
|
36
|
+
|
|
37
|
+
## Dates
|
|
38
|
+
|
|
39
|
+
Dates are calendar dates written `YYYY-MM-DD`, with no timezone. Convert an
|
|
40
|
+
instant to your local date before asking "on what day". An employment term's
|
|
41
|
+
`endedOn` is the last day employed, inclusive.
|
|
42
|
+
|
|
43
|
+
## Employment
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { EmploymentService } from '@happyvertical/smrt-human-resources';
|
|
47
|
+
|
|
48
|
+
const hr = new EmploymentService(
|
|
49
|
+
db,
|
|
50
|
+
{ tenantId, profileId: managerProfileId },
|
|
51
|
+
{ onEvent: (event) => void notified.push(event.type) },
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const employment = await hr.hire({
|
|
55
|
+
profileId: workerProfileId,
|
|
56
|
+
employeeNumber: 'E-1042',
|
|
57
|
+
startedOn: '2026-01-05',
|
|
58
|
+
position: 'Welder',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const check = await hr.check(workerProfileId, '2026-02-01');
|
|
62
|
+
// { ok: true, employmentId: employment.id, onLeave: false }
|
|
63
|
+
|
|
64
|
+
await hr.end(employment.id as string, {
|
|
65
|
+
endedOn: '2026-03-31', // last day employed, inclusive
|
|
66
|
+
reason: 'Season over',
|
|
67
|
+
});
|
|
68
|
+
const after = await hr.check(workerProfileId, '2026-04-01');
|
|
69
|
+
// { ok: false, reason: 'ended' }
|
|
70
|
+
|
|
71
|
+
const rehired = await hr.rehire(employment.id as string, {
|
|
72
|
+
startedOn: '2026-06-01',
|
|
73
|
+
});
|
|
74
|
+
// rehired.id === employment.id; hr.terms(rehired.id) has two terms
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- An end may be recorded ahead of the last day (a notice period). The stored
|
|
78
|
+
status becomes `ended` at once and means "an end has been recorded", not
|
|
79
|
+
"no longer employed today": the person is employed up to and including
|
|
80
|
+
`endedOn`. Ask "on a date" with `check(profileId, on)`, `employedOn(on)`,
|
|
81
|
+
`asOf(employmentId, on)` or `findByUser(userId, on)`. The package has no
|
|
82
|
+
clock and never flips a status when a date passes.
|
|
83
|
+
- There is one employment per tenant and profile. A rehire adds a term to the
|
|
84
|
+
same record, so its id is stable for anything that references it.
|
|
85
|
+
- `check(profileId, on)` and `employedOn(on)` answer from the dated terms, not
|
|
86
|
+
from the stored status. Someone on leave is still employed; `check` reports
|
|
87
|
+
`onLeave`.
|
|
88
|
+
- `changePosition`, `changeWorkerType`, `placeOnLeave` and `returnFromLeave`
|
|
89
|
+
are dated changes; `history()` returns them and `asOf(employmentId, on)`
|
|
90
|
+
replays them for a past date.
|
|
91
|
+
- `linkLogin` and `unlinkLogin` are recorded in `history()` with their
|
|
92
|
+
`effectiveOn`, but they are not replayed and not scheduled: the login is
|
|
93
|
+
linked or cleared as soon as the call is made, whatever the date says, and
|
|
94
|
+
`findByUser(userId, on)` reads the login stored now. To unlink on a future
|
|
95
|
+
day, call `unlinkLogin` on that day.
|
|
96
|
+
- Position, worker-type and leave changes are accepted inside the latest
|
|
97
|
+
term. After an end is recorded that is the final term, up to and including
|
|
98
|
+
its `endedOn`: the change is recorded and raises its usual event, the stored
|
|
99
|
+
status stays `ended`, and leave is validated against the replayed history
|
|
100
|
+
(`check().onLeave` and `asOf` answer it). A date after the last day is
|
|
101
|
+
rejected with `HR_STATUS_TRANSITION` (rehire first); a date before the term
|
|
102
|
+
started with `HR_INVALID`.
|
|
103
|
+
- A term holds its own changes: `end` rejects an `endedOn` earlier than a
|
|
104
|
+
position, worker-type or leave change already recorded in the term, and
|
|
105
|
+
`rehire` rejects a start before one.
|
|
106
|
+
- The login link (`userId`) is optional, and a login belongs to one
|
|
107
|
+
employment at a time. It stays with that employment, ended or not, until
|
|
108
|
+
`unlinkLogin` clears it there: `hire`, `rehire` and `linkLogin` reject
|
|
109
|
+
(`HR_INVALID`) a login stored on any other employment, so a worker on
|
|
110
|
+
notice keeps their login and nobody else can take it. To move a login,
|
|
111
|
+
unlink it first (`unlinkLogin` works on an ended employment).
|
|
112
|
+
`findByUser(userId)` returns only an employment whose
|
|
113
|
+
stored status is not `ended`, so it stops resolving as soon as an end is
|
|
114
|
+
recorded. `findByUser(userId, on)` answers from the terms instead: the
|
|
115
|
+
linked employment with a term covering `on`, whatever its status. A host
|
|
116
|
+
that records end dates ahead of time should pass the date, or a worker on
|
|
117
|
+
notice is locked out.
|
|
118
|
+
|
|
119
|
+
## Qualifications
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { QualificationService } from '@happyvertical/smrt-human-resources';
|
|
123
|
+
|
|
124
|
+
const qualifications = new QualificationService(db, actor, {
|
|
125
|
+
onEvent: (event) => void events.push(event), // after commit; a throwing handler never rolls back
|
|
126
|
+
});
|
|
127
|
+
const firstAidTicket = await qualifications.define({
|
|
128
|
+
key: 'first-aid',
|
|
129
|
+
name: 'First aid',
|
|
130
|
+
kind: 'certification',
|
|
131
|
+
expires: true,
|
|
132
|
+
validityMonths: 36,
|
|
133
|
+
});
|
|
134
|
+
const held = await qualifications.grant({
|
|
135
|
+
qualificationId: firstAidTicket.id as string,
|
|
136
|
+
profileId: workerProfileId,
|
|
137
|
+
issuedOn: '2026-03-01', // expiresOn defaults to 2029-03-01
|
|
138
|
+
certificateNumber: 'FA-20931',
|
|
139
|
+
verified: true,
|
|
140
|
+
});
|
|
141
|
+
const gate = await qualifications.check(
|
|
142
|
+
workerProfileId,
|
|
143
|
+
firstAidTicket.id as string,
|
|
144
|
+
'2029-03-02',
|
|
145
|
+
); // { ok: false, reason: 'expired' }
|
|
146
|
+
const renewal = await qualifications.renew(held.id as string, {
|
|
147
|
+
issuedOn: '2029-02-15',
|
|
148
|
+
}); // a new row; `held` still answers for 2026-03-01..2029-03-01
|
|
149
|
+
const after = await qualifications.check(
|
|
150
|
+
workerProfileId,
|
|
151
|
+
firstAidTicket.id as string,
|
|
152
|
+
'2029-03-02',
|
|
153
|
+
); // { ok: true, heldQualificationId: renewal.id, expiresOn: '2032-02-15' }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- `check()` is the gate an application calls before allowing work. When it
|
|
157
|
+
fails it says why: `not-held`, `not-yet-issued`, `expired`, `suspended` or
|
|
158
|
+
`revoked`.
|
|
159
|
+
- Expiry is computed from the dates, so a check needs no sweep.
|
|
160
|
+
- Standing is decided by date everywhere. `suspend`, `reinstate` and `revoke`
|
|
161
|
+
take an `effectiveOn` that may be ahead; the stored `status` changes at
|
|
162
|
+
once and only says what was last recorded. `check`, `holders`,
|
|
163
|
+
`listForProfile` and `expiringWithin` all answer from the dated history,
|
|
164
|
+
and so do the rules for the next change: `suspend` needs the qualification
|
|
165
|
+
to be neither suspended nor revoked on `effectiveOn`, `reinstate` needs it
|
|
166
|
+
suspended on that day, and neither may be dated before the latest
|
|
167
|
+
suspension or reinstatement.
|
|
168
|
+
- `revoke` is accepted for any date from the first issue date unless the
|
|
169
|
+
qualification is already revoked on or before it. A revocation recorded for
|
|
170
|
+
a later date (scheduled, or the end of an employment whose last day is
|
|
171
|
+
ahead) freezes nothing: until it takes effect the qualification can still
|
|
172
|
+
be suspended, reinstated, renewed, or revoked from an earlier day, and the
|
|
173
|
+
earliest revocation is the one in force. A stored `revoked` is final, so
|
|
174
|
+
those later changes are history rows only and the status keeps reading
|
|
175
|
+
`revoked`.
|
|
176
|
+
- A person has one live grant of a qualification at a time. `grant` is
|
|
177
|
+
refused (`HR_ALREADY_HELD`) unless everything they already hold of it is
|
|
178
|
+
revoked effective on or before the new `issuedOn`: a revocation scheduled
|
|
179
|
+
for 2027 does not allow a fresh grant dated 2026 (renew the existing one
|
|
180
|
+
instead), and a lapsed ticket is renewed, not granted again.
|
|
181
|
+
- A definition's `scope` says who a held qualification belongs to. A `person`
|
|
182
|
+
qualification survives a change of employment. An `employment` one is
|
|
183
|
+
revoked, with reason `employment-ended`, when that employment ends: it is
|
|
184
|
+
still good on the last day employed and revoked from the day after. That
|
|
185
|
+
cutoff always wins: it also applies to a qualification whose own revocation
|
|
186
|
+
was scheduled for a later date, and to one granted or renewed after the end
|
|
187
|
+
was recorded (during notice, or backdated), which is stored `revoked` at
|
|
188
|
+
once and passes `check` only through the last day employed. The end is
|
|
189
|
+
decided per renewal chain: the latest row gets one revocation and one
|
|
190
|
+
`held-qualification.revoked` event, the rows it renewed are cut off through
|
|
191
|
+
the chain, and a chain already revoked on or before the cutoff gets nothing.
|
|
192
|
+
- A renewal is a new row, and the chain shares its standing. Suspend, reinstate
|
|
193
|
+
or revoke the latest row: while it is suspended or revoked, the rows it
|
|
194
|
+
renewed do not pass `check` either, and acting on a row that was already
|
|
195
|
+
renewed is rejected (`HR_INVALID`). The change may take effect on any date
|
|
196
|
+
from the chain's first issue date, so a ticket in force today can still be
|
|
197
|
+
suspended or revoked after its renewal was recorded ahead of time: act on
|
|
198
|
+
the renewal with today's date, and the renewal does not restore standing
|
|
199
|
+
when its own issue date arrives.
|
|
200
|
+
- A renewal cannot end before what it renews (`HR_INVALID`): the renewed row
|
|
201
|
+
keeps answering for its own period, so a shorter renewal would lapse while
|
|
202
|
+
`check` still passed. To replace a qualification with a shorter one, revoke
|
|
203
|
+
it from that day and grant a new one.
|
|
204
|
+
- A renewal cannot be issued before the latest suspension or reinstatement
|
|
205
|
+
of the row it renews (`HR_INVALID`), nor while that row is suspended on the
|
|
206
|
+
issue date (`HR_STATUS_TRANSITION`: reinstate it first), nor when the row
|
|
207
|
+
is revoked on or before the issue date (`HR_INVALID`; grant a new one dated
|
|
208
|
+
on or after the revocation). A row whose revocation takes effect later is
|
|
209
|
+
still in force and can be renewed, so a ticket that lapses during a notice
|
|
210
|
+
period is not stuck: the renewal carries the cutoff forward (the pending
|
|
211
|
+
revocation, or the day after the last day employed if that is earlier), is
|
|
212
|
+
stored `revoked`, and raises `held-qualification.renewed` and `.revoked`.
|
|
213
|
+
- `verify(heldQualificationId, { documentAssetId })` records the actor as
|
|
214
|
+
having verified the document, now, and optionally attaches or replaces it.
|
|
215
|
+
It changes no status and works whatever the standing, including on a row
|
|
216
|
+
stored `revoked`.
|
|
217
|
+
- `holders(qualificationId, on, { employedOnly: true })` lists, for example,
|
|
218
|
+
the first aiders currently employed. `expiringWithin(days, today)` lists
|
|
219
|
+
what is about to lapse: qualifications that pass `check` on `today` (issued,
|
|
220
|
+
not suspended or revoked on that day, not already renewed) and whose last
|
|
221
|
+
day is within the window; `{ employedOnly: true }` keeps only people
|
|
222
|
+
employed on `today`.
|
|
223
|
+
- A blank id is rejected with `HR_INVALID`; `check` answers `not-held`.
|
|
224
|
+
- `seed(SUGGESTED_QUALIFICATIONS)` creates a few common definitions; it never
|
|
225
|
+
changes ones that already exist.
|
|
226
|
+
- A qualification is not a permission. Who may use the software stays with
|
|
227
|
+
`smrt-users`; an application may require both.
|
|
228
|
+
|
|
229
|
+
## Events and expiry
|
|
230
|
+
|
|
231
|
+
Both services take an `onEvent` hook, called after the change commits:
|
|
232
|
+
`employment.hired`, `.ended`, `.rehired`, `.position-changed`,
|
|
233
|
+
`.worker-type-changed`, `.leave-started`, `.leave-ended`, and
|
|
234
|
+
`held-qualification.granted`, `.renewed`, `.suspended`, `.reinstated`,
|
|
235
|
+
`.revoked`, `.expired`. Delivery is best-effort; bridge it to your own event
|
|
236
|
+
bus if you need more.
|
|
237
|
+
|
|
238
|
+
Construct the services with your root database handle. Each mutation begins
|
|
239
|
+
its own transaction (`beginTransaction()`), commits it, and then delivers its
|
|
240
|
+
events. A service built on a handle that is already inside a transaction (the
|
|
241
|
+
`tx` passed to `db.transaction()`, a savepoint scope, or a
|
|
242
|
+
`beginTransaction()` handle) could only deliver before the outer commit, so
|
|
243
|
+
it refuses every mutation with `HR_TRANSACTION_UNSUPPORTED` and writes
|
|
244
|
+
nothing; reads still work. Such a handle either has no `beginTransaction` or
|
|
245
|
+
refuses it with the SDK's `NestedTransactionError` (the native-capabilities
|
|
246
|
+
SQLite adapter, used for `vector` / `notifications`); the services never open
|
|
247
|
+
a nested transaction. An HR mutation therefore cannot be made atomic with
|
|
248
|
+
other work in a caller's transaction.
|
|
249
|
+
|
|
250
|
+
The package is tested on SQLite and PostgreSQL. Ids are normalized (to
|
|
251
|
+
canonical lowercase UUIDs) only on PostgreSQL; on any other adapter, DuckDB
|
|
252
|
+
included, pass canonical lowercase UUIDs yourself.
|
|
253
|
+
|
|
254
|
+
Nothing in the package runs on a schedule. Call
|
|
255
|
+
`qualifications.sweepExpired(today)` from your own scheduler to record lapsed
|
|
256
|
+
qualifications and receive `held-qualification.expired`. It decides by date,
|
|
257
|
+
so a ticket that runs out while suspended, or during a notice period, is
|
|
258
|
+
recorded too. A scheduled sweep should pass `{ since }`, a day on or before
|
|
259
|
+
the last day its previous run covered, so it does not re-read all history.
|
|
260
|
+
Reminders are the application's business.
|
|
261
|
+
|
|
262
|
+
## Extending Employment
|
|
263
|
+
|
|
264
|
+
Add application fields with a same-named subclass over the `employments`
|
|
265
|
+
table. Restate the closed surface on the subclass: exposure is not inherited.
|
|
266
|
+
Read your subclass through your own collection and `save()` your own fields on
|
|
267
|
+
a row the service created. The columns this package owns (`tenantId`,
|
|
268
|
+
`profileId`, `userId`, `employerProfileId`, `employeeNumber`, `workerType`,
|
|
269
|
+
`position`, `status`) still change only through `EmploymentService`: a save
|
|
270
|
+
that changes one of them, or that would insert a row, throws
|
|
271
|
+
`HR_WRITE_FORBIDDEN`.
|
|
272
|
+
|
|
273
|
+
## Svelte components
|
|
274
|
+
|
|
275
|
+
`@happyvertical/smrt-human-resources/svelte` exports props-driven components:
|
|
276
|
+
an employee list, an employee form, a person's qualifications with their
|
|
277
|
+
expiry state, and an expiring-soon list. The host loads records through the
|
|
278
|
+
services and passes plain view objects.
|
|
279
|
+
|
|
280
|
+
`EmployeeList` shows the stored status. Pass the latest term's `endedOn` as
|
|
281
|
+
`endsOn` (the fourth argument of `toEmployeeView`) and the row also reads
|
|
282
|
+
"Last day 2026-12-31", so a worker on notice is not mistaken for one who has
|
|
283
|
+
left. The component never compares the date with today.
|
|
284
|
+
|
|
285
|
+
`EmployeeForm` never saves. For a new hire it returns `startedOn`; for an edit
|
|
286
|
+
it returns a required `effectiveOn` (prefilled from the `today` prop), which
|
|
287
|
+
the host passes to `changePosition`, `changeWorkerType`, `linkLogin` or
|
|
288
|
+
`unlinkLogin` for each value that changed. Position and worker-type changes
|
|
289
|
+
take effect on that date; a login link or unlink takes effect at once, so a
|
|
290
|
+
future-dated edit that also clears the login unlinks it immediately. A host
|
|
291
|
+
that wants the login kept until the date should hold the unlink back and
|
|
292
|
+
call `unlinkLogin` on that day.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"__smrt-register__.d.ts","sourceRoot":"","sources":["../src/__smrt-register__.ts"],"names":[],"mappings":""}
|
package/dist/dates.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar-date helpers. HR dates are `YYYY-MM-DD` text, so they compare
|
|
3
|
+
* correctly as strings and carry no timezone; the application converts an
|
|
4
|
+
* instant to its own local date before asking.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
import { type IsoDate } from './types.js';
|
|
9
|
+
/** Whether `value` is a real calendar date written `YYYY-MM-DD`. */
|
|
10
|
+
export declare function isIsoDate(value: unknown): value is IsoDate;
|
|
11
|
+
/** Throw unless `value` is a real calendar date written `YYYY-MM-DD`. */
|
|
12
|
+
export declare function assertIsoDate(fieldName: string, value: unknown): IsoDate;
|
|
13
|
+
/** The calendar date `days` after `date` (negative moves back). */
|
|
14
|
+
export declare function addDays(date: IsoDate, days: number): IsoDate;
|
|
15
|
+
/**
|
|
16
|
+
* The calendar date `months` after `date`, clamped to the end of the target
|
|
17
|
+
* month (31 January + 1 month is the last day of February).
|
|
18
|
+
*/
|
|
19
|
+
export declare function addMonths(date: IsoDate, months: number): IsoDate;
|
|
20
|
+
/**
|
|
21
|
+
* Whether `on` falls inside the inclusive range `[start, end]`. A null `end`
|
|
22
|
+
* is open-ended.
|
|
23
|
+
*/
|
|
24
|
+
export declare function dateWithin(on: IsoDate, start: IsoDate, end: IsoDate | null): boolean;
|
|
25
|
+
//# sourceMappingURL=dates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dates.d.ts","sourceRoot":"","sources":["../src/dates.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAInD,oEAAoE;AACpE,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO,CAW1D;AAED,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAOxE;AAED,mEAAmE;AACnE,wBAAgB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAK5D;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAQhE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,EAAE,EAAE,OAAO,EACX,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,OAAO,GAAG,IAAI,GAClB,OAAO,CAET"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/employment/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC"}
|