@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.
Files changed (75) hide show
  1. package/LICENSE +662 -0
  2. package/README.md +71 -0
  3. package/dist/contract/capabilities.d.ts +45 -0
  4. package/dist/contract/capabilities.d.ts.map +1 -0
  5. package/dist/contract/capabilities.js +114 -0
  6. package/dist/contract/capabilities.js.map +1 -0
  7. package/dist/contract/events.d.ts +74 -0
  8. package/dist/contract/events.d.ts.map +1 -0
  9. package/dist/contract/events.js +63 -0
  10. package/dist/contract/events.js.map +1 -0
  11. package/dist/contract/index.d.ts +15 -0
  12. package/dist/contract/index.d.ts.map +1 -0
  13. package/dist/contract/index.js +15 -0
  14. package/dist/contract/index.js.map +1 -0
  15. package/dist/contract/models.d.ts +457 -0
  16. package/dist/contract/models.d.ts.map +1 -0
  17. package/dist/contract/models.js +381 -0
  18. package/dist/contract/models.js.map +1 -0
  19. package/dist/contract/permissions.d.ts +174 -0
  20. package/dist/contract/permissions.d.ts.map +1 -0
  21. package/dist/contract/permissions.js +209 -0
  22. package/dist/contract/permissions.js.map +1 -0
  23. package/dist/contract/router.d.ts +2720 -0
  24. package/dist/contract/router.d.ts.map +1 -0
  25. package/dist/contract/router.js +520 -0
  26. package/dist/contract/router.js.map +1 -0
  27. package/dist/contract/settings.d.ts +20 -0
  28. package/dist/contract/settings.d.ts.map +1 -0
  29. package/dist/contract/settings.js +34 -0
  30. package/dist/contract/settings.js.map +1 -0
  31. package/dist/policy/calendar.d.ts +64 -0
  32. package/dist/policy/calendar.d.ts.map +1 -0
  33. package/dist/policy/calendar.js +113 -0
  34. package/dist/policy/calendar.js.map +1 -0
  35. package/dist/server/index.d.ts +8 -0
  36. package/dist/server/index.d.ts.map +1 -0
  37. package/dist/server/index.js +141 -0
  38. package/dist/server/index.js.map +1 -0
  39. package/dist/server/packs/index.d.ts +50 -0
  40. package/dist/server/packs/index.d.ts.map +1 -0
  41. package/dist/server/packs/index.js +121 -0
  42. package/dist/server/packs/index.js.map +1 -0
  43. package/dist/server/router.d.ts +3095 -0
  44. package/dist/server/router.d.ts.map +1 -0
  45. package/dist/server/router.js +1537 -0
  46. package/dist/server/router.js.map +1 -0
  47. package/dist/server/schema.d.ts +2828 -0
  48. package/dist/server/schema.d.ts.map +1 -0
  49. package/dist/server/schema.js +313 -0
  50. package/dist/server/schema.js.map +1 -0
  51. package/dist/server/services/db.d.ts +22 -0
  52. package/dist/server/services/db.d.ts.map +1 -0
  53. package/dist/server/services/db.js +23 -0
  54. package/dist/server/services/db.js.map +1 -0
  55. package/dist/server/services/people.d.ts +110 -0
  56. package/dist/server/services/people.d.ts.map +1 -0
  57. package/dist/server/services/people.js +182 -0
  58. package/dist/server/services/people.js.map +1 -0
  59. package/dist/server/services/resolve.d.ts +66 -0
  60. package/dist/server/services/resolve.d.ts.map +1 -0
  61. package/dist/server/services/resolve.js +145 -0
  62. package/dist/server/services/resolve.js.map +1 -0
  63. package/migrations/0000_init.sql +223 -0
  64. package/migrations/0001_rls.sql +141 -0
  65. package/migrations/meta/0000_snapshot.json +1715 -0
  66. package/migrations/meta/_journal.json +20 -0
  67. package/package.json +59 -0
  68. package/src/client/index.ts +62 -0
  69. package/src/contract/capabilities.ts +117 -0
  70. package/src/contract/events.ts +84 -0
  71. package/src/contract/index.ts +14 -0
  72. package/src/contract/models.ts +437 -0
  73. package/src/contract/permissions.ts +217 -0
  74. package/src/contract/router.ts +611 -0
  75. 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");