@kernhq/module-hr 0.13.2 → 0.14.1
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/dist/contract/attendance.d.ts +35 -0
- package/dist/contract/attendance.d.ts.map +1 -1
- package/dist/contract/attendance.js +25 -1
- package/dist/contract/attendance.js.map +1 -1
- package/dist/contract/capabilities.d.ts.map +1 -1
- package/dist/contract/capabilities.js +10 -0
- package/dist/contract/capabilities.js.map +1 -1
- package/dist/contract/events.d.ts +17 -16
- package/dist/contract/events.d.ts.map +1 -1
- package/dist/contract/events.js +23 -16
- package/dist/contract/events.js.map +1 -1
- package/dist/contract/models.d.ts +1 -0
- package/dist/contract/models.d.ts.map +1 -1
- package/dist/contract/models.js +11 -0
- package/dist/contract/models.js.map +1 -1
- package/dist/contract/router.d.ts +12 -0
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/contract/router.js +11 -3
- package/dist/contract/router.js.map +1 -1
- package/dist/contract/settings.d.ts +0 -1
- package/dist/contract/settings.d.ts.map +1 -1
- package/dist/contract/settings.js +11 -5
- package/dist/contract/settings.js.map +1 -1
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/jobs.d.ts.map +1 -1
- package/dist/server/jobs.js +13 -5
- package/dist/server/jobs.js.map +1 -1
- package/dist/server/router.d.ts +12 -0
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/router.js +99 -16
- package/dist/server/router.js.map +1 -1
- package/dist/server/services/access.d.ts +176 -0
- package/dist/server/services/access.d.ts.map +1 -0
- package/dist/server/services/access.js +260 -0
- package/dist/server/services/access.js.map +1 -0
- package/dist/server/services/people.d.ts +1 -0
- package/dist/server/services/people.d.ts.map +1 -1
- package/dist/server/services/people.js +3 -0
- package/dist/server/services/people.js.map +1 -1
- package/package.json +1 -1
- package/src/client/components/DayDetail.svelte +32 -14
- package/src/client/components/PersonPanel.svelte +66 -5
- package/src/client/components/redaction.ts +39 -0
- package/src/client/messages.ts +241 -22
- package/src/client/pages/DirectoryPage.svelte +48 -1
- package/src/client/permissions.ts +6 -8
- package/src/client/settings/AccrualSettings.svelte +596 -57
- package/src/client/settings/GeneralSettings.svelte +12 -9
- package/src/contract/attendance.ts +27 -1
- package/src/contract/capabilities.ts +10 -0
- package/src/contract/events.ts +23 -19
- package/src/contract/models.ts +11 -0
- package/src/contract/router.ts +11 -3
- package/src/contract/settings.ts +11 -5
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
EmptyState,
|
|
8
8
|
Field,
|
|
9
9
|
formatDate,
|
|
10
|
+
Icon,
|
|
10
11
|
Input,
|
|
11
12
|
navigation,
|
|
12
13
|
RightPanel,
|
|
@@ -21,6 +22,7 @@ import { hrKeys, isoDate } from '../query.js'
|
|
|
21
22
|
import PersonDocumentsSection from './PersonDocumentsSection.svelte'
|
|
22
23
|
import PersonJobSection from './PersonJobSection.svelte'
|
|
23
24
|
import PersonSensitiveSection from './PersonSensitiveSection.svelte'
|
|
25
|
+
import { personnelWithheld } from './redaction.js'
|
|
24
26
|
import { explainRefusal } from './refusal.js'
|
|
25
27
|
|
|
26
28
|
/**
|
|
@@ -188,8 +190,30 @@ function officeTime(timezone: string): string {
|
|
|
188
190
|
|
|
189
191
|
const canManage = $derived(canHr('personManage'))
|
|
190
192
|
const left = $derived(person?.status === 'terminated')
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Whether the server withheld this person's personnel fields from this reader.
|
|
196
|
+
*
|
|
197
|
+
* The four it nulls arrive as nulls, so an empty phone row on this panel says "no phone number" —
|
|
198
|
+
* a different and wrong fact. The record carries the answer (`personnelHidden`, set where the
|
|
199
|
+
* nulling happens), so this is a read rather than an inference; the panel then explains itself once
|
|
200
|
+
* below the list rather than once per field.
|
|
201
|
+
*/
|
|
202
|
+
const withheld = $derived(person ? personnelWithheld(person) : false)
|
|
203
|
+
/**
|
|
204
|
+
* And only where something on *this* panel is actually blank because of it. The two are the same
|
|
205
|
+
* thing while the client and the server agree about the reader's keys; tying the sentence to what
|
|
206
|
+
* is on screen means a disagreement leaves a stale sentence over nothing rather than over a value
|
|
207
|
+
* the reader can plainly see.
|
|
208
|
+
*/
|
|
209
|
+
const hiddenHere = $derived(withheld && (!person?.phone || !person?.hiredOn))
|
|
191
210
|
</script>
|
|
192
211
|
|
|
212
|
+
{#snippet hiddenValue()}
|
|
213
|
+
<!-- A value in its own right, not an absence: the row stays, and says what it is. -->
|
|
214
|
+
<span class="withheld"><Icon name="eye-off" size={12} strokeWidth={1.8} />{t('person_hidden')}</span>
|
|
215
|
+
{/snippet}
|
|
216
|
+
|
|
193
217
|
{#snippet footerActions()}
|
|
194
218
|
<div class="actions">
|
|
195
219
|
<!-- Disabled with the reason beside it, not hidden: the button was there a moment ago, and a
|
|
@@ -225,7 +249,7 @@ const left = $derived(person?.status === 'terminated')
|
|
|
225
249
|
</div>
|
|
226
250
|
</div>
|
|
227
251
|
|
|
228
|
-
<dl>
|
|
252
|
+
<dl class:tight={hiddenHere}>
|
|
229
253
|
{#if resolution?.primaryOfficeName}
|
|
230
254
|
<dt>{t('office')}</dt>
|
|
231
255
|
<dd>{resolution.primaryOfficeName}</dd>
|
|
@@ -241,13 +265,22 @@ const left = $derived(person?.status === 'terminated')
|
|
|
241
265
|
<dt>{t('employee_no')}</dt>
|
|
242
266
|
<dd>{person.employeeNo}</dd>
|
|
243
267
|
{/if}
|
|
244
|
-
|
|
268
|
+
<!--
|
|
269
|
+
Two of the four fields the server redacts. A withheld one keeps its row and is marked, so
|
|
270
|
+
that "we are not showing you this" cannot be read as "there is nothing here"; one that is
|
|
271
|
+
merely empty keeps the behaviour it had, and drops out of the list.
|
|
272
|
+
-->
|
|
273
|
+
{#if person.hiredOn || withheld}
|
|
245
274
|
<dt>{t('started')}</dt>
|
|
246
|
-
<dd>
|
|
275
|
+
<dd>
|
|
276
|
+
{#if person.hiredOn}{formatDate(`${person.hiredOn}T00:00:00`)}{:else}{@render hiddenValue()}{/if}
|
|
277
|
+
</dd>
|
|
247
278
|
{/if}
|
|
248
|
-
{#if person.phone}
|
|
279
|
+
{#if person.phone || withheld}
|
|
249
280
|
<dt>{t('phone')}</dt>
|
|
250
|
-
<dd>
|
|
281
|
+
<dd>
|
|
282
|
+
{#if person.phone}{person.phone}{:else}{@render hiddenValue()}{/if}
|
|
283
|
+
</dd>
|
|
251
284
|
{/if}
|
|
252
285
|
{#if resolution?.orgUnitPath}
|
|
253
286
|
<dt>{t('department')}</dt>
|
|
@@ -259,6 +292,14 @@ const left = $derived(person?.status === 'terminated')
|
|
|
259
292
|
{/if}
|
|
260
293
|
</dl>
|
|
261
294
|
|
|
295
|
+
<!--
|
|
296
|
+
Once for the panel, never once per field. The same sentence beside every marked row is a
|
|
297
|
+
lecture; the marks say *which* fields, and this says why, once, under all of them.
|
|
298
|
+
-->
|
|
299
|
+
{#if hiddenHere}
|
|
300
|
+
<p class="hint">{t('person_hidden_hint')}</p>
|
|
301
|
+
{/if}
|
|
302
|
+
|
|
262
303
|
<Badge tone={person.status === 'active' ? 'active' : person.status === 'on_leave' ? 'upcoming' : 'grey'}
|
|
263
304
|
>{person.status === 'active'
|
|
264
305
|
? t('status_active')
|
|
@@ -389,6 +430,11 @@ dl {
|
|
|
389
430
|
gap: 8px 16px;
|
|
390
431
|
margin: 0 0 16px;
|
|
391
432
|
}
|
|
433
|
+
/* The sentence below the list belongs to it, so the list gives up its own gap to keep them one
|
|
434
|
+
block rather than two things that happen to follow each other. */
|
|
435
|
+
dl.tight {
|
|
436
|
+
margin-block-end: 8px;
|
|
437
|
+
}
|
|
392
438
|
dt {
|
|
393
439
|
color: var(--kern-ink-500);
|
|
394
440
|
font-size: 12px;
|
|
@@ -396,6 +442,21 @@ dt {
|
|
|
396
442
|
dd {
|
|
397
443
|
margin: 0;
|
|
398
444
|
}
|
|
445
|
+
/* A colour and a smaller size, never opacity — a faded value is unreadable whatever token it
|
|
446
|
+
names, and this one has to be read to be believed. */
|
|
447
|
+
.withheld {
|
|
448
|
+
display: inline-flex;
|
|
449
|
+
align-items: center;
|
|
450
|
+
gap: 5px;
|
|
451
|
+
color: var(--kern-ink-500);
|
|
452
|
+
font-size: 12px;
|
|
453
|
+
}
|
|
454
|
+
.hint {
|
|
455
|
+
margin: 0 0 16px;
|
|
456
|
+
font-size: 12px;
|
|
457
|
+
line-height: 1.45;
|
|
458
|
+
color: var(--kern-ink-500);
|
|
459
|
+
}
|
|
399
460
|
.actions {
|
|
400
461
|
display: flex;
|
|
401
462
|
gap: 8px;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the personnel fields on a card were withheld, or are genuinely empty.
|
|
3
|
+
*
|
|
4
|
+
* `HrAccessService` nulls four of them — personal email, phone, hire date, termination date — for
|
|
5
|
+
* anybody outside the reader's record scope, and nulls them **in place**: a redacted card parses as
|
|
6
|
+
* an ordinary `Person`, so a blank phone number would otherwise arrive at a screen meaning either
|
|
7
|
+
* "we are not showing you this" or "this person never gave us one". Those are two different facts
|
|
8
|
+
* about a colleague, and printing the second when the first is true is the defect this exists to
|
|
9
|
+
* stop.
|
|
10
|
+
*
|
|
11
|
+
* **The record says which, and this only reads it.** `Person.personnelHidden` is set in `forViewer`,
|
|
12
|
+
* the single place that does the nulling — so the answer comes from the code that made the decision
|
|
13
|
+
* rather than from a client re-deriving it.
|
|
14
|
+
*
|
|
15
|
+
* That matters because the client cannot re-derive it. This helper used to infer the answer from
|
|
16
|
+
* the reader's own permission keys, which works for the two extremes — somebody holding
|
|
17
|
+
* `hr.person.view_all` or `hr.person.manage` has nothing hidden, somebody holding none of the
|
|
18
|
+
* widening keys has everything but their own record hidden — and fails in the middle. A line
|
|
19
|
+
* manager or a country HR person holds `view_team` or `view_office`, and **which** people those
|
|
20
|
+
* cover is resolved on the server from the org chart: headship of a unit over an ltree subtree,
|
|
21
|
+
* headship of an office, `manager_person_id`, all as of today, across rows the directory never
|
|
22
|
+
* fetches. Inferring it would have meant a second implementation of that resolution, and "all four
|
|
23
|
+
* fields are null" is not evidence — a person really can have no personal email, no phone and no
|
|
24
|
+
* recorded hire date.
|
|
25
|
+
*/
|
|
26
|
+
export interface PersonnelSubject {
|
|
27
|
+
/**
|
|
28
|
+
* True when the server withheld the personnel fields on this record.
|
|
29
|
+
*
|
|
30
|
+
* Optional so a caller can pass a row that predates the field — `dev:mock` fixtures, an older
|
|
31
|
+
* cached payload — and get `false` rather than a crash. Absent means "not withheld", which is the
|
|
32
|
+
* safe direction: a screen that fails to mark a hidden field shows a blank, while one that marks
|
|
33
|
+
* an empty field asserts something false about a colleague.
|
|
34
|
+
*/
|
|
35
|
+
personnelHidden?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Were this card's personnel fields withheld from the reader? */
|
|
39
|
+
export const personnelWithheld = (person: PersonnelSubject): boolean => person.personnelHidden === true
|