@kernhq/module-hr 0.1.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.
- package/LICENSE +662 -0
- package/README.md +71 -0
- package/dist/contract/capabilities.d.ts +45 -0
- package/dist/contract/capabilities.d.ts.map +1 -0
- package/dist/contract/capabilities.js +114 -0
- package/dist/contract/capabilities.js.map +1 -0
- package/dist/contract/events.d.ts +74 -0
- package/dist/contract/events.d.ts.map +1 -0
- package/dist/contract/events.js +63 -0
- package/dist/contract/events.js.map +1 -0
- package/dist/contract/index.d.ts +15 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +15 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/contract/models.d.ts +457 -0
- package/dist/contract/models.d.ts.map +1 -0
- package/dist/contract/models.js +381 -0
- package/dist/contract/models.js.map +1 -0
- package/dist/contract/permissions.d.ts +174 -0
- package/dist/contract/permissions.d.ts.map +1 -0
- package/dist/contract/permissions.js +209 -0
- package/dist/contract/permissions.js.map +1 -0
- package/dist/contract/router.d.ts +2720 -0
- package/dist/contract/router.d.ts.map +1 -0
- package/dist/contract/router.js +520 -0
- package/dist/contract/router.js.map +1 -0
- package/dist/contract/settings.d.ts +20 -0
- package/dist/contract/settings.d.ts.map +1 -0
- package/dist/contract/settings.js +34 -0
- package/dist/contract/settings.js.map +1 -0
- package/dist/policy/calendar.d.ts +64 -0
- package/dist/policy/calendar.d.ts.map +1 -0
- package/dist/policy/calendar.js +113 -0
- package/dist/policy/calendar.js.map +1 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +141 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/packs/index.d.ts +50 -0
- package/dist/server/packs/index.d.ts.map +1 -0
- package/dist/server/packs/index.js +121 -0
- package/dist/server/packs/index.js.map +1 -0
- package/dist/server/router.d.ts +3095 -0
- package/dist/server/router.d.ts.map +1 -0
- package/dist/server/router.js +1537 -0
- package/dist/server/router.js.map +1 -0
- package/dist/server/schema.d.ts +2828 -0
- package/dist/server/schema.d.ts.map +1 -0
- package/dist/server/schema.js +313 -0
- package/dist/server/schema.js.map +1 -0
- package/dist/server/services/db.d.ts +22 -0
- package/dist/server/services/db.d.ts.map +1 -0
- package/dist/server/services/db.js +23 -0
- package/dist/server/services/db.js.map +1 -0
- package/dist/server/services/people.d.ts +110 -0
- package/dist/server/services/people.d.ts.map +1 -0
- package/dist/server/services/people.js +182 -0
- package/dist/server/services/people.js.map +1 -0
- package/dist/server/services/resolve.d.ts +66 -0
- package/dist/server/services/resolve.d.ts.map +1 -0
- package/dist/server/services/resolve.js +145 -0
- package/dist/server/services/resolve.js.map +1 -0
- package/migrations/0000_init.sql +223 -0
- package/migrations/0001_rls.sql +141 -0
- package/migrations/meta/0000_snapshot.json +1715 -0
- package/migrations/meta/_journal.json +20 -0
- package/package.json +59 -0
- package/src/client/index.ts +62 -0
- package/src/contract/capabilities.ts +117 -0
- package/src/contract/events.ts +84 -0
- package/src/contract/index.ts +14 -0
- package/src/contract/models.ts +437 -0
- package/src/contract/permissions.ts +217 -0
- package/src/contract/router.ts +611 -0
- package/src/contract/settings.ts +35 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { KernError, uuidv7 } from '@kernhq/kernel';
|
|
2
|
+
import { and, eq, isNull, sql } from 'drizzle-orm';
|
|
3
|
+
import { employments, officeAssignments, people, personHistory } from '../schema.js';
|
|
4
|
+
import { isOpen, todayIso } from './db.js';
|
|
5
|
+
/**
|
|
6
|
+
* Writing people, employment and office moves — the three things that must stay consistent.
|
|
7
|
+
*
|
|
8
|
+
* Everything effective-dated goes through here rather than through the router, because "close the
|
|
9
|
+
* open row, then insert the new one" has to happen in one transaction and in that order. Done the
|
|
10
|
+
* other way round, a unique or exclusion constraint fires on the overlap and the change looks like
|
|
11
|
+
* a validation error instead of a race.
|
|
12
|
+
*/
|
|
13
|
+
export class PeopleService {
|
|
14
|
+
kernel;
|
|
15
|
+
constructor(kernel) {
|
|
16
|
+
this.kernel = kernel;
|
|
17
|
+
}
|
|
18
|
+
/** Serialise a row for the wire. Postgres `numeric` arrives as a string; the contract says number. */
|
|
19
|
+
static toPerson(row) {
|
|
20
|
+
return {
|
|
21
|
+
...row,
|
|
22
|
+
// `status`, `employment_type` and friends are `text` in Postgres and unions in the contract.
|
|
23
|
+
// Narrowing here rather than widening the contract keeps the enum meaningful for every
|
|
24
|
+
// consumer; the column is only ever written from the same union.
|
|
25
|
+
status: row.status,
|
|
26
|
+
custom: row.custom ?? {},
|
|
27
|
+
createdAt: row.createdAt.toISOString(),
|
|
28
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
static toEmployment(row) {
|
|
32
|
+
return {
|
|
33
|
+
...row,
|
|
34
|
+
employmentType: row.employmentType,
|
|
35
|
+
fte: Number.parseFloat(row.fte ?? '1'),
|
|
36
|
+
contractHoursWeek: row.contractHoursWeek === null ? null : Number.parseFloat(row.contractHoursWeek),
|
|
37
|
+
createdAt: row.createdAt.toISOString(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async load(tx, workspaceId, personId) {
|
|
41
|
+
const [row] = await tx
|
|
42
|
+
.select()
|
|
43
|
+
.from(people)
|
|
44
|
+
.where(and(eq(people.workspaceId, workspaceId), eq(people.id, personId)))
|
|
45
|
+
.limit(1);
|
|
46
|
+
if (!row)
|
|
47
|
+
throw KernError.notFound('Person');
|
|
48
|
+
return row;
|
|
49
|
+
}
|
|
50
|
+
async byUserId(tx, workspaceId, userId) {
|
|
51
|
+
const [row] = await tx
|
|
52
|
+
.select()
|
|
53
|
+
.from(people)
|
|
54
|
+
.where(and(eq(people.workspaceId, workspaceId), eq(people.userId, userId)))
|
|
55
|
+
.limit(1);
|
|
56
|
+
return row;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The next employee number, from workspace settings.
|
|
60
|
+
*
|
|
61
|
+
* Read-modify-write rather than a sequence, because the prefix and the counter are things an
|
|
62
|
+
* administrator edits — and a Postgres sequence cannot be edited without surprising them. Two
|
|
63
|
+
* people created in the same instant can therefore collide; the unique index catches it and the
|
|
64
|
+
* caller retries, which is the right trade for something that happens a handful of times a day.
|
|
65
|
+
*/
|
|
66
|
+
async nextEmployeeNo(workspaceId, settings) {
|
|
67
|
+
if (!settings.employeeNumberPrefix && settings.employeeNumberNext <= 1)
|
|
68
|
+
return null;
|
|
69
|
+
const next = settings.employeeNumberNext;
|
|
70
|
+
await this.kernel.settings.setModule(workspaceId, 'hr', {
|
|
71
|
+
...settings,
|
|
72
|
+
employeeNumberNext: next + 1,
|
|
73
|
+
});
|
|
74
|
+
return `${settings.employeeNumberPrefix}${next}`;
|
|
75
|
+
}
|
|
76
|
+
/** Append-only. Every field change lands here, and nothing rewrites it. */
|
|
77
|
+
async record(tx, workspaceId, personId, actorId, changes, source = 'app') {
|
|
78
|
+
if (!changes.length)
|
|
79
|
+
return;
|
|
80
|
+
await tx.insert(personHistory).values(changes.map((c) => ({
|
|
81
|
+
id: uuidv7(),
|
|
82
|
+
workspaceId,
|
|
83
|
+
personId,
|
|
84
|
+
field: c.field,
|
|
85
|
+
from: (c.from ?? null),
|
|
86
|
+
to: (c.to ?? null),
|
|
87
|
+
actorId,
|
|
88
|
+
source,
|
|
89
|
+
})));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Close the open employment row and open a new one from `effectiveFrom`.
|
|
93
|
+
*
|
|
94
|
+
* The new row inherits everything the caller did not name, so "change her manager" does not
|
|
95
|
+
* silently blank her department. A change dated before the current row's start is refused rather
|
|
96
|
+
than than quietly reordering history — that is a correction, and corrections need their own path.
|
|
97
|
+
*/
|
|
98
|
+
async changeEmployment(tx, workspaceId, personId, effectiveFrom, patch) {
|
|
99
|
+
const [open] = await tx
|
|
100
|
+
.select()
|
|
101
|
+
.from(employments)
|
|
102
|
+
.where(and(eq(employments.workspaceId, workspaceId), eq(employments.personId, personId), isOpen(employments.effectiveTo)))
|
|
103
|
+
.limit(1);
|
|
104
|
+
if (open && effectiveFrom < open.effectiveFrom)
|
|
105
|
+
throw KernError.badRequest(`This person's current record starts on ${open.effectiveFrom}; a change cannot be dated before it.`);
|
|
106
|
+
if (open) {
|
|
107
|
+
// The previous period ends the day before the new one starts. Computed in SQL so the boundary
|
|
108
|
+
// is Postgres's own date arithmetic rather than a string manipulation that has to know about
|
|
109
|
+
// month lengths and leap years.
|
|
110
|
+
await tx
|
|
111
|
+
.update(employments)
|
|
112
|
+
.set({ effectiveTo: sql `${effectiveFrom}::date - 1` })
|
|
113
|
+
.where(eq(employments.id, open.id));
|
|
114
|
+
}
|
|
115
|
+
const [row] = await tx
|
|
116
|
+
.insert(employments)
|
|
117
|
+
.values({
|
|
118
|
+
id: uuidv7(),
|
|
119
|
+
workspaceId,
|
|
120
|
+
personId,
|
|
121
|
+
effectiveFrom,
|
|
122
|
+
effectiveTo: null,
|
|
123
|
+
orgUnitId: patch.orgUnitId ?? open?.orgUnitId ?? null,
|
|
124
|
+
positionId: patch.positionId ?? open?.positionId ?? null,
|
|
125
|
+
legalEntityId: patch.legalEntityId ?? open?.legalEntityId ?? null,
|
|
126
|
+
costCenterId: patch.costCenterId ?? open?.costCenterId ?? null,
|
|
127
|
+
managerPersonId: patch.managerPersonId ?? open?.managerPersonId ?? null,
|
|
128
|
+
employmentType: patch.employmentType ?? open?.employmentType ?? 'full_time',
|
|
129
|
+
fte: patch.fte ?? open?.fte ?? '1.000',
|
|
130
|
+
contractHoursWeek: patch.contractHoursWeek ?? open?.contractHoursWeek ?? null,
|
|
131
|
+
reason: patch.reason ?? null,
|
|
132
|
+
})
|
|
133
|
+
.returning();
|
|
134
|
+
return row;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Assign somebody to an office from a date, optionally as their primary.
|
|
138
|
+
*
|
|
139
|
+
* Making one primary demotes the other primary rather than leaving two — the database refuses two
|
|
140
|
+
* anyway, and finding that out as a constraint violation would turn an ordinary office move into
|
|
141
|
+
* an error the person doing it cannot act on.
|
|
142
|
+
*/
|
|
143
|
+
async assignOffice(tx, workspaceId, personId, officeId, isPrimary, effectiveFrom, reason) {
|
|
144
|
+
const open = await tx
|
|
145
|
+
.select()
|
|
146
|
+
.from(officeAssignments)
|
|
147
|
+
.where(and(eq(officeAssignments.workspaceId, workspaceId), eq(officeAssignments.personId, personId), isNull(officeAssignments.effectiveTo)));
|
|
148
|
+
for (const row of open) {
|
|
149
|
+
if (row.officeId === officeId) {
|
|
150
|
+
// Already here. Close it so the new row carries the new primary flag and start date rather
|
|
151
|
+
// than leaving two rows for one office.
|
|
152
|
+
await tx
|
|
153
|
+
.update(officeAssignments)
|
|
154
|
+
.set({ effectiveTo: sql `${effectiveFrom}::date - 1` })
|
|
155
|
+
.where(eq(officeAssignments.id, row.id));
|
|
156
|
+
}
|
|
157
|
+
else if (isPrimary && row.isPrimary) {
|
|
158
|
+
// Demote, do not close: somebody moving their primary from Istanbul to Amsterdam usually
|
|
159
|
+
// keeps a presence in Istanbul, and closing it would remove them from that directory.
|
|
160
|
+
await tx.update(officeAssignments).set({ isPrimary: false }).where(eq(officeAssignments.id, row.id));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
await tx.insert(officeAssignments).values({
|
|
164
|
+
id: uuidv7(),
|
|
165
|
+
workspaceId,
|
|
166
|
+
personId,
|
|
167
|
+
officeId,
|
|
168
|
+
isPrimary,
|
|
169
|
+
effectiveFrom,
|
|
170
|
+
reason,
|
|
171
|
+
});
|
|
172
|
+
return tx
|
|
173
|
+
.select()
|
|
174
|
+
.from(officeAssignments)
|
|
175
|
+
.where(and(eq(officeAssignments.workspaceId, workspaceId), eq(officeAssignments.personId, personId), isNull(officeAssignments.effectiveTo)));
|
|
176
|
+
}
|
|
177
|
+
static toAssignment(row) {
|
|
178
|
+
return { ...row, createdAt: row.createdAt.toISOString() };
|
|
179
|
+
}
|
|
180
|
+
today = todayIso;
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=people.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"people.js","sourceRoot":"","sources":["../../../src/server/services/people.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type Tx } from '@kernhq/kernel';
|
|
2
|
+
import type { PersonResolution, WorkingWeek } from '../../contract/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* What actually applies to a person on a date — and which rung of the ladder answered.
|
|
5
|
+
*
|
|
6
|
+
* ```
|
|
7
|
+
* person override → primary office → org unit → workspace default
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* **This is the only implementation of that order.** Calendars, timezones and (later) accrual,
|
|
11
|
+
* overtime and rounding policies all resolve through here. Four services each growing their own
|
|
12
|
+
* slightly different version of the ladder is the classic way a multi-office HR model rots: they
|
|
13
|
+
* agree for a year, then one of them forgets that a non-primary office does not vote, and a report
|
|
14
|
+
* quietly disagrees with a payslip.
|
|
15
|
+
*
|
|
16
|
+
* Two rules it enforces so nothing else has to:
|
|
17
|
+
*
|
|
18
|
+
* - **Only the primary office decides.** A person may hold several concurrent office assignments —
|
|
19
|
+
* somebody splitting the week between two sites — and the others grant presence, not authority.
|
|
20
|
+
* Otherwise "how many days off do I have" has two answers, which no employee or auditor accepts.
|
|
21
|
+
* - **Everything is asked "on a date".** A person who moved offices in March gets March's answer for
|
|
22
|
+
* a March question and today's for a today question. Resolving against the *current* row is the
|
|
23
|
+
* bug that makes a backdated leave request cost the wrong number of days.
|
|
24
|
+
*/
|
|
25
|
+
export declare class ResolveService {
|
|
26
|
+
/**
|
|
27
|
+
* Resolve for several people at once.
|
|
28
|
+
*
|
|
29
|
+
* Batched deliberately. A report over five hundred people that resolves row by row is five hundred
|
|
30
|
+
* ladder walks, and the ladder is four queries deep — the same shape the tracker's field-value
|
|
31
|
+
* loader had to be rescued from. Callers with one person use `forPerson`, which is this with a
|
|
32
|
+
* single-element array.
|
|
33
|
+
*/
|
|
34
|
+
forPeople(tx: Tx, workspaceId: string, personIds: string[], on?: string): Promise<Map<string, PersonResolution>>;
|
|
35
|
+
forPerson(tx: Tx, workspaceId: string, personId: string, on?: string): Promise<PersonResolution>;
|
|
36
|
+
/**
|
|
37
|
+
* The workspace's default office.
|
|
38
|
+
*
|
|
39
|
+
* Always exists: HR creates it when the module is enabled for a workspace, built from the
|
|
40
|
+
* workspace country. That is what lets the `offices` capability be a *reveal* rather than a
|
|
41
|
+
* migration, and why nothing above has a "no office at all" branch to get wrong.
|
|
42
|
+
*/
|
|
43
|
+
defaultOffice(tx: Tx, workspaceId: string): Promise<{
|
|
44
|
+
id: string;
|
|
45
|
+
workspaceId: string;
|
|
46
|
+
name: string;
|
|
47
|
+
code: string | null;
|
|
48
|
+
kind: string;
|
|
49
|
+
parentOfficeId: string | null;
|
|
50
|
+
legalEntityId: string | null;
|
|
51
|
+
country: string;
|
|
52
|
+
region: string | null;
|
|
53
|
+
city: string | null;
|
|
54
|
+
timezone: string;
|
|
55
|
+
calendarId: string | null;
|
|
56
|
+
address: Record<string, string> | null;
|
|
57
|
+
isDefault: boolean;
|
|
58
|
+
headPersonId: string | null;
|
|
59
|
+
archivedAt: Date | null;
|
|
60
|
+
createdAt: Date;
|
|
61
|
+
updatedAt: Date;
|
|
62
|
+
} | undefined>;
|
|
63
|
+
}
|
|
64
|
+
/** Monday to Friday. Overridden per calendar — Iran's weekend is Friday, the Gulf's Friday and Saturday. */
|
|
65
|
+
export declare const DEFAULT_WORKING_WEEK: WorkingWeek;
|
|
66
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../../src/server/services/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,EAAE,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,KAAK,EAAE,gBAAgB,EAAkB,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAI5F;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,cAAc;IACzB;;;;;;;OAOG;IACG,SAAS,CACb,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EAAE,EACnB,EAAE,GAAE,MAAmB,GACtB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAkGnC,SAAS,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAOtG;;;;;;OAMG;IACG,aAAa,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;CAQhD;AAED,4GAA4G;AAC5G,eAAO,MAAM,oBAAoB,EAAE,WAQlC,CAAA"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { KernError } from '@kernhq/kernel';
|
|
2
|
+
import { and, eq, inArray } from 'drizzle-orm';
|
|
3
|
+
import { calendars, employments, officeAssignments, offices, orgUnits, people } from '../schema.js';
|
|
4
|
+
import { inForceOn, todayIso } from './db.js';
|
|
5
|
+
/**
|
|
6
|
+
* What actually applies to a person on a date — and which rung of the ladder answered.
|
|
7
|
+
*
|
|
8
|
+
* ```
|
|
9
|
+
* person override → primary office → org unit → workspace default
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* **This is the only implementation of that order.** Calendars, timezones and (later) accrual,
|
|
13
|
+
* overtime and rounding policies all resolve through here. Four services each growing their own
|
|
14
|
+
* slightly different version of the ladder is the classic way a multi-office HR model rots: they
|
|
15
|
+
* agree for a year, then one of them forgets that a non-primary office does not vote, and a report
|
|
16
|
+
* quietly disagrees with a payslip.
|
|
17
|
+
*
|
|
18
|
+
* Two rules it enforces so nothing else has to:
|
|
19
|
+
*
|
|
20
|
+
* - **Only the primary office decides.** A person may hold several concurrent office assignments —
|
|
21
|
+
* somebody splitting the week between two sites — and the others grant presence, not authority.
|
|
22
|
+
* Otherwise "how many days off do I have" has two answers, which no employee or auditor accepts.
|
|
23
|
+
* - **Everything is asked "on a date".** A person who moved offices in March gets March's answer for
|
|
24
|
+
* a March question and today's for a today question. Resolving against the *current* row is the
|
|
25
|
+
* bug that makes a backdated leave request cost the wrong number of days.
|
|
26
|
+
*/
|
|
27
|
+
export class ResolveService {
|
|
28
|
+
/**
|
|
29
|
+
* Resolve for several people at once.
|
|
30
|
+
*
|
|
31
|
+
* Batched deliberately. A report over five hundred people that resolves row by row is five hundred
|
|
32
|
+
* ladder walks, and the ladder is four queries deep — the same shape the tracker's field-value
|
|
33
|
+
* loader had to be rescued from. Callers with one person use `forPerson`, which is this with a
|
|
34
|
+
* single-element array.
|
|
35
|
+
*/
|
|
36
|
+
async forPeople(tx, workspaceId, personIds, on = todayIso()) {
|
|
37
|
+
const out = new Map();
|
|
38
|
+
if (!personIds.length)
|
|
39
|
+
return out;
|
|
40
|
+
const personRows = await tx
|
|
41
|
+
.select({ id: people.id, timezone: people.timezone })
|
|
42
|
+
.from(people)
|
|
43
|
+
.where(and(eq(people.workspaceId, workspaceId), inArray(people.id, personIds)));
|
|
44
|
+
const assignments = await tx
|
|
45
|
+
.select()
|
|
46
|
+
.from(officeAssignments)
|
|
47
|
+
.where(and(eq(officeAssignments.workspaceId, workspaceId), inArray(officeAssignments.personId, personIds), inForceOn(officeAssignments.effectiveFrom, officeAssignments.effectiveTo, on)));
|
|
48
|
+
const employmentRows = await tx
|
|
49
|
+
.select()
|
|
50
|
+
.from(employments)
|
|
51
|
+
.where(and(eq(employments.workspaceId, workspaceId), inArray(employments.personId, personIds), inForceOn(employments.effectiveFrom, employments.effectiveTo, on)));
|
|
52
|
+
const officeIds = [...new Set(assignments.map((a) => a.officeId))];
|
|
53
|
+
const officeRows = officeIds.length
|
|
54
|
+
? await tx
|
|
55
|
+
.select()
|
|
56
|
+
.from(offices)
|
|
57
|
+
.where(and(eq(offices.workspaceId, workspaceId), inArray(offices.id, officeIds)))
|
|
58
|
+
: [];
|
|
59
|
+
const officeById = new Map(officeRows.map((o) => [o.id, o]));
|
|
60
|
+
const unitIds = [...new Set(employmentRows.map((e) => e.orgUnitId).filter((x) => !!x))];
|
|
61
|
+
const unitRows = unitIds.length
|
|
62
|
+
? await tx
|
|
63
|
+
.select()
|
|
64
|
+
.from(orgUnits)
|
|
65
|
+
.where(and(eq(orgUnits.workspaceId, workspaceId), inArray(orgUnits.id, unitIds)))
|
|
66
|
+
: [];
|
|
67
|
+
const unitById = new Map(unitRows.map((u) => [u.id, u]));
|
|
68
|
+
const calendarIds = [...new Set(officeRows.map((o) => o.calendarId).filter((x) => !!x))];
|
|
69
|
+
const calendarRows = calendarIds.length
|
|
70
|
+
? await tx
|
|
71
|
+
.select()
|
|
72
|
+
.from(calendars)
|
|
73
|
+
.where(and(eq(calendars.workspaceId, workspaceId), inArray(calendars.id, calendarIds)))
|
|
74
|
+
: [];
|
|
75
|
+
const calendarById = new Map(calendarRows.map((c) => [c.id, c]));
|
|
76
|
+
const defaultOffice = await this.defaultOffice(tx, workspaceId);
|
|
77
|
+
for (const p of personRows) {
|
|
78
|
+
const mine = assignments.filter((a) => a.personId === p.id);
|
|
79
|
+
// Only the primary decides. If a workspace somehow has none on this date — a person created
|
|
80
|
+
// before any assignment, a gap left by a backdated move — the default office answers, which is
|
|
81
|
+
// why there is always exactly one.
|
|
82
|
+
const primaryAssignment = mine.find((a) => a.isPrimary);
|
|
83
|
+
const primary = primaryAssignment ? officeById.get(primaryAssignment.officeId) : undefined;
|
|
84
|
+
const office = primary ?? defaultOffice;
|
|
85
|
+
const employment = employmentRows.find((e) => e.personId === p.id);
|
|
86
|
+
const unit = employment?.orgUnitId ? unitById.get(employment.orgUnitId) : undefined;
|
|
87
|
+
const calendar = office?.calendarId ? calendarById.get(office.calendarId) : undefined;
|
|
88
|
+
const timezoneFrom = p.timezone ? 'person' : 'office';
|
|
89
|
+
out.set(p.id, {
|
|
90
|
+
personId: p.id,
|
|
91
|
+
on,
|
|
92
|
+
primaryOfficeId: office?.id ?? null,
|
|
93
|
+
primaryOfficeName: office?.name ?? null,
|
|
94
|
+
otherOfficeIds: mine.filter((a) => a.officeId !== office?.id).map((a) => a.officeId),
|
|
95
|
+
country: office?.country ?? null,
|
|
96
|
+
// A person override beats the office, for somebody who genuinely works from another zone.
|
|
97
|
+
// Falling back to UTC rather than the server's zone is deliberate: a server's zone is an
|
|
98
|
+
// accident of deployment, and a wrong answer that moves when you redeploy is the worst kind.
|
|
99
|
+
timezone: p.timezone ?? office?.timezone ?? 'UTC',
|
|
100
|
+
timezoneFrom,
|
|
101
|
+
calendarId: calendar?.id ?? null,
|
|
102
|
+
calendarFrom: calendar ? 'office' : null,
|
|
103
|
+
workingWeek: calendar?.workingWeek ?? DEFAULT_WORKING_WEEK,
|
|
104
|
+
legalEntityId: employment?.legalEntityId ?? office?.legalEntityId ?? null,
|
|
105
|
+
orgUnitId: unit?.id ?? null,
|
|
106
|
+
orgUnitPath: unit?.path ?? null,
|
|
107
|
+
managerPersonId: employment?.managerPersonId ?? null,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
async forPerson(tx, workspaceId, personId, on) {
|
|
113
|
+
const map = await this.forPeople(tx, workspaceId, [personId], on);
|
|
114
|
+
const found = map.get(personId);
|
|
115
|
+
if (!found)
|
|
116
|
+
throw KernError.notFound('Person');
|
|
117
|
+
return found;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The workspace's default office.
|
|
121
|
+
*
|
|
122
|
+
* Always exists: HR creates it when the module is enabled for a workspace, built from the
|
|
123
|
+
* workspace country. That is what lets the `offices` capability be a *reveal* rather than a
|
|
124
|
+
* migration, and why nothing above has a "no office at all" branch to get wrong.
|
|
125
|
+
*/
|
|
126
|
+
async defaultOffice(tx, workspaceId) {
|
|
127
|
+
const [row] = await tx
|
|
128
|
+
.select()
|
|
129
|
+
.from(offices)
|
|
130
|
+
.where(and(eq(offices.workspaceId, workspaceId), eq(offices.isDefault, true)))
|
|
131
|
+
.limit(1);
|
|
132
|
+
return row;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Monday to Friday. Overridden per calendar — Iran's weekend is Friday, the Gulf's Friday and Saturday. */
|
|
136
|
+
export const DEFAULT_WORKING_WEEK = {
|
|
137
|
+
mon: 1,
|
|
138
|
+
tue: 1,
|
|
139
|
+
wed: 1,
|
|
140
|
+
thu: 1,
|
|
141
|
+
fri: 1,
|
|
142
|
+
sat: 0,
|
|
143
|
+
sun: 0,
|
|
144
|
+
};
|
|
145
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../src/server/services/resolve.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
CREATE SCHEMA IF NOT EXISTS "mod_hr";
|
|
2
|
+
--> statement-breakpoint
|
|
3
|
+
CREATE TABLE "mod_hr"."calendar_days" (
|
|
4
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
5
|
+
"workspace_id" uuid NOT NULL,
|
|
6
|
+
"calendar_id" uuid NOT NULL,
|
|
7
|
+
"date" date NOT NULL,
|
|
8
|
+
"kind" text DEFAULT 'public_holiday' NOT NULL,
|
|
9
|
+
"name" text NOT NULL,
|
|
10
|
+
"working_fraction" numeric(3, 2) DEFAULT '0' NOT NULL,
|
|
11
|
+
"source" text DEFAULT 'custom' NOT NULL,
|
|
12
|
+
"paid" boolean DEFAULT true NOT NULL,
|
|
13
|
+
"note" text,
|
|
14
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
15
|
+
);
|
|
16
|
+
--> statement-breakpoint
|
|
17
|
+
CREATE TABLE "mod_hr"."calendars" (
|
|
18
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
19
|
+
"workspace_id" uuid NOT NULL,
|
|
20
|
+
"name" text NOT NULL,
|
|
21
|
+
"extends_id" uuid,
|
|
22
|
+
"country" text,
|
|
23
|
+
"region" text,
|
|
24
|
+
"working_week" jsonb DEFAULT '{"mon":1,"tue":1,"wed":1,"thu":1,"fri":1,"sat":0,"sun":0}'::jsonb NOT NULL,
|
|
25
|
+
"source" text DEFAULT 'custom' NOT NULL,
|
|
26
|
+
"pack_key" text,
|
|
27
|
+
"pack_version" text,
|
|
28
|
+
"archived_at" timestamp with time zone,
|
|
29
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
30
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
31
|
+
);
|
|
32
|
+
--> statement-breakpoint
|
|
33
|
+
CREATE TABLE "mod_hr"."cost_centers" (
|
|
34
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
35
|
+
"workspace_id" uuid NOT NULL,
|
|
36
|
+
"code" text NOT NULL,
|
|
37
|
+
"name" text NOT NULL,
|
|
38
|
+
"office_id" uuid,
|
|
39
|
+
"org_unit_id" uuid,
|
|
40
|
+
"legal_entity_id" uuid,
|
|
41
|
+
"archived_at" timestamp with time zone,
|
|
42
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
43
|
+
);
|
|
44
|
+
--> statement-breakpoint
|
|
45
|
+
CREATE TABLE "mod_hr"."custom_field_defs" (
|
|
46
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
47
|
+
"workspace_id" uuid NOT NULL,
|
|
48
|
+
"key" text NOT NULL,
|
|
49
|
+
"name" text NOT NULL,
|
|
50
|
+
"type" text NOT NULL,
|
|
51
|
+
"options" jsonb,
|
|
52
|
+
"required" boolean DEFAULT false NOT NULL,
|
|
53
|
+
"sensitive" boolean DEFAULT false NOT NULL,
|
|
54
|
+
"section" text DEFAULT 'profile' NOT NULL,
|
|
55
|
+
"order" integer DEFAULT 0 NOT NULL,
|
|
56
|
+
"archived_at" timestamp with time zone,
|
|
57
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
58
|
+
);
|
|
59
|
+
--> statement-breakpoint
|
|
60
|
+
CREATE TABLE "mod_hr"."employments" (
|
|
61
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
62
|
+
"workspace_id" uuid NOT NULL,
|
|
63
|
+
"person_id" uuid NOT NULL,
|
|
64
|
+
"effective_from" date NOT NULL,
|
|
65
|
+
"effective_to" date,
|
|
66
|
+
"org_unit_id" uuid,
|
|
67
|
+
"position_id" uuid,
|
|
68
|
+
"legal_entity_id" uuid,
|
|
69
|
+
"cost_center_id" uuid,
|
|
70
|
+
"manager_person_id" uuid,
|
|
71
|
+
"employment_type" text DEFAULT 'full_time' NOT NULL,
|
|
72
|
+
"fte" numeric(4, 3) DEFAULT '1.000' NOT NULL,
|
|
73
|
+
"contract_hours_week" numeric(5, 2),
|
|
74
|
+
"reason" text,
|
|
75
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
76
|
+
);
|
|
77
|
+
--> statement-breakpoint
|
|
78
|
+
CREATE TABLE "mod_hr"."legal_entities" (
|
|
79
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
80
|
+
"workspace_id" uuid NOT NULL,
|
|
81
|
+
"name" text NOT NULL,
|
|
82
|
+
"registration_no" text,
|
|
83
|
+
"tax_no" text,
|
|
84
|
+
"country" text NOT NULL,
|
|
85
|
+
"currency" text,
|
|
86
|
+
"archived_at" timestamp with time zone,
|
|
87
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
88
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
89
|
+
);
|
|
90
|
+
--> statement-breakpoint
|
|
91
|
+
CREATE TABLE "mod_hr"."office_assignments" (
|
|
92
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
93
|
+
"workspace_id" uuid NOT NULL,
|
|
94
|
+
"person_id" uuid NOT NULL,
|
|
95
|
+
"office_id" uuid NOT NULL,
|
|
96
|
+
"is_primary" boolean DEFAULT true NOT NULL,
|
|
97
|
+
"effective_from" date NOT NULL,
|
|
98
|
+
"effective_to" date,
|
|
99
|
+
"reason" text,
|
|
100
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
101
|
+
);
|
|
102
|
+
--> statement-breakpoint
|
|
103
|
+
CREATE TABLE "mod_hr"."offices" (
|
|
104
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
105
|
+
"workspace_id" uuid NOT NULL,
|
|
106
|
+
"name" text NOT NULL,
|
|
107
|
+
"code" text,
|
|
108
|
+
"kind" text DEFAULT 'branch' NOT NULL,
|
|
109
|
+
"parent_office_id" uuid,
|
|
110
|
+
"legal_entity_id" uuid,
|
|
111
|
+
"country" text NOT NULL,
|
|
112
|
+
"region" text,
|
|
113
|
+
"city" text,
|
|
114
|
+
"timezone" text DEFAULT 'UTC' NOT NULL,
|
|
115
|
+
"calendar_id" uuid,
|
|
116
|
+
"address" jsonb,
|
|
117
|
+
"is_default" boolean DEFAULT false NOT NULL,
|
|
118
|
+
"head_person_id" uuid,
|
|
119
|
+
"archived_at" timestamp with time zone,
|
|
120
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
121
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
122
|
+
);
|
|
123
|
+
--> statement-breakpoint
|
|
124
|
+
CREATE TABLE "mod_hr"."org_units" (
|
|
125
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
126
|
+
"workspace_id" uuid NOT NULL,
|
|
127
|
+
"parent_id" uuid,
|
|
128
|
+
"path" "ltree" NOT NULL,
|
|
129
|
+
"name" text NOT NULL,
|
|
130
|
+
"code" text,
|
|
131
|
+
"head_person_id" uuid,
|
|
132
|
+
"archived_at" timestamp with time zone,
|
|
133
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
134
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
135
|
+
);
|
|
136
|
+
--> statement-breakpoint
|
|
137
|
+
CREATE TABLE "mod_hr"."people" (
|
|
138
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
139
|
+
"workspace_id" uuid NOT NULL,
|
|
140
|
+
"user_id" uuid,
|
|
141
|
+
"employee_no" text,
|
|
142
|
+
"display_name" text NOT NULL,
|
|
143
|
+
"work_email" text,
|
|
144
|
+
"personal_email" text,
|
|
145
|
+
"phone" text,
|
|
146
|
+
"photo_file_id" uuid,
|
|
147
|
+
"status" text DEFAULT 'active' NOT NULL,
|
|
148
|
+
"hired_on" date,
|
|
149
|
+
"terminated_on" date,
|
|
150
|
+
"timezone" text,
|
|
151
|
+
"custom" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
152
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
153
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
154
|
+
);
|
|
155
|
+
--> statement-breakpoint
|
|
156
|
+
CREATE TABLE "mod_hr"."people_sensitive" (
|
|
157
|
+
"person_id" uuid PRIMARY KEY NOT NULL,
|
|
158
|
+
"workspace_id" uuid NOT NULL,
|
|
159
|
+
"national_id_enc" text,
|
|
160
|
+
"birth_date" date,
|
|
161
|
+
"iban_enc" text,
|
|
162
|
+
"emergency_contact" jsonb,
|
|
163
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
164
|
+
);
|
|
165
|
+
--> statement-breakpoint
|
|
166
|
+
CREATE TABLE "mod_hr"."person_documents" (
|
|
167
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
168
|
+
"workspace_id" uuid NOT NULL,
|
|
169
|
+
"person_id" uuid NOT NULL,
|
|
170
|
+
"file_id" uuid NOT NULL,
|
|
171
|
+
"name" text NOT NULL,
|
|
172
|
+
"kind" text DEFAULT 'other' NOT NULL,
|
|
173
|
+
"issued_on" date,
|
|
174
|
+
"expires_on" date,
|
|
175
|
+
"uploaded_by" uuid,
|
|
176
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
177
|
+
);
|
|
178
|
+
--> statement-breakpoint
|
|
179
|
+
CREATE TABLE "mod_hr"."person_history" (
|
|
180
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
181
|
+
"workspace_id" uuid NOT NULL,
|
|
182
|
+
"person_id" uuid NOT NULL,
|
|
183
|
+
"field" text NOT NULL,
|
|
184
|
+
"from_value" jsonb,
|
|
185
|
+
"to_value" jsonb,
|
|
186
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
187
|
+
"actor_id" uuid,
|
|
188
|
+
"source" text DEFAULT 'app' NOT NULL
|
|
189
|
+
);
|
|
190
|
+
--> statement-breakpoint
|
|
191
|
+
CREATE TABLE "mod_hr"."positions" (
|
|
192
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
193
|
+
"workspace_id" uuid NOT NULL,
|
|
194
|
+
"title" text NOT NULL,
|
|
195
|
+
"code" text,
|
|
196
|
+
"job_family" text,
|
|
197
|
+
"level" text,
|
|
198
|
+
"archived_at" timestamp with time zone,
|
|
199
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
200
|
+
);
|
|
201
|
+
--> statement-breakpoint
|
|
202
|
+
CREATE INDEX "hr_calendar_days_idx" ON "mod_hr"."calendar_days" USING btree ("workspace_id","calendar_id","date");--> statement-breakpoint
|
|
203
|
+
CREATE UNIQUE INDEX "hr_calendar_days_uq" ON "mod_hr"."calendar_days" USING btree ("calendar_id","date","kind");--> statement-breakpoint
|
|
204
|
+
CREATE INDEX "hr_calendars_ws_idx" ON "mod_hr"."calendars" USING btree ("workspace_id","archived_at");--> statement-breakpoint
|
|
205
|
+
CREATE UNIQUE INDEX "hr_cost_centers_ws_code_uq" ON "mod_hr"."cost_centers" USING btree ("workspace_id","code");--> statement-breakpoint
|
|
206
|
+
CREATE UNIQUE INDEX "hr_fields_ws_key_uq" ON "mod_hr"."custom_field_defs" USING btree ("workspace_id","key");--> statement-breakpoint
|
|
207
|
+
CREATE INDEX "hr_employments_person_idx" ON "mod_hr"."employments" USING btree ("workspace_id","person_id","effective_from");--> statement-breakpoint
|
|
208
|
+
CREATE INDEX "hr_employments_ws_manager_idx" ON "mod_hr"."employments" USING btree ("workspace_id","manager_person_id");--> statement-breakpoint
|
|
209
|
+
CREATE INDEX "hr_employments_ws_unit_idx" ON "mod_hr"."employments" USING btree ("workspace_id","org_unit_id");--> statement-breakpoint
|
|
210
|
+
CREATE INDEX "hr_entities_ws_idx" ON "mod_hr"."legal_entities" USING btree ("workspace_id","archived_at");--> statement-breakpoint
|
|
211
|
+
CREATE INDEX "hr_office_assign_person_idx" ON "mod_hr"."office_assignments" USING btree ("workspace_id","person_id","effective_from");--> statement-breakpoint
|
|
212
|
+
CREATE INDEX "hr_office_assign_office_idx" ON "mod_hr"."office_assignments" USING btree ("workspace_id","office_id","effective_from");--> statement-breakpoint
|
|
213
|
+
CREATE INDEX "hr_offices_ws_idx" ON "mod_hr"."offices" USING btree ("workspace_id","archived_at");--> statement-breakpoint
|
|
214
|
+
CREATE INDEX "hr_offices_ws_country_idx" ON "mod_hr"."offices" USING btree ("workspace_id","country");--> statement-breakpoint
|
|
215
|
+
CREATE INDEX "hr_org_units_ws_idx" ON "mod_hr"."org_units" USING btree ("workspace_id","archived_at");--> statement-breakpoint
|
|
216
|
+
CREATE INDEX "hr_people_ws_status_idx" ON "mod_hr"."people" USING btree ("workspace_id","status","display_name");--> statement-breakpoint
|
|
217
|
+
CREATE UNIQUE INDEX "hr_people_ws_user_uq" ON "mod_hr"."people" USING btree ("workspace_id","user_id");--> statement-breakpoint
|
|
218
|
+
CREATE UNIQUE INDEX "hr_people_ws_empno_uq" ON "mod_hr"."people" USING btree ("workspace_id","employee_no");--> statement-breakpoint
|
|
219
|
+
CREATE INDEX "hr_people_sensitive_ws_idx" ON "mod_hr"."people_sensitive" USING btree ("workspace_id");--> statement-breakpoint
|
|
220
|
+
CREATE INDEX "hr_person_docs_idx" ON "mod_hr"."person_documents" USING btree ("workspace_id","person_id","created_at");--> statement-breakpoint
|
|
221
|
+
CREATE INDEX "hr_person_docs_expiry_idx" ON "mod_hr"."person_documents" USING btree ("workspace_id","expires_on");--> statement-breakpoint
|
|
222
|
+
CREATE INDEX "hr_person_history_idx" ON "mod_hr"."person_history" USING btree ("workspace_id","person_id","created_at");--> statement-breakpoint
|
|
223
|
+
CREATE INDEX "hr_positions_ws_idx" ON "mod_hr"."positions" USING btree ("workspace_id","archived_at");
|