@kernhq/module-hr 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/contract/attendance.d.ts +35 -0
  2. package/dist/contract/attendance.d.ts.map +1 -1
  3. package/dist/contract/attendance.js +25 -1
  4. package/dist/contract/attendance.js.map +1 -1
  5. package/dist/contract/capabilities.d.ts.map +1 -1
  6. package/dist/contract/capabilities.js +10 -0
  7. package/dist/contract/capabilities.js.map +1 -1
  8. package/dist/contract/events.d.ts +17 -16
  9. package/dist/contract/events.d.ts.map +1 -1
  10. package/dist/contract/events.js +23 -16
  11. package/dist/contract/events.js.map +1 -1
  12. package/dist/contract/models.d.ts +1 -0
  13. package/dist/contract/models.d.ts.map +1 -1
  14. package/dist/contract/models.js +11 -0
  15. package/dist/contract/models.js.map +1 -1
  16. package/dist/contract/router.d.ts +12 -0
  17. package/dist/contract/router.d.ts.map +1 -1
  18. package/dist/contract/router.js +11 -3
  19. package/dist/contract/router.js.map +1 -1
  20. package/dist/contract/settings.d.ts +0 -1
  21. package/dist/contract/settings.d.ts.map +1 -1
  22. package/dist/contract/settings.js +11 -5
  23. package/dist/contract/settings.js.map +1 -1
  24. package/dist/server/index.d.ts +0 -1
  25. package/dist/server/index.d.ts.map +1 -1
  26. package/dist/server/jobs.d.ts.map +1 -1
  27. package/dist/server/jobs.js +13 -5
  28. package/dist/server/jobs.js.map +1 -1
  29. package/dist/server/router.d.ts +12 -0
  30. package/dist/server/router.d.ts.map +1 -1
  31. package/dist/server/router.js +99 -16
  32. package/dist/server/router.js.map +1 -1
  33. package/dist/server/services/access.d.ts +176 -0
  34. package/dist/server/services/access.d.ts.map +1 -0
  35. package/dist/server/services/access.js +260 -0
  36. package/dist/server/services/access.js.map +1 -0
  37. package/dist/server/services/people.d.ts +1 -0
  38. package/dist/server/services/people.d.ts.map +1 -1
  39. package/dist/server/services/people.js +3 -0
  40. package/dist/server/services/people.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/client/components/DayDetail.svelte +32 -14
  43. package/src/client/components/PersonPanel.svelte +66 -5
  44. package/src/client/components/redaction.ts +51 -0
  45. package/src/client/messages.ts +241 -22
  46. package/src/client/pages/DirectoryPage.svelte +45 -1
  47. package/src/client/permissions.ts +12 -4
  48. package/src/client/settings/AccrualSettings.svelte +596 -57
  49. package/src/client/settings/GeneralSettings.svelte +12 -9
  50. package/src/contract/attendance.ts +27 -1
  51. package/src/contract/capabilities.ts +10 -0
  52. package/src/contract/events.ts +23 -19
  53. package/src/contract/models.ts +11 -0
  54. package/src/contract/router.ts +11 -3
  55. package/src/contract/settings.ts +11 -5
@@ -0,0 +1,176 @@
1
+ import type { Principal } from '@kernhq/contracts';
2
+ import type { Kernel, Tx } from '@kernhq/kernel';
3
+ /**
4
+ * Who a viewer may read, and how much of them.
5
+ *
6
+ * The module has always declared four widths of "other people" and enforced one. `hr.person.view`
7
+ * defaults to `member`, so every colleague read every colleague's whole row — personal email, phone
8
+ * number, hire date, termination date — and `view_team`, `view_office` and `view_all` sat in the
9
+ * role editor gating nothing. This is the thing that reads them.
10
+ *
11
+ * **The split is by field, not by row, and that is a product decision.** A staff directory every
12
+ * colleague can open is what a company wants; hiding half the workforce from an engineer looking up
13
+ * who runs Payroll would be a worse product, not a safer one. So `hr.person.view` still returns
14
+ * every person — as a **card**: name, employee number, work email, photo, status, timezone, office.
15
+ * The four fields in `PERSONNEL_FIELDS` are the *personnel record* behind that card, and the three
16
+ * widening keys decide whose card fills out into one:
17
+ *
18
+ * - nobody's but your own, with `hr.person.view` alone;
19
+ * - `hr.person.view_team` — the org-unit subtree you head, plus your direct reports;
20
+ * - `hr.person.view_office` — everybody assigned to an office you head, primary or not, because a
21
+ * non-primary assignment is exactly what "visible to that office's local HR" means;
22
+ * - `hr.person.view_all` — the workspace, and the only one that answers `null`.
23
+ *
24
+ * None of the three implies another, so a country HR manager does not silently become a global one.
25
+ * You always see your own record: identity, never a grant. `hr.person.manage` reads what it writes —
26
+ * see `grantsFor`.
27
+ *
28
+ * **Why this is a computed set of ids and not `.use(requires('hr.person.view_team'))`.**
29
+ * `Authz.can` falls through to the workspace-level effective set whenever there is no object-scope
30
+ * binding for the id it was handed, so an object-scoped `requires` passes for anybody holding the
31
+ * key at workspace level and scopes *nothing*. It would look like it worked. The scope has to be
32
+ * derived from the org chart — `org_units.head_person_id` over the ltree subtree, `offices.head_
33
+ * person_id`, `employments.manager_person_id` — which is what these columns are for and what the
34
+ * GiST index on `org_units.path` was built to answer.
35
+ *
36
+ * **What is not resolved yet:** an object-scope *binding* of `view_team`/`view_office` to a unit or
37
+ * office id, which `permissions.ts` describes. `Authz` exposes no way to ask "is this bound *at*
38
+ * this object" — `can(object, id)` answers true for anybody holding the key at workspace level too
39
+ * — so honouring it would mean one `can` call per office in the workspace on every directory read.
40
+ * Headship is what the columns can answer today; the binding needs either `PermissionScopeKind` to
41
+ * grow `org_unit`/`office` or `Authz` to expose its bindings, both changes to the kernel.
42
+ */
43
+ export declare class HrAccessService {
44
+ private readonly kernel;
45
+ /** Everything, however it was arrived at. Frozen so no caller can narrow the shared answer. */
46
+ private static readonly UNBOUNDED;
47
+ constructor(kernel: Kernel);
48
+ /**
49
+ * Which of the three widening keys this viewer holds, at workspace level.
50
+ *
51
+ * Workspace scope on purpose. The keys are declared `scope: 'object'` so an administrator *can*
52
+ * bind them to a unit or an office, but the question here is only "do you hold this key at all";
53
+ * *which* team or office it covers comes from the org chart below, never from the check.
54
+ *
55
+ * `view_all` is asked through `can`, so an owner and an instance admin pass the way they pass
56
+ * everything else and the two narrower reads are skipped. Below them one `effective()` answers
57
+ * every remaining key, rather than three `can` calls resolving the same set three times.
58
+ *
59
+ * **`hr.person.manage` reads everything it can write.** The edit form is populated from the
60
+ * record it is about to save back, so a writer who saw a redacted card would blank the very
61
+ * fields they could not see — silent data loss on an ordinary edit. Managing is workspace-wide
62
+ * already and has no narrow variant to lose, so this takes nothing away from anybody: it only
63
+ * closes the hole a custom role opens by granting `manage` without `view_all`.
64
+ */
65
+ grantsFor(principal: Principal, workspaceId: string): Promise<HrAccessGrants>;
66
+ /**
67
+ * The people whose full personnel record this viewer may read. `null` means every person.
68
+ *
69
+ * `null` rather than a list of every id in the workspace: an HR administrator in a company of
70
+ * five thousand should not pay for a five-thousand-element set on every directory page, and
71
+ * "unbounded" is a different fact from "happens to cover everybody today".
72
+ *
73
+ * The viewer's own person id is resolved here rather than passed in, because the answer for an
74
+ * unbounded viewer must not cost a lookup at all — and because every caller would otherwise
75
+ * repeat the same two lines before asking.
76
+ */
77
+ visiblePersonIds(tx: Tx, workspaceId: string, principal: Principal): Promise<string[] | null>;
78
+ /** The viewer's own HR record, if they have one. Null for a member who was never made a person. */
79
+ personIdOf(tx: Tx, workspaceId: string, principal: Principal): Promise<string | null>;
80
+ /**
81
+ * `view_team`: everybody in the org-unit subtree this person heads, plus their direct reports.
82
+ *
83
+ * Two questions rather than one, because they answer different shapes of manager. A department
84
+ * head is named on `org_units.head_person_id` and needs the whole subtree under it — one GiST
85
+ * lookup on `path <@`, not a recursive walk. A team lead who heads no unit at all is named on
86
+ * their reports' `employments.manager_person_id`, which has its own index.
87
+ *
88
+ * Headship of an *archived* unit grants nothing: a department that has been dissolved should not
89
+ * keep handing out records. Units inside the subtree are not filtered the same way — the people
90
+ * in them are still employed, and losing them because a sub-department was tidied up would be a
91
+ * hole rather than a narrowing.
92
+ */
93
+ /**
94
+ * Scope is asked as of **today**, through `inForceOn`, not "the row with no end date".
95
+ *
96
+ * The two disagree exactly where this module dates rows, and it dates them routinely, in both
97
+ * directions:
98
+ *
99
+ * - `people.offboard` writes `effective_to` = the last working day, which is normally in the
100
+ * *future*. Keying on a null end date drops somebody the moment their leaving is recorded, so
101
+ * their manager loses the record for the whole notice period — including `terminatedOn`, the
102
+ * one field that says when they actually go. That is the failure nobody reports, because a
103
+ * record that vanishes reads as "HR took it away".
104
+ * - `changeEmployment` closes the open row at `effective_from - 1` and opens the new one, so a
105
+ * transfer dated 1 October and recorded in August would hand the record to the receiving
106
+ * manager five weeks early and take it from the current one just as early. A future joiner is
107
+ * the same shape: created `onboarding` with a future `hired_on`.
108
+ *
109
+ * Every other resolver here answers "who is this today" the same way — `approvals.ts`,
110
+ * `resolve.ts` and the office roster all use `inForceOn`. Visibility must not be the one that
111
+ * disagrees — and the roster was itself changed to this predicate in the same commit, because it
112
+ * had been on the null-end-date test and would otherwise have listed a different set of people
113
+ * from the one whose records this service lets you read, in the same response.
114
+ */
115
+ private teamOf;
116
+ /**
117
+ * `view_office`: everybody assigned to an office this person heads, as of today.
118
+ *
119
+ * Every open assignment, not only the primary one. The primary decides holidays, timezone and
120
+ * policy; the others grant presence — appearing in that office's directory, being visible to its
121
+ * local HR — and that second half is precisely this permission.
122
+ *
123
+ * An office's `parent_office_id` is not walked. A campus is geography, not authority, and there
124
+ * is no materialised path to walk it with; heading the campus and heading its buildings are two
125
+ * grants until somebody asks for the third.
126
+ */
127
+ private officeOf;
128
+ }
129
+ /** Which of the three widening keys a viewer holds. `all` implies the other two. */
130
+ export interface HrAccessGrants {
131
+ /** `hr.person.view_all` — every record in the workspace. */
132
+ all: boolean;
133
+ /** `hr.person.view_team` — the org-unit subtree you head, plus your direct reports. */
134
+ team: boolean;
135
+ /** `hr.person.view_office` — everybody assigned to an office you head. */
136
+ office: boolean;
137
+ }
138
+ /**
139
+ * The fields a directory card does not carry.
140
+ *
141
+ * Personal email and phone are contact details somebody gave their employer, not their colleagues.
142
+ * Hire and termination dates are employment facts: the first is somebody's seniority and the second
143
+ * is the date they were let go, which is not a thing to publish to the whole company on the day it
144
+ * happens.
145
+ *
146
+ * Everything else on `Person` stays: name, employee number, work email, photo, status, timezone,
147
+ * office, and `custom`. A directory that cannot tell you somebody's work email is not a directory.
148
+ *
149
+ * Nulled rather than omitted, because all four are `.nullable()` in the contract — so a card parses
150
+ * as a `Person` and no client has to learn a second shape.
151
+ */
152
+ export declare const PERSONNEL_FIELDS: readonly ["personalEmail", "phone", "hiredOn", "terminatedOn"];
153
+ /** The shape `forViewer` narrows. Structural, so it fits a row with extra columns joined onto it. */
154
+ export interface PersonnelFields {
155
+ personalEmail: string | null;
156
+ phone: string | null;
157
+ hiredOn: string | null;
158
+ terminatedOn: string | null;
159
+ personnelHidden: boolean;
160
+ }
161
+ /** What `visiblePersonIds` answered, ready to be asked about one person at a time. */
162
+ export type VisiblePeople = ReadonlySet<string> | null;
163
+ /** Build the lookup once per page rather than scanning an array per row. */
164
+ export declare const visibleSet: (ids: string[] | null) => VisiblePeople;
165
+ /** Does this viewer read the record behind the card, or only the card? */
166
+ export declare const seesRecordOf: (visible: VisiblePeople, personId: string) => boolean;
167
+ /**
168
+ * A person as this viewer may read them: the whole record, or the directory card.
169
+ *
170
+ * Returns the object it was given when nothing is hidden, so the common path — an HR administrator
171
+ * with `view_all` — allocates nothing.
172
+ */
173
+ export declare function forViewer<T extends PersonnelFields & {
174
+ id: string;
175
+ }>(person: T, visible: VisiblePeople): T;
176
+ //# sourceMappingURL=access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../../src/server/services/access.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAA;AAMhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,qBAAa,eAAe;IAQd,OAAO,CAAC,QAAQ,CAAC,MAAM;IAPnC,+FAA+F;IAC/F,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAI/B;gBAE2B,MAAM,EAAE,MAAM;IAE3C;;;;;;;;;;;;;;;;OAgBG;IACG,SAAS,CAAC,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAgBnF;;;;;;;;;;OAUG;IACG,gBAAgB,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC;IAkBnG,mGAAmG;IAC7F,UAAU,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAU3F;;;;;;;;;;;;OAYG;IACH;;;;;;;;;;;;;;;;;;;;;OAqBG;YACW,MAAM;IAoDpB;;;;;;;;;;OAUG;YACW,QAAQ;CAmCvB;AAED,oFAAoF;AACpF,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,GAAG,EAAE,OAAO,CAAA;IACZ,uFAAuF;IACvF,IAAI,EAAE,OAAO,CAAA;IACb,0EAA0E;IAC1E,MAAM,EAAE,OAAO,CAAA;CAChB;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB,gEAAiE,CAAA;AAE9F,qGAAqG;AACrG,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,eAAe,EAAE,OAAO,CAAA;CACzB;AAED,sFAAsF;AACtF,MAAM,MAAM,aAAa,GAAG,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,CAAA;AAEtD,4EAA4E;AAC5E,eAAO,MAAM,UAAU,GAAI,KAAK,MAAM,EAAE,GAAG,IAAI,KAAG,aAAqD,CAAA;AAEvG,0EAA0E;AAC1E,eAAO,MAAM,YAAY,GAAI,SAAS,aAAa,EAAE,UAAU,MAAM,KAAG,OAC7B,CAAA;AAE3C;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,eAAe,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,aAAa,GAAG,CAAC,CAa1G"}
@@ -0,0 +1,260 @@
1
+ import { and, eq, inArray, isNull, or, sql } from 'drizzle-orm';
2
+ import { HR_PERMISSIONS } from '../../contract/index.js';
3
+ import { employments, officeAssignments, offices, orgUnits, people } from '../schema.js';
4
+ import { inForceOn, todayIso } from './db.js';
5
+ /**
6
+ * Who a viewer may read, and how much of them.
7
+ *
8
+ * The module has always declared four widths of "other people" and enforced one. `hr.person.view`
9
+ * defaults to `member`, so every colleague read every colleague's whole row — personal email, phone
10
+ * number, hire date, termination date — and `view_team`, `view_office` and `view_all` sat in the
11
+ * role editor gating nothing. This is the thing that reads them.
12
+ *
13
+ * **The split is by field, not by row, and that is a product decision.** A staff directory every
14
+ * colleague can open is what a company wants; hiding half the workforce from an engineer looking up
15
+ * who runs Payroll would be a worse product, not a safer one. So `hr.person.view` still returns
16
+ * every person — as a **card**: name, employee number, work email, photo, status, timezone, office.
17
+ * The four fields in `PERSONNEL_FIELDS` are the *personnel record* behind that card, and the three
18
+ * widening keys decide whose card fills out into one:
19
+ *
20
+ * - nobody's but your own, with `hr.person.view` alone;
21
+ * - `hr.person.view_team` — the org-unit subtree you head, plus your direct reports;
22
+ * - `hr.person.view_office` — everybody assigned to an office you head, primary or not, because a
23
+ * non-primary assignment is exactly what "visible to that office's local HR" means;
24
+ * - `hr.person.view_all` — the workspace, and the only one that answers `null`.
25
+ *
26
+ * None of the three implies another, so a country HR manager does not silently become a global one.
27
+ * You always see your own record: identity, never a grant. `hr.person.manage` reads what it writes —
28
+ * see `grantsFor`.
29
+ *
30
+ * **Why this is a computed set of ids and not `.use(requires('hr.person.view_team'))`.**
31
+ * `Authz.can` falls through to the workspace-level effective set whenever there is no object-scope
32
+ * binding for the id it was handed, so an object-scoped `requires` passes for anybody holding the
33
+ * key at workspace level and scopes *nothing*. It would look like it worked. The scope has to be
34
+ * derived from the org chart — `org_units.head_person_id` over the ltree subtree, `offices.head_
35
+ * person_id`, `employments.manager_person_id` — which is what these columns are for and what the
36
+ * GiST index on `org_units.path` was built to answer.
37
+ *
38
+ * **What is not resolved yet:** an object-scope *binding* of `view_team`/`view_office` to a unit or
39
+ * office id, which `permissions.ts` describes. `Authz` exposes no way to ask "is this bound *at*
40
+ * this object" — `can(object, id)` answers true for anybody holding the key at workspace level too
41
+ * — so honouring it would mean one `can` call per office in the workspace on every directory read.
42
+ * Headship is what the columns can answer today; the binding needs either `PermissionScopeKind` to
43
+ * grow `org_unit`/`office` or `Authz` to expose its bindings, both changes to the kernel.
44
+ */
45
+ export class HrAccessService {
46
+ kernel;
47
+ /** Everything, however it was arrived at. Frozen so no caller can narrow the shared answer. */
48
+ static UNBOUNDED = Object.freeze({
49
+ all: true,
50
+ team: true,
51
+ office: true,
52
+ });
53
+ constructor(kernel) {
54
+ this.kernel = kernel;
55
+ }
56
+ /**
57
+ * Which of the three widening keys this viewer holds, at workspace level.
58
+ *
59
+ * Workspace scope on purpose. The keys are declared `scope: 'object'` so an administrator *can*
60
+ * bind them to a unit or an office, but the question here is only "do you hold this key at all";
61
+ * *which* team or office it covers comes from the org chart below, never from the check.
62
+ *
63
+ * `view_all` is asked through `can`, so an owner and an instance admin pass the way they pass
64
+ * everything else and the two narrower reads are skipped. Below them one `effective()` answers
65
+ * every remaining key, rather than three `can` calls resolving the same set three times.
66
+ *
67
+ * **`hr.person.manage` reads everything it can write.** The edit form is populated from the
68
+ * record it is about to save back, so a writer who saw a redacted card would blank the very
69
+ * fields they could not see — silent data loss on an ordinary edit. Managing is workspace-wide
70
+ * already and has no narrow variant to lose, so this takes nothing away from anybody: it only
71
+ * closes the hole a custom role opens by granting `manage` without `view_all`.
72
+ */
73
+ async grantsFor(principal, workspaceId) {
74
+ const all = await this.kernel.authz.can(principal, HR_PERMISSIONS.personViewAll, {
75
+ kind: 'workspace',
76
+ id: workspaceId,
77
+ workspaceId,
78
+ });
79
+ if (all)
80
+ return HrAccessService.UNBOUNDED;
81
+ const held = await this.kernel.authz.effective(principal, workspaceId);
82
+ if (held.has(HR_PERMISSIONS.personManage))
83
+ return HrAccessService.UNBOUNDED;
84
+ return {
85
+ all: false,
86
+ team: held.has(HR_PERMISSIONS.personViewTeam),
87
+ office: held.has(HR_PERMISSIONS.personViewOffice),
88
+ };
89
+ }
90
+ /**
91
+ * The people whose full personnel record this viewer may read. `null` means every person.
92
+ *
93
+ * `null` rather than a list of every id in the workspace: an HR administrator in a company of
94
+ * five thousand should not pay for a five-thousand-element set on every directory page, and
95
+ * "unbounded" is a different fact from "happens to cover everybody today".
96
+ *
97
+ * The viewer's own person id is resolved here rather than passed in, because the answer for an
98
+ * unbounded viewer must not cost a lookup at all — and because every caller would otherwise
99
+ * repeat the same two lines before asking.
100
+ */
101
+ async visiblePersonIds(tx, workspaceId, principal) {
102
+ const grants = await this.grantsFor(principal, workspaceId);
103
+ if (grants.all)
104
+ return null;
105
+ const viewer = await this.personIdOf(tx, workspaceId, principal);
106
+ // Plenty of members are not employees. They see the directory and no record but — since they
107
+ // have none — nobody's.
108
+ if (!viewer)
109
+ return [];
110
+ const ids = new Set([viewer]);
111
+ // One date for both resolvers, so a request that straddles midnight cannot answer the team from
112
+ // one day and the office from the next.
113
+ const today = todayIso();
114
+ if (grants.team)
115
+ for (const id of await this.teamOf(tx, workspaceId, viewer, today))
116
+ ids.add(id);
117
+ if (grants.office)
118
+ for (const id of await this.officeOf(tx, workspaceId, viewer, today))
119
+ ids.add(id);
120
+ return [...ids];
121
+ }
122
+ /** The viewer's own HR record, if they have one. Null for a member who was never made a person. */
123
+ async personIdOf(tx, workspaceId, principal) {
124
+ if (!principal.userId)
125
+ return null;
126
+ const [row] = await tx
127
+ .select({ id: people.id })
128
+ .from(people)
129
+ .where(and(eq(people.workspaceId, workspaceId), eq(people.userId, principal.userId)))
130
+ .limit(1);
131
+ return row?.id ?? null;
132
+ }
133
+ /**
134
+ * `view_team`: everybody in the org-unit subtree this person heads, plus their direct reports.
135
+ *
136
+ * Two questions rather than one, because they answer different shapes of manager. A department
137
+ * head is named on `org_units.head_person_id` and needs the whole subtree under it — one GiST
138
+ * lookup on `path <@`, not a recursive walk. A team lead who heads no unit at all is named on
139
+ * their reports' `employments.manager_person_id`, which has its own index.
140
+ *
141
+ * Headship of an *archived* unit grants nothing: a department that has been dissolved should not
142
+ * keep handing out records. Units inside the subtree are not filtered the same way — the people
143
+ * in them are still employed, and losing them because a sub-department was tidied up would be a
144
+ * hole rather than a narrowing.
145
+ */
146
+ /**
147
+ * Scope is asked as of **today**, through `inForceOn`, not "the row with no end date".
148
+ *
149
+ * The two disagree exactly where this module dates rows, and it dates them routinely, in both
150
+ * directions:
151
+ *
152
+ * - `people.offboard` writes `effective_to` = the last working day, which is normally in the
153
+ * *future*. Keying on a null end date drops somebody the moment their leaving is recorded, so
154
+ * their manager loses the record for the whole notice period — including `terminatedOn`, the
155
+ * one field that says when they actually go. That is the failure nobody reports, because a
156
+ * record that vanishes reads as "HR took it away".
157
+ * - `changeEmployment` closes the open row at `effective_from - 1` and opens the new one, so a
158
+ * transfer dated 1 October and recorded in August would hand the record to the receiving
159
+ * manager five weeks early and take it from the current one just as early. A future joiner is
160
+ * the same shape: created `onboarding` with a future `hired_on`.
161
+ *
162
+ * Every other resolver here answers "who is this today" the same way — `approvals.ts`,
163
+ * `resolve.ts` and the office roster all use `inForceOn`. Visibility must not be the one that
164
+ * disagrees — and the roster was itself changed to this predicate in the same commit, because it
165
+ * had been on the null-end-date test and would otherwise have listed a different set of people
166
+ * from the one whose records this service lets you read, in the same response.
167
+ */
168
+ async teamOf(tx, workspaceId, viewerPersonId, today) {
169
+ const ids = new Set();
170
+ const headed = await tx
171
+ .select({ path: orgUnits.path })
172
+ .from(orgUnits)
173
+ .where(and(eq(orgUnits.workspaceId, workspaceId), eq(orgUnits.headPersonId, viewerPersonId), isNull(orgUnits.archivedAt)));
174
+ if (headed.length) {
175
+ const subtree = or(...headed.map((u) => sql `${orgUnits.path} <@ ${u.path}::ltree`));
176
+ const rows = await tx
177
+ .select({ personId: employments.personId })
178
+ .from(employments)
179
+ .innerJoin(orgUnits, and(eq(orgUnits.id, employments.orgUnitId), eq(orgUnits.workspaceId, employments.workspaceId)))
180
+ .where(and(eq(employments.workspaceId, workspaceId), inForceOn(employments.effectiveFrom, employments.effectiveTo, today), subtree));
181
+ for (const r of rows)
182
+ ids.add(r.personId);
183
+ }
184
+ const reports = await tx
185
+ .select({ personId: employments.personId })
186
+ .from(employments)
187
+ .where(and(eq(employments.workspaceId, workspaceId), eq(employments.managerPersonId, viewerPersonId), inForceOn(employments.effectiveFrom, employments.effectiveTo, today)));
188
+ for (const r of reports)
189
+ ids.add(r.personId);
190
+ return [...ids];
191
+ }
192
+ /**
193
+ * `view_office`: everybody assigned to an office this person heads, as of today.
194
+ *
195
+ * Every open assignment, not only the primary one. The primary decides holidays, timezone and
196
+ * policy; the others grant presence — appearing in that office's directory, being visible to its
197
+ * local HR — and that second half is precisely this permission.
198
+ *
199
+ * An office's `parent_office_id` is not walked. A campus is geography, not authority, and there
200
+ * is no materialised path to walk it with; heading the campus and heading its buildings are two
201
+ * grants until somebody asks for the third.
202
+ */
203
+ async officeOf(tx, workspaceId, viewerPersonId, today) {
204
+ const headed = await tx
205
+ .select({ id: offices.id })
206
+ .from(offices)
207
+ .where(and(eq(offices.workspaceId, workspaceId), eq(offices.headPersonId, viewerPersonId), isNull(offices.archivedAt)));
208
+ if (!headed.length)
209
+ return [];
210
+ const rows = await tx
211
+ .select({ personId: officeAssignments.personId })
212
+ .from(officeAssignments)
213
+ .where(and(eq(officeAssignments.workspaceId, workspaceId), inArray(officeAssignments.officeId, headed.map((o) => o.id)),
214
+ // As of today, for the reason spelled out on `teamOf`: an office move is dated, so a null
215
+ // end date is neither "here now" nor "here on the day we are asking about".
216
+ inForceOn(officeAssignments.effectiveFrom, officeAssignments.effectiveTo, today)));
217
+ return [...new Set(rows.map((r) => r.personId))];
218
+ }
219
+ }
220
+ /**
221
+ * The fields a directory card does not carry.
222
+ *
223
+ * Personal email and phone are contact details somebody gave their employer, not their colleagues.
224
+ * Hire and termination dates are employment facts: the first is somebody's seniority and the second
225
+ * is the date they were let go, which is not a thing to publish to the whole company on the day it
226
+ * happens.
227
+ *
228
+ * Everything else on `Person` stays: name, employee number, work email, photo, status, timezone,
229
+ * office, and `custom`. A directory that cannot tell you somebody's work email is not a directory.
230
+ *
231
+ * Nulled rather than omitted, because all four are `.nullable()` in the contract — so a card parses
232
+ * as a `Person` and no client has to learn a second shape.
233
+ */
234
+ export const PERSONNEL_FIELDS = ['personalEmail', 'phone', 'hiredOn', 'terminatedOn'];
235
+ /** Build the lookup once per page rather than scanning an array per row. */
236
+ export const visibleSet = (ids) => (ids === null ? null : new Set(ids));
237
+ /** Does this viewer read the record behind the card, or only the card? */
238
+ export const seesRecordOf = (visible, personId) => visible === null || visible.has(personId);
239
+ /**
240
+ * A person as this viewer may read them: the whole record, or the directory card.
241
+ *
242
+ * Returns the object it was given when nothing is hidden, so the common path — an HR administrator
243
+ * with `view_all` — allocates nothing.
244
+ */
245
+ export function forViewer(person, visible) {
246
+ if (seesRecordOf(visible, person.id))
247
+ return person;
248
+ // `personnelHidden` is what lets a screen tell "withheld" from "empty". This is the only place
249
+ // that nulls these fields, so it is the only place that can honestly say so — a client trying to
250
+ // infer it would be re-deriving the org chart from data it never fetched.
251
+ return {
252
+ ...person,
253
+ personalEmail: null,
254
+ phone: null,
255
+ hiredOn: null,
256
+ terminatedOn: null,
257
+ personnelHidden: true,
258
+ };
259
+ }
260
+ //# sourceMappingURL=access.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access.js","sourceRoot":"","sources":["../../../src/server/services/access.ts"],"names":[],"mappings":"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"}
@@ -19,6 +19,7 @@ export declare class PeopleService {
19
19
  static toPerson(row: PersonRow): {
20
20
  status: PersonStatus;
21
21
  custom: Record<string, unknown>;
22
+ personnelHidden: boolean;
22
23
  createdAt: string;
23
24
  updatedAt: string;
24
25
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"people.d.ts","sourceRoot":"","sources":["../../../src/server/services/people.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,MAAM,EAAE,KAAK,EAAE,EAAU,MAAM,gBAAgB,CAAA;AAExE,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AACvF,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,EAAiB,MAAM,cAAc,CAAA;AAGpF,yDAAyD;AACzD,MAAM,MAAM,SAAS,GAAG,OAAO,MAAM,CAAC,YAAY,CAAA;AAClD,MAAM,MAAM,aAAa,GAAG,OAAO,WAAW,CAAC,YAAY,CAAA;AAE3D;;;;;;;GAOG;AACH,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,MAAM;IAE3C,sGAAsG;IACtG,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,SAAS;gBAMJ,YAAY;;;;;;;;;;;;;;;;;IAOtC,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,aAAa;wBAGI,cAAc;;;;;;;;;;;;;;;;IAOlD,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAUvE,QAAQ,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAS3F;;;;;;;OAOG;IACG,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAUvF,2EAA2E;IACrE,MAAM,CACV,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,OAAO,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,EAC7D,MAAM,SAAQ;IAiBhB;;;;;;OAMG;IACG,gBAAgB,CACpB,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,aAAa,GAAG,UAAU,GAAG,WAAW,CAAC,CAAC,GACnF,OAAO,CAAC,aAAa,CAAC;IAkDzB;;;;;;OAMG;IACG,YAAY,CAChB,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,OAAO,EAClB,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,MAAM,GAAG,IAAI;;;;;;;;;;;IAkDvB,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,OAAO,iBAAiB,CAAC,YAAY;;;;;;;;;;;IAI9D,KAAK,eAAW;CACjB"}
1
+ {"version":3,"file":"people.d.ts","sourceRoot":"","sources":["../../../src/server/services/people.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,MAAM,EAAE,KAAK,EAAE,EAAU,MAAM,gBAAgB,CAAA;AAExE,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AACvF,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,EAAiB,MAAM,cAAc,CAAA;AAGpF,yDAAyD;AACzD,MAAM,MAAM,SAAS,GAAG,OAAO,MAAM,CAAC,YAAY,CAAA;AAClD,MAAM,MAAM,aAAa,GAAG,OAAO,WAAW,CAAC,YAAY,CAAA;AAE3D;;;;;;;GAOG;AACH,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,MAAM;IAE3C,sGAAsG;IACtG,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,SAAS;gBAMJ,YAAY;;;;;;;;;;;;;;;;;;IAUtC,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,aAAa;wBAGI,cAAc;;;;;;;;;;;;;;;;IAOlD,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAUvE,QAAQ,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAS3F;;;;;;;OAOG;IACG,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAUvF,2EAA2E;IACrE,MAAM,CACV,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,OAAO,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,EAC7D,MAAM,SAAQ;IAiBhB;;;;;;OAMG;IACG,gBAAgB,CACpB,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,aAAa,GAAG,UAAU,GAAG,WAAW,CAAC,CAAC,GACnF,OAAO,CAAC,aAAa,CAAC;IAkDzB;;;;;;OAMG;IACG,YAAY,CAChB,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,OAAO,EAClB,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,MAAM,GAAG,IAAI;;;;;;;;;;;IAkDvB,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,OAAO,iBAAiB,CAAC,YAAY;;;;;;;;;;;IAI9D,KAAK,eAAW;CACjB"}
@@ -24,6 +24,9 @@ export class PeopleService {
24
24
  // consumer; the column is only ever written from the same union.
25
25
  status: row.status,
26
26
  custom: row.custom ?? {},
27
+ // False here and set true by `forViewer` alone, which is the only thing that withholds these
28
+ // fields. A record that never passed through it was never redacted.
29
+ personnelHidden: false,
27
30
  createdAt: row.createdAt.toISOString(),
28
31
  updatedAt: row.updatedAt.toISOString(),
29
32
  };
@@ -1 +1 @@
1
- {"version":3,"file":"people.js","sourceRoot":"","sources":["../../../src/server/services/people.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"people.js","sourceRoot":"","sources":["../../../src/server/services/people.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kernhq/module-hr",
3
- "version": "0.13.2",
3
+ "version": "0.14.0",
4
4
  "description": "Kern HR module: people, offices, org chart, calendars",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
@@ -109,20 +109,38 @@ const directionLabel = (direction: string) =>
109
109
  ? t('att_punch_break_start')
110
110
  : t('att_punch_break_end')
111
111
 
112
- const methodLabel = (method: string) =>
113
- method === 'web'
114
- ? t('att_method_web')
115
- : method === 'mobile'
116
- ? t('att_method_mobile')
117
- : method === 'kiosk'
118
- ? t('att_method_kiosk')
119
- : method === 'qr'
120
- ? t('att_method_qr')
121
- : method === 'device'
122
- ? t('att_method_device')
123
- : method === 'import'
124
- ? t('att_method_import')
125
- : t('att_method_manual')
112
+ /**
113
+ * What recorded the punch, in words.
114
+ *
115
+ * Keyed by the method the contract names, and `auto` is the machine's own — the job that closes a
116
+ * shift nobody clocked out of. Every method used to be a rung on a ternary ending in
117
+ * `att_method_manual`, so `auto`, and anything else the contract might add, landed on **"Entered by
118
+ * hand"**: the one badge on this panel that names a culprit, telling somebody arguing about their
119
+ * own timesheet that a person had typed a punch no person touched.
120
+ *
121
+ * So the fallback no longer claims anything. A method with no string here is a build behind the
122
+ * server, and it renders the raw code — the same bargain `anomalyLabel` below makes, and the same
123
+ * reasoning: a code somebody can quote in a question beats a confident wrong sentence about who
124
+ * touched their record.
125
+ */
126
+ const METHOD_KEYS: Record<string, string> = {
127
+ web: 'hr.att_method_web',
128
+ mobile: 'hr.att_method_mobile',
129
+ kiosk: 'hr.att_method_kiosk',
130
+ qr: 'hr.att_method_qr',
131
+ device: 'hr.att_method_device',
132
+ import: 'hr.att_method_import',
133
+ manual: 'hr.att_method_manual',
134
+ auto: 'hr.att_method_auto',
135
+ }
136
+ const methodLabel = (method: string): string => {
137
+ // Namespaced keys, because `t()` answers a miss with the key it was **given**: a bare
138
+ // `att_method_auto` comes back as `hr.att_method_auto`, which never equals what was passed, so a
139
+ // missing string would test as present and render the key at somebody.
140
+ const key = METHOD_KEYS[method]
141
+ const text = key ? t(key) : undefined
142
+ return text && text !== key ? text : method
143
+ }
126
144
 
127
145
  /**
128
146
  * The time, with the date added only when it is not this day's.