@kernhq/module-hr 0.2.0 → 0.3.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 (56) hide show
  1. package/dist/contract/approvals.d.ts +281 -0
  2. package/dist/contract/approvals.d.ts.map +1 -0
  3. package/dist/contract/approvals.js +127 -0
  4. package/dist/contract/approvals.js.map +1 -0
  5. package/dist/contract/capabilities.d.ts +1 -1
  6. package/dist/contract/capabilities.d.ts.map +1 -1
  7. package/dist/contract/capabilities.js +44 -0
  8. package/dist/contract/capabilities.js.map +1 -1
  9. package/dist/contract/events.d.ts +42 -0
  10. package/dist/contract/events.d.ts.map +1 -1
  11. package/dist/contract/events.js +42 -0
  12. package/dist/contract/events.js.map +1 -1
  13. package/dist/contract/index.d.ts +2 -0
  14. package/dist/contract/index.d.ts.map +1 -1
  15. package/dist/contract/index.js +2 -0
  16. package/dist/contract/index.js.map +1 -1
  17. package/dist/contract/leave.d.ts +168 -0
  18. package/dist/contract/leave.d.ts.map +1 -0
  19. package/dist/contract/leave.js +150 -0
  20. package/dist/contract/leave.js.map +1 -0
  21. package/dist/contract/permissions.d.ts +57 -0
  22. package/dist/contract/permissions.d.ts.map +1 -1
  23. package/dist/contract/permissions.js +69 -0
  24. package/dist/contract/permissions.js.map +1 -1
  25. package/dist/contract/router.d.ts +1446 -0
  26. package/dist/contract/router.d.ts.map +1 -1
  27. package/dist/contract/router.js +235 -0
  28. package/dist/contract/router.js.map +1 -1
  29. package/dist/server/router.d.ts +1646 -58
  30. package/dist/server/router.d.ts.map +1 -1
  31. package/dist/server/router.js +812 -2
  32. package/dist/server/router.js.map +1 -1
  33. package/dist/server/schema.d.ts +2084 -1
  34. package/dist/server/schema.d.ts.map +1 -1
  35. package/dist/server/schema.js +209 -0
  36. package/dist/server/schema.js.map +1 -1
  37. package/dist/server/services/approvals.d.ts +163 -0
  38. package/dist/server/services/approvals.d.ts.map +1 -0
  39. package/dist/server/services/approvals.js +325 -0
  40. package/dist/server/services/approvals.js.map +1 -0
  41. package/dist/server/services/ledger.d.ts +135 -0
  42. package/dist/server/services/ledger.d.ts.map +1 -0
  43. package/dist/server/services/ledger.js +178 -0
  44. package/dist/server/services/ledger.js.map +1 -0
  45. package/dist/server/services/people.d.ts +3 -3
  46. package/migrations/0002_leave.sql +237 -0
  47. package/migrations/meta/0002_snapshot.json +3009 -0
  48. package/migrations/meta/_journal.json +7 -0
  49. package/package.json +1 -1
  50. package/src/contract/approvals.ts +149 -0
  51. package/src/contract/capabilities.ts +44 -0
  52. package/src/contract/events.ts +57 -0
  53. package/src/contract/index.ts +2 -0
  54. package/src/contract/leave.ts +171 -0
  55. package/src/contract/permissions.ts +71 -0
  56. package/src/contract/router.ts +285 -0
@@ -15,6 +15,13 @@
15
15
  "when": 1787567807015,
16
16
  "tag": "0001_rls",
17
17
  "breakpoints": true
18
+ },
19
+ {
20
+ "idx": 2,
21
+ "version": "7",
22
+ "when": 1787601877456,
23
+ "tag": "0002_leave",
24
+ "breakpoints": true
18
25
  }
19
26
  ]
20
27
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kernhq/module-hr",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Kern HR module: people, offices, org chart, calendars",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
@@ -0,0 +1,149 @@
1
+ import { Timestamp, WorkspaceId } from '@kernhq/contracts'
2
+ import { z } from 'zod'
3
+ import { IsoDate } from './models.js'
4
+
5
+ const ws = { workspaceId: WorkspaceId }
6
+
7
+ /**
8
+ * One approval engine, for everything that needs signing off.
9
+ *
10
+ * Keyed by `subjectType` + `subjectId` rather than by a foreign key to leave, so regularization,
11
+ * overtime and timesheets attach to it later without a schema change. That seam is the reason this
12
+ * is not just a few columns on `leave_requests`.
13
+ *
14
+ * The chain is **snapshotted onto the request when it is raised**. Editing the workflow afterwards
15
+ * must not change who has to sign a request already in flight — the version of that mistake where
16
+ * somebody's approved leave silently needs another signature is very hard to explain.
17
+ */
18
+
19
+ export const ApprovalSubjectType = z.enum(['leave', 'regularization', 'overtime', 'timesheet', 'shift_swap'])
20
+ export type ApprovalSubjectType = z.infer<typeof ApprovalSubjectType>
21
+
22
+ /**
23
+ * Who is being asked. Resolved to people when the request is raised, so a later reorganisation does
24
+ * not silently move an in-flight approval to somebody else.
25
+ */
26
+ export const ApproverSubject = z.object({
27
+ kind: z.enum([
28
+ 'person',
29
+ /** The requester's manager on the day the request was raised. */
30
+ 'manager',
31
+ /** Two levels up. Falls back to one level if there is nobody above. */
32
+ 'manager_of_manager',
33
+ /** Whoever heads the requester's department. */
34
+ 'org_unit_head',
35
+ /** Whoever heads the requester's primary office — the local-HR step. */
36
+ 'office_head',
37
+ /** Anybody holding a permission key, workspace-wide. */
38
+ 'permission',
39
+ 'group',
40
+ ]),
41
+ /** Person id, permission key or group id, depending on `kind`. */
42
+ id: z.string().max(128).optional(),
43
+ })
44
+ export type ApproverSubject = z.infer<typeof ApproverSubject>
45
+
46
+ export const ApprovalStepMode = z.enum([
47
+ /** Everyone named must approve. */
48
+ 'all',
49
+ /** Any one of them is enough. */
50
+ 'any',
51
+ /** `minApprovals` of them. */
52
+ 'quorum',
53
+ ])
54
+
55
+ export const ApprovalStepSpec = z.object({
56
+ name: z.string().max(80),
57
+ approvers: z.array(ApproverSubject).min(1),
58
+ mode: ApprovalStepMode,
59
+ minApprovals: z.number().int().min(1),
60
+ /** Hours before the step is escalated or auto-decided. Null means it waits forever. */
61
+ slaHours: z.number().int().min(1).nullable(),
62
+ onTimeout: z.enum(['remind', 'escalate', 'auto_approve']),
63
+ })
64
+ export type ApprovalStepSpec = z.infer<typeof ApprovalStepSpec>
65
+
66
+ /** Steps run in order; the request advances only when the current one is satisfied. */
67
+ export const ApprovalChainSpec = z.object({
68
+ steps: z.array(ApprovalStepSpec).min(1),
69
+ })
70
+ export type ApprovalChainSpec = z.infer<typeof ApprovalChainSpec>
71
+
72
+ export const ApprovalChain = z.object({
73
+ id: z.uuid(),
74
+ ...ws,
75
+ name: z.string().min(1).max(120),
76
+ subjectType: ApprovalSubjectType,
77
+ spec: ApprovalChainSpec,
78
+ /** Used when nothing more specific matches. Exactly one per subject type. */
79
+ isDefault: z.boolean(),
80
+ archivedAt: Timestamp.nullable(),
81
+ })
82
+ export type ApprovalChain = z.infer<typeof ApprovalChain>
83
+
84
+ export const ApprovalStatus = z.enum(['pending', 'approved', 'rejected', 'cancelled'])
85
+ export type ApprovalStatus = z.infer<typeof ApprovalStatus>
86
+
87
+ export const ApprovalDecision = z.object({
88
+ id: z.uuid(),
89
+ stepId: z.uuid(),
90
+ approverId: z.uuid(),
91
+ /** Set when somebody decided in another person's place through a delegation. */
92
+ onBehalfOfId: z.uuid().nullable(),
93
+ decision: z.enum(['approve', 'reject']),
94
+ comment: z.string().max(1000).nullable(),
95
+ at: Timestamp,
96
+ })
97
+ export type ApprovalDecision = z.infer<typeof ApprovalDecision>
98
+
99
+ export const ApprovalStep = z.object({
100
+ id: z.uuid(),
101
+ requestId: z.uuid(),
102
+ stepIndex: z.number().int(),
103
+ name: z.string(),
104
+ mode: ApprovalStepMode,
105
+ minApprovals: z.number().int(),
106
+ /** Expanded at request time; a later reorganisation does not move an in-flight approval. */
107
+ approverIds: z.array(z.uuid()),
108
+ status: ApprovalStatus,
109
+ dueAt: Timestamp.nullable(),
110
+ escalatedAt: Timestamp.nullable(),
111
+ decisions: z.array(ApprovalDecision),
112
+ })
113
+ export type ApprovalStep = z.infer<typeof ApprovalStep>
114
+
115
+ export const ApprovalRequest = z.object({
116
+ id: z.uuid(),
117
+ ...ws,
118
+ subjectType: ApprovalSubjectType,
119
+ subjectId: z.uuid(),
120
+ /** A one-line description of what is being approved, so an inbox is readable without joins. */
121
+ summary: z.string().max(200),
122
+ status: ApprovalStatus,
123
+ currentStep: z.number().int(),
124
+ requestedBy: z.uuid().nullable(),
125
+ requestedAt: Timestamp,
126
+ decidedAt: Timestamp.nullable(),
127
+ steps: z.array(ApprovalStep),
128
+ })
129
+ export type ApprovalRequest = z.infer<typeof ApprovalRequest>
130
+
131
+ /**
132
+ * Somebody else may decide in my place while I am away.
133
+ *
134
+ * A delegation does not move the request; it lets the delegate act on it, and the decision records
135
+ * both people. "Who approved this" must never become ambiguous.
136
+ */
137
+ export const Delegation = z.object({
138
+ id: z.uuid(),
139
+ ...ws,
140
+ fromPersonId: z.uuid(),
141
+ toPersonId: z.uuid(),
142
+ /** Null delegates every subject type. */
143
+ subjectType: ApprovalSubjectType.nullable(),
144
+ startsOn: IsoDate,
145
+ endsOn: IsoDate,
146
+ reason: z.string().max(200).nullable(),
147
+ createdAt: Timestamp,
148
+ })
149
+ export type Delegation = z.infer<typeof Delegation>
@@ -64,6 +64,26 @@ export const hrCapabilities = defineCapabilities([
64
64
  defaultEnabled: true,
65
65
  level: 1,
66
66
  },
67
+ {
68
+ id: 'leave',
69
+ label: 'Leave',
70
+ description: 'Time off: types, balances, requests and approvals',
71
+ dependsOn: ['core', 'calendars'],
72
+ // On by default. Leave is what most companies come to an HR system for, and a directory that
73
+ // cannot answer "who is off next week" is answering a question nobody asked.
74
+ defaultEnabled: true,
75
+ level: 1,
76
+ },
77
+ {
78
+ id: 'approvals',
79
+ label: 'Approval chains',
80
+ description: 'Named multi-step approvals with delegation, instead of a single manager',
81
+ dependsOn: ['core'],
82
+ // Off by default: at Level 1 the requester's manager approves, implicitly, and a company with
83
+ // one approver does not need a chain editor to find out that it has one.
84
+ defaultEnabled: false,
85
+ level: 2,
86
+ },
67
87
  {
68
88
  id: 'documents',
69
89
  label: 'Employee documents',
@@ -114,4 +134,28 @@ export const hrCapabilityProcedures: Record<string, readonly string[]> = {
114
134
  'calendars.workingDays',
115
135
  ],
116
136
  documents: ['documents.list', 'documents.attach', 'documents.remove'],
137
+ leave: [
138
+ 'leave.types.list',
139
+ 'leave.types.create',
140
+ 'leave.types.update',
141
+ 'leave.types.archive',
142
+ 'leave.balance.get',
143
+ 'leave.ledger.list',
144
+ 'leave.adjust',
145
+ 'leave.requests.list',
146
+ 'leave.requests.get',
147
+ 'leave.requests.simulate',
148
+ 'leave.requests.create',
149
+ 'leave.requests.cancel',
150
+ 'leave.team.calendar',
151
+ ],
152
+ approvals: [
153
+ 'approvals.chains.list',
154
+ 'approvals.chains.create',
155
+ 'approvals.chains.update',
156
+ 'approvals.chains.archive',
157
+ 'approvals.delegate',
158
+ 'approvals.revokeDelegation',
159
+ 'approvals.delegations',
160
+ ],
117
161
  }
@@ -72,6 +72,63 @@ export const hrEvents = {
72
72
  * sheet) is stale from here. The payload names the date range touched so a consumer can recompute
73
73
  * that window rather than everything.
74
74
  */
75
+ leaveRequested: defineEvent(
76
+ 'hr.leave.requested',
77
+ z.object({
78
+ requestId: z.uuid(),
79
+ workspaceId: WorkspaceId,
80
+ personId: z.uuid(),
81
+ startsOn: z.iso.date(),
82
+ endsOn: z.iso.date(),
83
+ }),
84
+ ),
85
+ /**
86
+ * Decided either way, with the outcome in the payload.
87
+ *
88
+ * One event rather than approved/rejected pairs: every consumer so far cares that a decision
89
+ * happened and then branches, and two events means two subscriptions to keep in step.
90
+ */
91
+ leaveDecided: defineEvent(
92
+ 'hr.leave.decided',
93
+ z.object({
94
+ requestId: z.uuid(),
95
+ workspaceId: WorkspaceId,
96
+ personId: z.uuid(),
97
+ status: z.string(),
98
+ startsOn: z.iso.date(),
99
+ endsOn: z.iso.date(),
100
+ }),
101
+ ),
102
+ /** A balance moved. Carries the delta so a consumer need not re-sum the ledger. */
103
+ leaveBalanceChanged: defineEvent(
104
+ 'hr.leave.balance_changed',
105
+ z.object({
106
+ workspaceId: WorkspaceId,
107
+ personId: z.uuid(),
108
+ leaveTypeId: z.uuid(),
109
+ deltaMinutes: z.number().int(),
110
+ }),
111
+ ),
112
+ approvalRequested: defineEvent(
113
+ 'hr.approval.requested',
114
+ z.object({
115
+ requestId: z.uuid(),
116
+ workspaceId: WorkspaceId,
117
+ subjectType: z.string(),
118
+ subjectId: z.uuid(),
119
+ approverIds: z.array(z.uuid()),
120
+ }),
121
+ ),
122
+ approvalDecided: defineEvent(
123
+ 'hr.approval.decided',
124
+ z.object({
125
+ requestId: z.uuid(),
126
+ workspaceId: WorkspaceId,
127
+ subjectType: z.string(),
128
+ subjectId: z.uuid(),
129
+ status: z.string(),
130
+ }),
131
+ ),
75
132
  calendarChanged: defineEvent(
76
133
  'hr.calendar.changed',
77
134
  z.object({
@@ -6,8 +6,10 @@
6
6
  * exists here and not in the router is a lie that compiles. `module.test.ts` checks exactly that,
7
7
  * and also that every procedure listed in `hrCapabilityProcedures` carries its capability guard.
8
8
  */
9
+ export * from './approvals.js'
9
10
  export * from './capabilities.js'
10
11
  export * from './events.js'
12
+ export * from './leave.js'
11
13
  export * from './models.js'
12
14
  export * from './permissions.js'
13
15
  export * from './router.js'
@@ -0,0 +1,171 @@
1
+ import { Timestamp, WorkspaceId } from '@kernhq/contracts'
2
+ import { z } from 'zod'
3
+ import { IsoDate } from './models.js'
4
+
5
+ const ws = { workspaceId: WorkspaceId }
6
+
7
+ /**
8
+ * Leave, and the one rule everything here follows: **a balance is a sum, never a stored number.**
9
+ *
10
+ * Every grant, accrual, consumption, reversal, expiry and adjustment is an append-only ledger entry.
11
+ * Cancelling approved leave inserts a reversal; it does not delete the consumption. That costs a
12
+ * little arithmetic and buys the only thing that matters when an employee and HR disagree about a
13
+ * balance: a list of what happened, in order, that nobody edited.
14
+ */
15
+
16
+ export const LeaveUnit = z.enum(['day', 'half_day', 'hour'])
17
+ export type LeaveUnit = z.infer<typeof LeaveUnit>
18
+
19
+ export const LeaveType = z.object({
20
+ id: z.uuid(),
21
+ ...ws,
22
+ key: z
23
+ .string()
24
+ .min(1)
25
+ .max(48)
26
+ .regex(/^[a-z][a-z0-9_]*$/),
27
+ name: z.string().min(1).max(120),
28
+ /** Unpaid leave still consumes calendar days; it just does not cost money. */
29
+ paid: z.boolean(),
30
+ unit: LeaveUnit,
31
+ color: z.string().max(32).nullable(),
32
+ icon: z.string().max(48).nullable(),
33
+ /** A sick note after N consecutive days. Null means never. */
34
+ requiresDocumentAfterDays: z.number().int().min(1).nullable(),
35
+ /** Weekends and public holidays inside a request do not consume balance. Almost always true. */
36
+ countsWorkingDaysOnly: z.boolean(),
37
+ allowNegative: z.boolean(),
38
+ /** How far below zero, in minutes. Only consulted when `allowNegative`. */
39
+ maxNegativeMinutes: z.number().int().min(0),
40
+ /** Sort order in pickers. */
41
+ order: z.number().int(),
42
+ archivedAt: Timestamp.nullable(),
43
+ })
44
+ export type LeaveType = z.infer<typeof LeaveType>
45
+
46
+ /**
47
+ * Why a ledger entry exists. The set is closed on purpose — a new kind of movement is a decision,
48
+ * not a string somebody passes in.
49
+ */
50
+ export const LedgerKind = z.enum([
51
+ /** An allowance handed out: the annual entitlement, a one-off award. */
52
+ 'grant',
53
+ /** Earned over time by an accrual policy. */
54
+ 'accrual',
55
+ /** Spent by an approved request. Negative. */
56
+ 'consumption',
57
+ /** Undoes an earlier entry, cancellation or a retroactive correction. Points at what it reverses. */
58
+ 'reversal',
59
+ /** Unused balance lapsing at a carry-forward deadline. Negative. */
60
+ 'expiry',
61
+ /** A human decided. Always carries a reason. */
62
+ 'adjustment',
63
+ 'carry_in',
64
+ 'carry_out',
65
+ /** Paid out instead of taken. Negative. */
66
+ 'encashment',
67
+ ])
68
+ export type LedgerKind = z.infer<typeof LedgerKind>
69
+
70
+ export const LeaveLedgerEntry = z.object({
71
+ id: z.uuid(),
72
+ ...ws,
73
+ personId: z.uuid(),
74
+ leaveTypeId: z.uuid(),
75
+ kind: LedgerKind,
76
+ /**
77
+ * Signed, in minutes. Minutes rather than days because half-days, hourly leave and part-time
78
+ * fractions all divide a day, and a decimal day accumulates rounding error across a year of them.
79
+ */
80
+ amountMinutes: z.number().int(),
81
+ effectiveOn: IsoDate,
82
+ /** Which entitlement year this belongs to. Carry-forward and expiry work on it. */
83
+ periodYear: z.number().int(),
84
+ requestId: z.uuid().nullable(),
85
+ reversesEntryId: z.uuid().nullable(),
86
+ /** Which policy version produced it, so a recomputation can tell what it was computed with. */
87
+ policyHash: z.string().max(64).nullable(),
88
+ reason: z.string().max(500).nullable(),
89
+ createdBy: z.uuid().nullable(),
90
+ createdAt: Timestamp,
91
+ })
92
+ export type LeaveLedgerEntry = z.infer<typeof LeaveLedgerEntry>
93
+
94
+ export const LeaveBalance = z.object({
95
+ personId: z.uuid(),
96
+ leaveTypeId: z.uuid(),
97
+ leaveTypeName: z.string(),
98
+ unit: LeaveUnit,
99
+ periodYear: z.number().int(),
100
+ /** The sum of every entry. Minutes. */
101
+ balanceMinutes: z.number().int(),
102
+ /** Approved but not yet taken — already spent, shown separately so "remaining" is not a surprise. */
103
+ bookedMinutes: z.number().int(),
104
+ /** Submitted and not yet decided. Not spent, but not available either. */
105
+ pendingMinutes: z.number().int(),
106
+ /** `balance - pending`. What a person can actually request today without going negative. */
107
+ availableMinutes: z.number().int(),
108
+ /** The same figures in whatever unit the type uses, for display. */
109
+ balance: z.number(),
110
+ available: z.number(),
111
+ })
112
+ export type LeaveBalance = z.infer<typeof LeaveBalance>
113
+
114
+ export const LeaveRequestStatus = z.enum([
115
+ 'draft',
116
+ 'pending',
117
+ 'approved',
118
+ 'rejected',
119
+ 'cancelled',
120
+ /** Approved, then cancelled or corrected afterwards. The ledger carries a reversal. */
121
+ 'withdrawn',
122
+ ])
123
+ export type LeaveRequestStatus = z.infer<typeof LeaveRequestStatus>
124
+
125
+ /** Which half of a day a request starts or ends on. */
126
+ export const DayPart = z.enum(['full', 'morning', 'afternoon'])
127
+ export type DayPart = z.infer<typeof DayPart>
128
+
129
+ export const LeaveRequest = z.object({
130
+ id: z.uuid(),
131
+ ...ws,
132
+ personId: z.uuid(),
133
+ leaveTypeId: z.uuid(),
134
+ startsOn: IsoDate,
135
+ endsOn: IsoDate,
136
+ startPart: DayPart,
137
+ endPart: DayPart,
138
+ /** For hourly leave. Null for day-based types. */
139
+ hours: z.number().min(0).max(24).nullable(),
140
+ /** Working days consumed, after the calendar is applied. Recomputed on approval. */
141
+ workingDays: z.number(),
142
+ minutes: z.number().int(),
143
+ status: LeaveRequestStatus,
144
+ reason: z.string().max(1000).nullable(),
145
+ documentFileId: z.uuid().nullable(),
146
+ approvalRequestId: z.uuid().nullable(),
147
+ decidedAt: Timestamp.nullable(),
148
+ createdAt: Timestamp,
149
+ updatedAt: Timestamp,
150
+ })
151
+ export type LeaveRequest = z.infer<typeof LeaveRequest>
152
+
153
+ /** What a request would cost, before anybody submits it. */
154
+ export const LeaveSimulation = z.object({
155
+ workingDays: z.number(),
156
+ minutes: z.number().int(),
157
+ /** Day by day, so somebody can see *why* a five-day request costs three. */
158
+ days: z.array(
159
+ z.object({
160
+ date: IsoDate,
161
+ fraction: z.number(),
162
+ counted: z.boolean(),
163
+ reason: z.string().nullable(),
164
+ }),
165
+ ),
166
+ balanceBeforeMinutes: z.number().int(),
167
+ balanceAfterMinutes: z.number().int(),
168
+ /** Reasons this would be refused if submitted. Empty means it would go through. */
169
+ blockers: z.array(z.object({ code: z.string(), message: z.string() })),
170
+ })
171
+ export type LeaveSimulation = z.infer<typeof LeaveSimulation>
@@ -181,6 +181,69 @@ export const hrPermissions = definePermissions([
181
181
  dangerous: true,
182
182
  },
183
183
 
184
+ // ---------------------------------------------------------------- leave
185
+ {
186
+ key: 'hr.leave.request',
187
+ label: 'Request time off',
188
+ // Everybody. A permission an employee cannot lack is noise, but this one is genuinely revocable
189
+ // — a contractor who books time off through their agency should not have the button.
190
+ scope: 'workspace',
191
+ defaultRoles: ['owner', 'admin', 'member'],
192
+ dangerous: false,
193
+ },
194
+ {
195
+ key: 'hr.leave.view',
196
+ label: 'View leave types and your own balance',
197
+ scope: 'workspace',
198
+ defaultRoles: ['owner', 'admin', 'member'],
199
+ dangerous: false,
200
+ },
201
+ {
202
+ key: 'hr.leave.view_team',
203
+ label: "View your team's leave and balances",
204
+ scope: 'object',
205
+ defaultRoles: ['owner', 'admin'],
206
+ dangerous: false,
207
+ },
208
+ {
209
+ key: 'hr.leave.view_ledger',
210
+ label: "View the movements behind somebody's balance",
211
+ scope: 'workspace',
212
+ defaultRoles: ['owner', 'admin'],
213
+ dangerous: false,
214
+ },
215
+ {
216
+ key: 'hr.leave.manage',
217
+ label: 'Configure leave types',
218
+ scope: 'workspace',
219
+ defaultRoles: ['owner', 'admin'],
220
+ dangerous: false,
221
+ },
222
+ {
223
+ key: 'hr.leave.adjust',
224
+ label: "Change somebody's balance by hand",
225
+ description: 'Adds or removes leave directly. Every adjustment is recorded with its reason.',
226
+ scope: 'workspace',
227
+ defaultRoles: [],
228
+ dangerous: true,
229
+ },
230
+
231
+ // ---------------------------------------------------------------- approvals
232
+ {
233
+ key: 'hr.approval.manage',
234
+ label: 'Configure approval chains',
235
+ scope: 'workspace',
236
+ defaultRoles: ['owner', 'admin'],
237
+ dangerous: false,
238
+ },
239
+ {
240
+ key: 'hr.approval.delegate',
241
+ label: 'Hand your approvals to somebody else while away',
242
+ scope: 'workspace',
243
+ defaultRoles: ['owner', 'admin', 'member'],
244
+ dangerous: false,
245
+ },
246
+
184
247
  // ---------------------------------------------------------------- fields
185
248
  {
186
249
  key: 'hr.field.manage',
@@ -214,4 +277,12 @@ export const HR_PERMISSIONS = {
214
277
  documentView: 'hr.document.view',
215
278
  documentManage: 'hr.document.manage',
216
279
  fieldManage: 'hr.field.manage',
280
+ leaveRequest: 'hr.leave.request',
281
+ leaveView: 'hr.leave.view',
282
+ leaveViewTeam: 'hr.leave.view_team',
283
+ leaveViewLedger: 'hr.leave.view_ledger',
284
+ leaveManage: 'hr.leave.manage',
285
+ leaveAdjust: 'hr.leave.adjust',
286
+ approvalManage: 'hr.approval.manage',
287
+ approvalDelegate: 'hr.approval.delegate',
217
288
  } as const