@kernhq/module-hr 0.2.0 → 0.4.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/dist/contract/approvals.d.ts +281 -0
- package/dist/contract/approvals.d.ts.map +1 -0
- package/dist/contract/approvals.js +127 -0
- package/dist/contract/approvals.js.map +1 -0
- package/dist/contract/attendance.d.ts +240 -0
- package/dist/contract/attendance.d.ts.map +1 -0
- package/dist/contract/attendance.js +154 -0
- package/dist/contract/attendance.js.map +1 -0
- package/dist/contract/capabilities.d.ts +1 -1
- package/dist/contract/capabilities.d.ts.map +1 -1
- package/dist/contract/capabilities.js +80 -0
- package/dist/contract/capabilities.js.map +1 -1
- package/dist/contract/events.d.ts +60 -0
- package/dist/contract/events.d.ts.map +1 -1
- package/dist/contract/events.js +60 -0
- package/dist/contract/events.js.map +1 -1
- package/dist/contract/index.d.ts +3 -0
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +3 -0
- package/dist/contract/index.js.map +1 -1
- package/dist/contract/leave.d.ts +168 -0
- package/dist/contract/leave.d.ts.map +1 -0
- package/dist/contract/leave.js +150 -0
- package/dist/contract/leave.js.map +1 -0
- package/dist/contract/permissions.d.ts +100 -0
- package/dist/contract/permissions.d.ts.map +1 -1
- package/dist/contract/permissions.js +119 -0
- package/dist/contract/permissions.js.map +1 -1
- package/dist/contract/router.d.ts +2440 -0
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/contract/router.js +393 -0
- package/dist/contract/router.js.map +1 -1
- package/dist/policy/time.d.ts +48 -0
- package/dist/policy/time.d.ts.map +1 -0
- package/dist/policy/time.js +144 -0
- package/dist/policy/time.js.map +1 -0
- package/dist/policy/working-time.d.ts +98 -0
- package/dist/policy/working-time.d.ts.map +1 -0
- package/dist/policy/working-time.js +190 -0
- package/dist/policy/working-time.js.map +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/jobs.d.ts +16 -0
- package/dist/server/jobs.d.ts.map +1 -0
- package/dist/server/jobs.js +166 -0
- package/dist/server/jobs.js.map +1 -0
- package/dist/server/router.d.ts +2731 -58
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/router.js +1306 -3
- package/dist/server/router.js.map +1 -1
- package/dist/server/schema.d.ts +3344 -1
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +325 -0
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/approvals.d.ts +163 -0
- package/dist/server/services/approvals.d.ts.map +1 -0
- package/dist/server/services/approvals.js +325 -0
- package/dist/server/services/approvals.js.map +1 -0
- package/dist/server/services/attendance.d.ts +150 -0
- package/dist/server/services/attendance.d.ts.map +1 -0
- package/dist/server/services/attendance.js +239 -0
- package/dist/server/services/attendance.js.map +1 -0
- package/dist/server/services/ledger.d.ts +135 -0
- package/dist/server/services/ledger.d.ts.map +1 -0
- package/dist/server/services/ledger.js +178 -0
- package/dist/server/services/ledger.js.map +1 -0
- package/dist/server/services/people.d.ts +3 -3
- package/migrations/0002_leave.sql +237 -0
- package/migrations/0003_attendance.sql +206 -0
- package/migrations/meta/0002_snapshot.json +3009 -0
- package/migrations/meta/0003_snapshot.json +3681 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +1 -1
- package/src/contract/approvals.ts +149 -0
- package/src/contract/attendance.ts +180 -0
- package/src/contract/capabilities.ts +80 -0
- package/src/contract/events.ts +81 -0
- package/src/contract/index.ts +3 -0
- package/src/contract/leave.ts +171 -0
- package/src/contract/permissions.ts +122 -0
- package/src/contract/router.ts +481 -0
|
@@ -15,6 +15,20 @@
|
|
|
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
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"idx": 3,
|
|
28
|
+
"version": "7",
|
|
29
|
+
"when": 1787634652573,
|
|
30
|
+
"tag": "0003_attendance",
|
|
31
|
+
"breakpoints": true
|
|
18
32
|
}
|
|
19
33
|
]
|
|
20
34
|
}
|
package/package.json
CHANGED
|
@@ -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>
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { Timestamp, WorkspaceId } from '@kernhq/contracts'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { IsoDate, TimeZone, WallClock } from './models.js'
|
|
4
|
+
|
|
5
|
+
const ws = { workspaceId: WorkspaceId }
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Attendance: who was at work, when, and for how long.
|
|
9
|
+
*
|
|
10
|
+
* Two rules shape everything here:
|
|
11
|
+
*
|
|
12
|
+
* - **Raw punches are immutable.** A wrong punch is *voided* by a correcting row, never edited. An
|
|
13
|
+
* attendance record somebody can quietly rewrite is worth nothing in the dispute it exists for.
|
|
14
|
+
* - **The day sheet is derived.** `AttendanceDay` is a projection of punches + schedule + calendar +
|
|
15
|
+
* leave, recomputable from scratch at any time. It is never the source of truth, so a bad
|
|
16
|
+
* computation is a bug to fix and re-run rather than data to repair by hand.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const PunchDirection = z.enum(['in', 'out', 'break_start', 'break_end'])
|
|
20
|
+
export type PunchDirection = z.infer<typeof PunchDirection>
|
|
21
|
+
|
|
22
|
+
export const PunchMethod = z.enum(['web', 'mobile', 'kiosk', 'qr', 'device', 'import', 'manual'])
|
|
23
|
+
export type PunchMethod = z.infer<typeof PunchMethod>
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* How much the recorded instant can be trusted.
|
|
27
|
+
*
|
|
28
|
+
* `trusted` was stamped by the server as it happened. `claimed` came from a client that was offline
|
|
29
|
+
* and is asserting when it happened — believed within a policy threshold, flagged beyond it.
|
|
30
|
+
* `disputed` is a claim somebody has to look at. Client clocks lie, sometimes by accident and
|
|
31
|
+
* sometimes not, and a system that cannot say which is which cannot defend any of its numbers.
|
|
32
|
+
*/
|
|
33
|
+
export const PunchTrust = z.enum(['trusted', 'claimed', 'disputed'])
|
|
34
|
+
export type PunchTrust = z.infer<typeof PunchTrust>
|
|
35
|
+
|
|
36
|
+
export const Punch = z.object({
|
|
37
|
+
id: z.uuid(),
|
|
38
|
+
...ws,
|
|
39
|
+
personId: z.uuid(),
|
|
40
|
+
direction: PunchDirection,
|
|
41
|
+
/** Server-stamped, or the client's claim when it was offline. Always an instant, never a local time. */
|
|
42
|
+
at: Timestamp,
|
|
43
|
+
/** What the client believed the time was. Kept for audit even when it disagrees. */
|
|
44
|
+
clientReportedAt: Timestamp.nullable(),
|
|
45
|
+
/** How far the client's clock was out, in milliseconds. Null when the server stamped it. */
|
|
46
|
+
skewMs: z.number().int().nullable(),
|
|
47
|
+
/** The day this belongs to, in the person's schedule zone. A night shift lands on its start date. */
|
|
48
|
+
businessDate: IsoDate,
|
|
49
|
+
/** The zone the punch actually happened in — for audit, not for attribution. */
|
|
50
|
+
timezone: TimeZone,
|
|
51
|
+
method: PunchMethod,
|
|
52
|
+
/** The office whose geofence or allowlist accepted it; any office the person is assigned to. */
|
|
53
|
+
officeId: z.uuid().nullable(),
|
|
54
|
+
deviceId: z.uuid().nullable(),
|
|
55
|
+
geo: z.object({ lat: z.number(), lng: z.number(), accuracyM: z.number().optional() }).nullable(),
|
|
56
|
+
trust: PunchTrust,
|
|
57
|
+
/** Set when a later correction voided this row. The row itself never changes otherwise. */
|
|
58
|
+
voidedByPunchId: z.uuid().nullable(),
|
|
59
|
+
note: z.string().max(500).nullable(),
|
|
60
|
+
createdAt: Timestamp,
|
|
61
|
+
})
|
|
62
|
+
export type Punch = z.infer<typeof Punch>
|
|
63
|
+
|
|
64
|
+
export const AttendanceStatus = z.enum([
|
|
65
|
+
'present',
|
|
66
|
+
'absent',
|
|
67
|
+
'leave',
|
|
68
|
+
'holiday',
|
|
69
|
+
'weekend',
|
|
70
|
+
'partial',
|
|
71
|
+
/** Something is unpaired — usually a missing clock-out. Needs a human. */
|
|
72
|
+
'pending',
|
|
73
|
+
])
|
|
74
|
+
export type AttendanceStatus = z.infer<typeof AttendanceStatus>
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* One person, one day, derived.
|
|
78
|
+
*
|
|
79
|
+
* Recomputable from punches, schedule, calendar and leave at any moment. `policyHash` records what
|
|
80
|
+
* produced it, so a recomputation can tell whether a row is stale; `locked` mirrors the period, so
|
|
81
|
+
* a closed month cannot silently move.
|
|
82
|
+
*/
|
|
83
|
+
export const AttendanceDay = z.object({
|
|
84
|
+
id: z.uuid(),
|
|
85
|
+
...ws,
|
|
86
|
+
personId: z.uuid(),
|
|
87
|
+
businessDate: IsoDate,
|
|
88
|
+
scheduledMinutes: z.number().int(),
|
|
89
|
+
workedMinutes: z.number().int(),
|
|
90
|
+
breakMinutes: z.number().int(),
|
|
91
|
+
overtimeMinutes: z.number().int(),
|
|
92
|
+
lateMinutes: z.number().int(),
|
|
93
|
+
earlyLeaveMinutes: z.number().int(),
|
|
94
|
+
status: AttendanceStatus,
|
|
95
|
+
leaveRequestId: z.uuid().nullable(),
|
|
96
|
+
anomalies: z.array(z.string()),
|
|
97
|
+
firstIn: Timestamp.nullable(),
|
|
98
|
+
lastOut: Timestamp.nullable(),
|
|
99
|
+
policyHash: z.string().max(64).nullable(),
|
|
100
|
+
locked: z.boolean(),
|
|
101
|
+
computedAt: Timestamp,
|
|
102
|
+
})
|
|
103
|
+
export type AttendanceDay = z.infer<typeof AttendanceDay>
|
|
104
|
+
|
|
105
|
+
/** A week of shifts, in wall-clock readings. Meaningless until a date and a zone arrive. */
|
|
106
|
+
export const ScheduleWeek = z.record(
|
|
107
|
+
z.enum(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']),
|
|
108
|
+
z
|
|
109
|
+
.object({
|
|
110
|
+
start: WallClock,
|
|
111
|
+
end: WallClock,
|
|
112
|
+
breakMinutes: z.number().int().min(0).max(480).default(0),
|
|
113
|
+
})
|
|
114
|
+
.nullable(),
|
|
115
|
+
)
|
|
116
|
+
export type ScheduleWeek = z.infer<typeof ScheduleWeek>
|
|
117
|
+
|
|
118
|
+
export const Schedule = z.object({
|
|
119
|
+
id: z.uuid(),
|
|
120
|
+
...ws,
|
|
121
|
+
name: z.string().min(1).max(120),
|
|
122
|
+
kind: z.enum(['fixed', 'flexible', 'shift']),
|
|
123
|
+
week: ScheduleWeek,
|
|
124
|
+
/** `office` means the person's primary office — the default, and almost always right. */
|
|
125
|
+
tzMode: z.enum(['office', 'person', 'fixed']),
|
|
126
|
+
tz: TimeZone.nullable(),
|
|
127
|
+
graceInMinutes: z.number().int().min(0).max(240),
|
|
128
|
+
graceOutMinutes: z.number().int().min(0).max(240),
|
|
129
|
+
roundingStepMinutes: z.number().int().min(0).max(60),
|
|
130
|
+
roundingDirection: z.enum(['nearest', 'employee', 'employer']),
|
|
131
|
+
/** Close an open shift automatically after this many minutes. Null leaves it for a human. */
|
|
132
|
+
autoClockOutAfterMinutes: z.number().int().min(60).nullable(),
|
|
133
|
+
archivedAt: Timestamp.nullable(),
|
|
134
|
+
})
|
|
135
|
+
export type Schedule = z.infer<typeof Schedule>
|
|
136
|
+
|
|
137
|
+
export const ScheduleAssignment = z.object({
|
|
138
|
+
id: z.uuid(),
|
|
139
|
+
...ws,
|
|
140
|
+
personId: z.uuid(),
|
|
141
|
+
scheduleId: z.uuid(),
|
|
142
|
+
effectiveFrom: IsoDate,
|
|
143
|
+
effectiveTo: IsoDate.nullable(),
|
|
144
|
+
})
|
|
145
|
+
export type ScheduleAssignment = z.infer<typeof ScheduleAssignment>
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A request to fix a wrong or missing punch.
|
|
149
|
+
*
|
|
150
|
+
* It does not edit anything. Approving it writes *new* punches that void the old ones, so the
|
|
151
|
+
* original record and the correction both survive — which is the difference between a corrected
|
|
152
|
+
* timesheet and an edited one.
|
|
153
|
+
*/
|
|
154
|
+
export const Regularization = z.object({
|
|
155
|
+
id: z.uuid(),
|
|
156
|
+
...ws,
|
|
157
|
+
personId: z.uuid(),
|
|
158
|
+
businessDate: IsoDate,
|
|
159
|
+
/** Null when the person never punched at all and is asking for the whole day to be recorded. */
|
|
160
|
+
punchId: z.uuid().nullable(),
|
|
161
|
+
proposed: z.array(z.object({ direction: PunchDirection, at: Timestamp })),
|
|
162
|
+
reason: z.string().min(1).max(1000),
|
|
163
|
+
status: z.enum(['pending', 'approved', 'rejected', 'cancelled']),
|
|
164
|
+
approvalRequestId: z.uuid().nullable(),
|
|
165
|
+
appliedAt: Timestamp.nullable(),
|
|
166
|
+
createdAt: Timestamp,
|
|
167
|
+
})
|
|
168
|
+
export type Regularization = z.infer<typeof Regularization>
|
|
169
|
+
|
|
170
|
+
/** The person's own live state: are they clocked in, since when, on a break? */
|
|
171
|
+
export const ClockState = z.object({
|
|
172
|
+
personId: z.uuid(),
|
|
173
|
+
businessDate: IsoDate,
|
|
174
|
+
clockedIn: z.boolean(),
|
|
175
|
+
onBreak: z.boolean(),
|
|
176
|
+
since: Timestamp.nullable(),
|
|
177
|
+
workedMinutesToday: z.number().int(),
|
|
178
|
+
timezone: TimeZone,
|
|
179
|
+
})
|
|
180
|
+
export type ClockState = z.infer<typeof ClockState>
|
|
@@ -64,6 +64,44 @@ 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
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'attendance',
|
|
89
|
+
label: 'Attendance',
|
|
90
|
+
description: 'Clock in and out, schedules and a daily sheet',
|
|
91
|
+
dependsOn: ['core', 'calendars'],
|
|
92
|
+
// Off by default. Plenty of companies never clock anybody in, and a directory that offers a
|
|
93
|
+
// clock button to salaried staff is offering a feature nobody asked for.
|
|
94
|
+
defaultEnabled: false,
|
|
95
|
+
level: 1,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'overtime',
|
|
99
|
+
label: 'Overtime',
|
|
100
|
+
description: 'Detect and approve time worked beyond the schedule',
|
|
101
|
+
dependsOn: ['attendance'],
|
|
102
|
+
defaultEnabled: false,
|
|
103
|
+
level: 2,
|
|
104
|
+
},
|
|
67
105
|
{
|
|
68
106
|
id: 'documents',
|
|
69
107
|
label: 'Employee documents',
|
|
@@ -114,4 +152,46 @@ export const hrCapabilityProcedures: Record<string, readonly string[]> = {
|
|
|
114
152
|
'calendars.workingDays',
|
|
115
153
|
],
|
|
116
154
|
documents: ['documents.list', 'documents.attach', 'documents.remove'],
|
|
155
|
+
leave: [
|
|
156
|
+
'leave.types.list',
|
|
157
|
+
'leave.types.create',
|
|
158
|
+
'leave.types.update',
|
|
159
|
+
'leave.types.archive',
|
|
160
|
+
'leave.balance.get',
|
|
161
|
+
'leave.ledger.list',
|
|
162
|
+
'leave.adjust',
|
|
163
|
+
'leave.requests.list',
|
|
164
|
+
'leave.requests.get',
|
|
165
|
+
'leave.requests.simulate',
|
|
166
|
+
'leave.requests.create',
|
|
167
|
+
'leave.requests.cancel',
|
|
168
|
+
'leave.team.calendar',
|
|
169
|
+
],
|
|
170
|
+
attendance: [
|
|
171
|
+
'attendance.state',
|
|
172
|
+
'attendance.clockIn',
|
|
173
|
+
'attendance.clockOut',
|
|
174
|
+
'attendance.breakStart',
|
|
175
|
+
'attendance.breakEnd',
|
|
176
|
+
'attendance.punches.list',
|
|
177
|
+
'attendance.punches.void',
|
|
178
|
+
'attendance.days.list',
|
|
179
|
+
'attendance.days.recompute',
|
|
180
|
+
'attendance.schedules.list',
|
|
181
|
+
'attendance.schedules.create',
|
|
182
|
+
'attendance.schedules.update',
|
|
183
|
+
'attendance.schedules.archive',
|
|
184
|
+
'attendance.schedules.assign',
|
|
185
|
+
'attendance.regularizations.list',
|
|
186
|
+
'attendance.regularizations.request',
|
|
187
|
+
],
|
|
188
|
+
approvals: [
|
|
189
|
+
'approvals.chains.list',
|
|
190
|
+
'approvals.chains.create',
|
|
191
|
+
'approvals.chains.update',
|
|
192
|
+
'approvals.chains.archive',
|
|
193
|
+
'approvals.delegate',
|
|
194
|
+
'approvals.revokeDelegation',
|
|
195
|
+
'approvals.delegations',
|
|
196
|
+
],
|
|
117
197
|
}
|
package/src/contract/events.ts
CHANGED
|
@@ -72,6 +72,87 @@ 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
|
+
),
|
|
132
|
+
punchRecorded: defineEvent(
|
|
133
|
+
'hr.punch.recorded',
|
|
134
|
+
z.object({
|
|
135
|
+
punchId: z.uuid(),
|
|
136
|
+
workspaceId: WorkspaceId,
|
|
137
|
+
personId: z.uuid(),
|
|
138
|
+
direction: z.string(),
|
|
139
|
+
businessDate: z.iso.date(),
|
|
140
|
+
}),
|
|
141
|
+
),
|
|
142
|
+
/**
|
|
143
|
+
* A derived day changed. Carries the date so a consumer recomputes that window rather than
|
|
144
|
+
* everything — this fires on every punch, so a coarse payload would be a stampede.
|
|
145
|
+
*/
|
|
146
|
+
attendanceDayComputed: defineEvent(
|
|
147
|
+
'hr.attendance.day_computed',
|
|
148
|
+
z.object({
|
|
149
|
+
workspaceId: WorkspaceId,
|
|
150
|
+
personId: z.uuid(),
|
|
151
|
+
businessDate: z.iso.date(),
|
|
152
|
+
status: z.string(),
|
|
153
|
+
workedMinutes: z.number().int(),
|
|
154
|
+
}),
|
|
155
|
+
),
|
|
75
156
|
calendarChanged: defineEvent(
|
|
76
157
|
'hr.calendar.changed',
|
|
77
158
|
z.object({
|
package/src/contract/index.ts
CHANGED
|
@@ -6,8 +6,11 @@
|
|
|
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'
|
|
10
|
+
export * from './attendance.js'
|
|
9
11
|
export * from './capabilities.js'
|
|
10
12
|
export * from './events.js'
|
|
13
|
+
export * from './leave.js'
|
|
11
14
|
export * from './models.js'
|
|
12
15
|
export * from './permissions.js'
|
|
13
16
|
export * from './router.js'
|