@kernhq/module-hr 0.14.0 → 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/package.json
CHANGED
|
@@ -22,7 +22,7 @@ import { hrKeys, isoDate } from '../query.js'
|
|
|
22
22
|
import PersonDocumentsSection from './PersonDocumentsSection.svelte'
|
|
23
23
|
import PersonJobSection from './PersonJobSection.svelte'
|
|
24
24
|
import PersonSensitiveSection from './PersonSensitiveSection.svelte'
|
|
25
|
-
import {
|
|
25
|
+
import { personnelWithheld } from './redaction.js'
|
|
26
26
|
import { explainRefusal } from './refusal.js'
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -195,11 +195,11 @@ const left = $derived(person?.status === 'terminated')
|
|
|
195
195
|
* Whether the server withheld this person's personnel fields from this reader.
|
|
196
196
|
*
|
|
197
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.
|
|
199
|
-
*
|
|
200
|
-
* list rather than once per field.
|
|
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
201
|
*/
|
|
202
|
-
const withheld = $derived(person ?
|
|
202
|
+
const withheld = $derived(person ? personnelWithheld(person) : false)
|
|
203
203
|
/**
|
|
204
204
|
* And only where something on *this* panel is actually blank because of it. The two are the same
|
|
205
205
|
* thing while the client and the server agree about the reader's keys; tying the sentence to what
|
|
@@ -1,51 +1,39 @@
|
|
|
1
|
-
import { session } from '@kernhq/ui'
|
|
2
|
-
import { canHr, canSeeFullRecords } from '../permissions.js'
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
2
|
* Whether the personnel fields on a card were withheld, or are genuinely empty.
|
|
6
3
|
*
|
|
7
4
|
* `HrAccessService` nulls four of them — personal email, phone, hire date, termination date — for
|
|
8
5
|
* anybody outside the reader's record scope, and nulls them **in place**: a redacted card parses as
|
|
9
|
-
* an ordinary `Person`, so a blank phone number
|
|
10
|
-
* showing you this" or "this person never gave us one". Those are two different facts
|
|
11
|
-
* colleague, and printing the second when the first is true is the defect this exists to
|
|
12
|
-
*
|
|
13
|
-
* Nothing on the record says which it is, so this reconstructs what it can from the reader's own
|
|
14
|
-
* keys — and answers `unknown` rather than guessing where it cannot:
|
|
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.
|
|
15
10
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* fields they could not see. Everybody reads their own record besides, holding nothing at all.
|
|
20
|
-
* - `withheld` — the reader holds none of the three widening keys, so every card but their own came
|
|
21
|
-
* back with all four nulled. Certain, and the common case: `hr.person.view` is a member default
|
|
22
|
-
* and most colleagues hold nothing above it.
|
|
23
|
-
* - `unknown` — the reader holds `view_team` or `view_office`. **Which** people those cover is
|
|
24
|
-
* resolved on the server from the org chart — headship of a unit or an office, direct reports, as
|
|
25
|
-
* of today — and none of that reaches the client. A screen renders the field exactly as it
|
|
26
|
-
* arrived rather than labelling a genuinely empty phone number "Hidden".
|
|
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.
|
|
27
14
|
*
|
|
28
|
-
* That
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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.
|
|
32
25
|
*/
|
|
33
|
-
export type PersonnelVisibility = 'full' | 'withheld' | 'unknown'
|
|
34
|
-
|
|
35
|
-
/** Enough of a person to answer for — a directory row, a whole record, an office roster entry. */
|
|
36
26
|
export interface PersonnelSubject {
|
|
37
|
-
/**
|
|
38
|
-
|
|
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
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (canHr('personViewAll') || canHr('personManage')) return 'full'
|
|
44
|
-
// Identity, never a grant. Plenty of employees have no account, so a null on either side is
|
|
45
|
-
// nobody's match rather than everybody's.
|
|
46
|
-
const userId = session.user?.id
|
|
47
|
-
if (userId && person.userId === userId) return 'full'
|
|
48
|
-
// `view_all` is answered above, so what is left of that union is team-or-office scope: a set of
|
|
49
|
-
// people only the server can name.
|
|
50
|
-
return canSeeFullRecords() ? 'unknown' : 'withheld'
|
|
51
|
-
}
|
|
38
|
+
/** Were this card's personnel fields withheld from the reader? */
|
|
39
|
+
export const personnelWithheld = (person: PersonnelSubject): boolean => person.personnelHidden === true
|
|
@@ -25,7 +25,7 @@ import { getHrApi } from '../api-instance.js'
|
|
|
25
25
|
import DecisionDialog from '../components/DecisionDialog.svelte'
|
|
26
26
|
import PersonFormDialog from '../components/PersonFormDialog.svelte'
|
|
27
27
|
import PersonPanel from '../components/PersonPanel.svelte'
|
|
28
|
-
import {
|
|
28
|
+
import { personnelWithheld } from '../components/redaction.js'
|
|
29
29
|
import { t } from '../i18n.js'
|
|
30
30
|
import type { ApprovalRequest } from '../index.js'
|
|
31
31
|
import { canHr, HR_CAPABILITIES } from '../permissions.js'
|
|
@@ -434,12 +434,15 @@ const started = (iso: string | null) =>
|
|
|
434
434
|
* and it arrives here as a null like any other — so the em dash this column drew for it said
|
|
435
435
|
* "never started", about everybody in the company, to every colleague without a widening key.
|
|
436
436
|
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
437
|
+
* The record says which it is — `personnelHidden`, set at the one place that does the nulling — so
|
|
438
|
+
* a row is marked only when the server actually withheld it, and a genuinely empty hire date keeps
|
|
439
|
+
* its dash. Marked once, under the table, rather than per row.
|
|
440
|
+
*
|
|
441
|
+
* The `!person.hiredOn` half stays deliberately: the mark only ever goes over a value that is
|
|
442
|
+
* absent, so a stale or unrecognised payload can never paint "Hidden" across data on screen.
|
|
440
443
|
*/
|
|
441
|
-
const startWithheld = (person: {
|
|
442
|
-
!person.hiredOn &&
|
|
444
|
+
const startWithheld = (person: { hiredOn: string | null; personnelHidden?: boolean }) =>
|
|
445
|
+
!person.hiredOn && personnelWithheld(person)
|
|
443
446
|
|
|
444
447
|
/** Whether anything on the page is actually marked — the sentence must not outlive the marks. */
|
|
445
448
|
const anyWithheld = $derived(people.some(startWithheld))
|
|
@@ -22,19 +22,9 @@ export function canHr(permission: HrPermission): boolean {
|
|
|
22
22
|
return session.can(HR_PERMISSIONS[permission])
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* personnel record, and the server nulls all four for anybody outside the reader's scope.
|
|
32
|
-
*
|
|
33
|
-
* The three do not imply one another — a country HR manager must not silently become a global one —
|
|
34
|
-
* so a screen asks the union once, here. It is only a hint: which *people* fall inside a team or an
|
|
35
|
-
* office is resolved on the server from the org chart, so a screen may not conclude from `true`
|
|
36
|
-
* that a particular person's record is readable. Render what came back; use this to decide whether
|
|
37
|
-
* a "personal details" section is worth offering at all.
|
|
38
|
-
*/
|
|
39
|
-
export const canSeeFullRecords = (): boolean =>
|
|
40
|
-
canHr('personViewTeam') || canHr('personViewOffice') || canHr('personViewAll')
|
|
25
|
+
// No union helper over the three person-visibility keys. There has been one here twice —
|
|
26
|
+
// `canSeeOthers`, then `canSeeFullRecords` — and both were dead the whole time, because the question
|
|
27
|
+
// a screen actually has is "was *this* record withheld", and the answer to that is on the record:
|
|
28
|
+
// `Person.personnelHidden`, set by the server at the one place that does the nulling. A client-side
|
|
29
|
+
// union can only say "the reader holds one of three keys somewhere", which is never enough to decide
|
|
30
|
+
// what to draw over one person's phone number. See `components/redaction.ts`.
|