@kernhq/module-hr 0.10.0 → 0.10.2
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/attendance.d.ts +1 -0
- package/dist/contract/attendance.d.ts.map +1 -1
- package/dist/contract/attendance.js +9 -0
- package/dist/contract/attendance.js.map +1 -1
- package/dist/contract/capabilities.d.ts +16 -0
- package/dist/contract/capabilities.d.ts.map +1 -1
- package/dist/contract/capabilities.js +16 -0
- package/dist/contract/capabilities.js.map +1 -1
- package/dist/contract/router.d.ts +1 -0
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/policy/accrual.d.ts +35 -4
- package/dist/policy/accrual.d.ts.map +1 -1
- package/dist/policy/accrual.js +141 -15
- package/dist/policy/accrual.js.map +1 -1
- package/dist/policy/working-time.d.ts +115 -9
- package/dist/policy/working-time.d.ts.map +1 -1
- package/dist/policy/working-time.js +120 -20
- package/dist/policy/working-time.js.map +1 -1
- package/dist/server/jobs.d.ts +14 -6
- package/dist/server/jobs.d.ts.map +1 -1
- package/dist/server/jobs.js +489 -144
- package/dist/server/jobs.js.map +1 -1
- package/dist/server/packs/index.d.ts +14 -0
- package/dist/server/packs/index.d.ts.map +1 -1
- package/dist/server/packs/index.js +19 -0
- package/dist/server/packs/index.js.map +1 -1
- package/dist/server/router.d.ts +1888 -111
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/router.js +154 -63
- package/dist/server/router.js.map +1 -1
- package/dist/server/schema.d.ts +17 -0
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +81 -2
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/approvals.d.ts +0 -2
- package/dist/server/services/approvals.d.ts.map +1 -1
- package/dist/server/services/approvals.js +12 -4
- package/dist/server/services/approvals.js.map +1 -1
- package/dist/server/services/attendance.d.ts +120 -8
- package/dist/server/services/attendance.d.ts.map +1 -1
- package/dist/server/services/attendance.js +226 -14
- package/dist/server/services/attendance.js.map +1 -1
- package/dist/server/services/ledger.d.ts +0 -2
- package/dist/server/services/ledger.d.ts.map +1 -1
- package/dist/server/services/ledger.js +1 -2
- package/dist/server/services/ledger.js.map +1 -1
- package/dist/server/services/policies.d.ts +6 -2
- package/dist/server/services/policies.d.ts.map +1 -1
- package/dist/server/services/policies.js +7 -2
- package/dist/server/services/policies.js.map +1 -1
- package/migrations/0005_approval_requester.sql +11 -1
- package/migrations/0006_schedule_no_overlap.sql +85 -0
- package/migrations/0007_hot_path_indexes.sql +38 -0
- package/migrations/0009_beyond_cap_minutes.sql +15 -0
- package/migrations/meta/0005_snapshot.json +4047 -0
- package/migrations/meta/0007_snapshot.json +4225 -0
- package/migrations/meta/0009_snapshot.json +4231 -0
- package/migrations/meta/_journal.json +21 -0
- package/package.json +7 -7
- package/src/client/components/ClockControls.svelte +127 -20
- package/src/client/components/ClockControls.test.ts +446 -0
- package/src/client/components/DelegationDialog.svelte +12 -3
- package/src/client/components/LeaveRequestDialog.svelte +254 -25
- package/src/client/components/PersonFormDialog.svelte +53 -3
- package/src/client/components/PersonPanel.svelte +134 -26
- package/src/client/i18n.ts +5 -927
- package/src/client/messages.test.ts +154 -0
- package/src/client/messages.ts +3388 -0
- package/src/client/mock.ts +1576 -161
- package/src/client/module.ts +15 -1
- package/src/client/pages/ApprovalsPage.svelte +120 -46
- package/src/client/pages/AttendancePage.svelte +106 -20
- package/src/client/pages/DirectoryPage.svelte +192 -55
- package/src/client/pages/LeavePage.svelte +280 -25
- package/src/client/pages/OfficesPage.svelte +26 -8
- package/src/client/pages/leave-and-attendance.test.ts +588 -0
- package/src/client/query.ts +12 -0
- package/src/client/settings/CalendarsSettings.svelte +1363 -12
- package/src/client/settings/CapabilitiesSettings.svelte +276 -19
- package/src/client/settings/GeneralSettings.svelte +420 -0
- package/src/client/settings/LeaveSettings.svelte +884 -12
- package/src/client/settings/OfficesSettings.svelte +1182 -12
- package/src/client/settings/SchedulesSettings.svelte +1149 -12
- package/src/client/summary.ts +14 -7
- package/src/client/widgets/ApprovalsWidget.svelte +133 -20
- package/src/client/widgets/HeadcountWidget.svelte +74 -8
- package/src/client/widgets/LeaveBalanceWidget.svelte +40 -5
- package/src/client/widgets/WhosOutWidget.svelte +47 -8
- package/src/contract/attendance.ts +9 -0
- package/src/contract/capabilities.ts +17 -0
|
@@ -30,6 +30,22 @@ export interface RoundingPolicy {
|
|
|
30
30
|
direction: 'nearest' | 'employee' | 'employer';
|
|
31
31
|
}
|
|
32
32
|
export declare const NO_ROUNDING: RoundingPolicy;
|
|
33
|
+
/**
|
|
34
|
+
* An overtime policy as a day sees it, already resolved.
|
|
35
|
+
*
|
|
36
|
+
* `computeDay` is pure and stays pure, so which policy applies to this person on this date — and
|
|
37
|
+
* how much of the cap the year has already used — are answered by the caller against the ladder and
|
|
38
|
+
* the sheet, and arrive here as numbers. Absent, every minute past the schedule counts, which is
|
|
39
|
+
* what a workspace with no overtime policy means.
|
|
40
|
+
*/
|
|
41
|
+
export interface OvertimePolicy {
|
|
42
|
+
/** Minutes past the schedule before overtime starts counting. */
|
|
43
|
+
thresholdMinutes: number;
|
|
44
|
+
/** Cap for the containing period, in minutes. Null is uncapped. */
|
|
45
|
+
capMinutes: number | null;
|
|
46
|
+
/** Overtime already counted in that period, on days before this one. */
|
|
47
|
+
alreadyCountedMinutes: number;
|
|
48
|
+
}
|
|
33
49
|
export type PunchDirection = 'in' | 'out' | 'break_start' | 'break_end';
|
|
34
50
|
export interface PunchInput {
|
|
35
51
|
/** The instant, server-stamped. Milliseconds since the epoch. */
|
|
@@ -42,6 +58,18 @@ export interface DayComputation {
|
|
|
42
58
|
workedMinutes: number;
|
|
43
59
|
breakMinutes: number;
|
|
44
60
|
overtimeMinutes: number;
|
|
61
|
+
/**
|
|
62
|
+
* Hours worked past the schedule that the cap would not take.
|
|
63
|
+
*
|
|
64
|
+
* Reported rather than dropped: a jurisdiction capping overtime caps what may be *worked*, so
|
|
65
|
+
* these are hours somebody actually put in and a compliance report has to be able to sum them.
|
|
66
|
+
*
|
|
67
|
+
* **Null and zero say different things.** Null is no cap in force — nothing to exceed, and
|
|
68
|
+
* nothing a report should count as compliant. Zero is a cap in force that this day stayed inside.
|
|
69
|
+
* Only the policy separates them, so the distinction is drawn here rather than re-derived
|
|
70
|
+
* wherever the number is stored.
|
|
71
|
+
*/
|
|
72
|
+
beyondCapMinutes: number | null;
|
|
45
73
|
lateMinutes: number;
|
|
46
74
|
earlyLeaveMinutes: number;
|
|
47
75
|
status: 'present' | 'absent' | 'partial' | 'pending';
|
|
@@ -58,18 +86,94 @@ export interface DayComputation {
|
|
|
58
86
|
*/
|
|
59
87
|
export declare const crossesMidnight: (shift: ShiftSpec) => boolean;
|
|
60
88
|
/**
|
|
61
|
-
*
|
|
89
|
+
* Whether a shift is already running, by business date — the fact a clock reading cannot supply.
|
|
90
|
+
*
|
|
91
|
+
* Both are read from punches already filed: a clock-in with no clock-out after it. Gathering them is
|
|
92
|
+
* the caller's job and deciding what they mean is this file's, the same split `computeDay` already
|
|
93
|
+
* makes with the punches themselves.
|
|
94
|
+
*/
|
|
95
|
+
export interface OpenShifts {
|
|
96
|
+
/** A shift left open on the punch's own local date. */
|
|
97
|
+
onLocalDate: boolean;
|
|
98
|
+
/** A shift left open on the date before it — the night that may still be running. */
|
|
99
|
+
onPreviousDate: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* The previous date already holds punches, open or closed.
|
|
102
|
+
*
|
|
103
|
+
* Distinct from `onPreviousDate`, and the difference is a day sheet nobody can explain. A night
|
|
104
|
+
* worked 22:00–05:30 and clocked out of is *closed*, so it is not open — but it is also not
|
|
105
|
+
* waiting for anybody, and an arrival at 05:50 that joined it rewrote a finished Friday from
|
|
106
|
+
* 450 minutes to 1060, with four punches and nine hours of overtime, from somebody turning up
|
|
107
|
+
* early for Saturday.
|
|
108
|
+
*/
|
|
109
|
+
workedPreviousDate: boolean;
|
|
110
|
+
}
|
|
111
|
+
/** Nobody is mid-shift and nothing was worked yesterday, so every punch is an arrival. */
|
|
112
|
+
export declare const NOTHING_OPEN: OpenShifts;
|
|
113
|
+
/**
|
|
114
|
+
* What one business date's punches say about whether a shift, and a break, are still running.
|
|
115
|
+
*
|
|
116
|
+
* One reduction behind three questions that must never disagree: which shift the next punch joins,
|
|
117
|
+
* whether the transition it asks for is possible, and what the clock widget tells the person before
|
|
118
|
+
* they press anything. Rows have to arrive sorted by instant. `out` closes a break as well as a
|
|
119
|
+
* shift, because somebody who clocks out without ending their break has still stopped working.
|
|
120
|
+
*/
|
|
121
|
+
export declare function openState(punches: Array<{
|
|
122
|
+
direction: string;
|
|
123
|
+
}>): {
|
|
124
|
+
clockedIn: boolean;
|
|
125
|
+
onBreak: boolean;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Which shift a punch belongs to, and the business date that follows from it.
|
|
129
|
+
*
|
|
130
|
+
* A punch in the morning between a night shift and a day shift has two readings and **the clock
|
|
131
|
+
* cannot separate them**: at 06:59 on a Saturday, somebody who worked the 22:00–06:00 night is
|
|
132
|
+
* leaving it, and somebody who did not is arriving early for the 08:00 morning. Three versions of
|
|
133
|
+
* this function drew a line through that window — at the night's scheduled end, at the morning's
|
|
134
|
+
* start, at the midpoint of the gap widened by `graceInMinutes` — and every one of them filed one of
|
|
135
|
+
* those two people on the wrong day, one minute either side of wherever the line had moved to. There
|
|
136
|
+
* is no line to draw. What separates the two readings is not in the instant.
|
|
137
|
+
*
|
|
138
|
+
* It is in the punches. A shift is **open** when a clock-in has been filed on a business date with
|
|
139
|
+
* nothing closing it, and at most one shift is open at a time, because a person is in one place. So:
|
|
62
140
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
141
|
+
* 1. **A departure belongs to the shift it is leaving.** Where a shift is open the punch joins it,
|
|
142
|
+
* however far past the scheduled end it is made, because nothing else can close it. The most
|
|
143
|
+
* recent open one wins: that is the shift they are actually on.
|
|
144
|
+
* 2. **Everything else is an arrival**, and it opens the shift the clock says is running — the
|
|
145
|
+
* previous night while that night is still scheduled to run and the day's own shift has not
|
|
146
|
+
* started yet, otherwise the day it happened on.
|
|
67
147
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
148
|
+
* An open shift is a fact in the database rather than a guess about an instant, which is why this
|
|
149
|
+
* degrades correctly at both ends where a boundary could not: somebody who never clocked in last
|
|
150
|
+
* night has nothing open, so their early arrival is an arrival at any hour, and somebody who worked
|
|
151
|
+
* the night has something open, so their late departure is a departure however long they overran.
|
|
152
|
+
* No constant appears in either rule, so there is no minute left to get wrong.
|
|
153
|
+
*
|
|
154
|
+
* Rule (1) reaches exactly one calendar day back, and that is what stops a shift nobody ever closed
|
|
155
|
+
* claiming punches for ever: a day later the date in between holds no punches, nothing is open, and
|
|
156
|
+
* arrivals resume. Until then the person is *told* they are still clocked in, because
|
|
157
|
+
* `attendance.state` reads this same attribution from the same rows — so the action offered to them
|
|
158
|
+
* is the one that closes it, and no refusal arrives that the screen did not predict.
|
|
159
|
+
*
|
|
160
|
+
* One thing stays undecided, and it costs a date rather than a refusal. Where the night was NEVER
|
|
161
|
+
* WORKED — nothing open and nothing filed — a punch inside its scheduled span reads as a late
|
|
162
|
+
* arrival for it: at 05:59 that files a two-hour-early arrival for the morning on the night's date.
|
|
163
|
+
* Both ends of that shift still land together, since the clock-out follows the shift the clock-in
|
|
164
|
+
* opened, so nobody is turned away. The sheet is wrong about which day, and says somebody was very
|
|
165
|
+
* late for a shift they were in fact early for.
|
|
166
|
+
*
|
|
167
|
+
* It is left because nothing available at that instant settles it: at 05:59 a person with a night
|
|
168
|
+
* scheduled and nothing filed is either eleven minutes from the end of a shift they never started
|
|
169
|
+
* or two hours early for the next one, and only the clock-out — which has not happened — tells them
|
|
170
|
+
* apart. Deciding it on how much of the night is left would be a fourth constant, and three have
|
|
171
|
+
* been tried. A night already worked is a different question and is answered above.
|
|
71
172
|
*/
|
|
72
|
-
export declare function
|
|
173
|
+
export declare function attributeToShift(instantMs: number, timeZone: string, shiftFor: (date: IsoDate) => ShiftSpec | null, open: OpenShifts): {
|
|
174
|
+
businessDate: IsoDate;
|
|
175
|
+
shift: ShiftSpec | null;
|
|
176
|
+
};
|
|
73
177
|
/**
|
|
74
178
|
* The scheduled span of a shift on a date — computed from instants, not from clock readings.
|
|
75
179
|
*
|
|
@@ -92,6 +196,8 @@ export declare function computeDay(opts: {
|
|
|
92
196
|
shift: ShiftSpec | null;
|
|
93
197
|
punches: PunchInput[];
|
|
94
198
|
rounding?: RoundingPolicy;
|
|
199
|
+
/** The resolved overtime policy. Absent, everything past the schedule is overtime. */
|
|
200
|
+
overtime?: OvertimePolicy;
|
|
95
201
|
/** Approved leave or a holiday: not an absence, whatever the punches say. */
|
|
96
202
|
excused?: boolean;
|
|
97
203
|
}): DayComputation;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"working-time.d.ts","sourceRoot":"","sources":["../../src/policy/working-time.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"working-time.d.ts","sourceRoot":"","sources":["../../src/policy/working-time.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Overtime lives beside the accrual arithmetic because a capped hour often becomes compensatory
|
|
2
|
+
// leave rather than pay. It is the same function either way, so it is imported rather than copied.
|
|
3
|
+
import { overtimeFor } from './accrual.js';
|
|
1
4
|
import { dateIn, minutesOfDayIn, nextDate, previousDate, zonedToInstant } from './time.js';
|
|
2
5
|
export const NO_ROUNDING = { stepMinutes: 0, direction: 'nearest' };
|
|
3
6
|
/**
|
|
@@ -11,29 +14,108 @@ const toMinutes = (wall) => {
|
|
|
11
14
|
const [h, m] = wall.split(':').map(Number);
|
|
12
15
|
return h * 60 + m;
|
|
13
16
|
};
|
|
17
|
+
/** Nobody is mid-shift and nothing was worked yesterday, so every punch is an arrival. */
|
|
18
|
+
export const NOTHING_OPEN = {
|
|
19
|
+
onLocalDate: false,
|
|
20
|
+
onPreviousDate: false,
|
|
21
|
+
workedPreviousDate: false,
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* What one business date's punches say about whether a shift, and a break, are still running.
|
|
25
|
+
*
|
|
26
|
+
* One reduction behind three questions that must never disagree: which shift the next punch joins,
|
|
27
|
+
* whether the transition it asks for is possible, and what the clock widget tells the person before
|
|
28
|
+
* they press anything. Rows have to arrive sorted by instant. `out` closes a break as well as a
|
|
29
|
+
* shift, because somebody who clocks out without ending their break has still stopped working.
|
|
30
|
+
*/
|
|
31
|
+
export function openState(punches) {
|
|
32
|
+
let clockedIn = false;
|
|
33
|
+
let onBreak = false;
|
|
34
|
+
for (const punch of punches) {
|
|
35
|
+
if (punch.direction === 'in')
|
|
36
|
+
clockedIn = true;
|
|
37
|
+
else if (punch.direction === 'out') {
|
|
38
|
+
clockedIn = false;
|
|
39
|
+
onBreak = false;
|
|
40
|
+
}
|
|
41
|
+
else if (punch.direction === 'break_start')
|
|
42
|
+
onBreak = true;
|
|
43
|
+
else if (punch.direction === 'break_end')
|
|
44
|
+
onBreak = false;
|
|
45
|
+
}
|
|
46
|
+
return { clockedIn, onBreak };
|
|
47
|
+
}
|
|
14
48
|
/**
|
|
15
|
-
* Which
|
|
49
|
+
* Which shift a punch belongs to, and the business date that follows from it.
|
|
16
50
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
51
|
+
* A punch in the morning between a night shift and a day shift has two readings and **the clock
|
|
52
|
+
* cannot separate them**: at 06:59 on a Saturday, somebody who worked the 22:00–06:00 night is
|
|
53
|
+
* leaving it, and somebody who did not is arriving early for the 08:00 morning. Three versions of
|
|
54
|
+
* this function drew a line through that window — at the night's scheduled end, at the morning's
|
|
55
|
+
* start, at the midpoint of the gap widened by `graceInMinutes` — and every one of them filed one of
|
|
56
|
+
* those two people on the wrong day, one minute either side of wherever the line had moved to. There
|
|
57
|
+
* is no line to draw. What separates the two readings is not in the instant.
|
|
21
58
|
*
|
|
22
|
-
*
|
|
23
|
-
* it
|
|
24
|
-
*
|
|
59
|
+
* It is in the punches. A shift is **open** when a clock-in has been filed on a business date with
|
|
60
|
+
* nothing closing it, and at most one shift is open at a time, because a person is in one place. So:
|
|
61
|
+
*
|
|
62
|
+
* 1. **A departure belongs to the shift it is leaving.** Where a shift is open the punch joins it,
|
|
63
|
+
* however far past the scheduled end it is made, because nothing else can close it. The most
|
|
64
|
+
* recent open one wins: that is the shift they are actually on.
|
|
65
|
+
* 2. **Everything else is an arrival**, and it opens the shift the clock says is running — the
|
|
66
|
+
* previous night while that night is still scheduled to run and the day's own shift has not
|
|
67
|
+
* started yet, otherwise the day it happened on.
|
|
68
|
+
*
|
|
69
|
+
* An open shift is a fact in the database rather than a guess about an instant, which is why this
|
|
70
|
+
* degrades correctly at both ends where a boundary could not: somebody who never clocked in last
|
|
71
|
+
* night has nothing open, so their early arrival is an arrival at any hour, and somebody who worked
|
|
72
|
+
* the night has something open, so their late departure is a departure however long they overran.
|
|
73
|
+
* No constant appears in either rule, so there is no minute left to get wrong.
|
|
74
|
+
*
|
|
75
|
+
* Rule (1) reaches exactly one calendar day back, and that is what stops a shift nobody ever closed
|
|
76
|
+
* claiming punches for ever: a day later the date in between holds no punches, nothing is open, and
|
|
77
|
+
* arrivals resume. Until then the person is *told* they are still clocked in, because
|
|
78
|
+
* `attendance.state` reads this same attribution from the same rows — so the action offered to them
|
|
79
|
+
* is the one that closes it, and no refusal arrives that the screen did not predict.
|
|
80
|
+
*
|
|
81
|
+
* One thing stays undecided, and it costs a date rather than a refusal. Where the night was NEVER
|
|
82
|
+
* WORKED — nothing open and nothing filed — a punch inside its scheduled span reads as a late
|
|
83
|
+
* arrival for it: at 05:59 that files a two-hour-early arrival for the morning on the night's date.
|
|
84
|
+
* Both ends of that shift still land together, since the clock-out follows the shift the clock-in
|
|
85
|
+
* opened, so nobody is turned away. The sheet is wrong about which day, and says somebody was very
|
|
86
|
+
* late for a shift they were in fact early for.
|
|
87
|
+
*
|
|
88
|
+
* It is left because nothing available at that instant settles it: at 05:59 a person with a night
|
|
89
|
+
* scheduled and nothing filed is either eleven minutes from the end of a shift they never started
|
|
90
|
+
* or two hours early for the next one, and only the clock-out — which has not happened — tells them
|
|
91
|
+
* apart. Deciding it on how much of the night is left would be a fourth constant, and three have
|
|
92
|
+
* been tried. A night already worked is a different question and is answered above.
|
|
25
93
|
*/
|
|
26
|
-
export function
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
94
|
+
export function attributeToShift(instantMs, timeZone, shiftFor, open) {
|
|
95
|
+
const local = dateIn(instantMs, timeZone);
|
|
96
|
+
const yesterday = previousDate(local);
|
|
97
|
+
// (1) A departure. Deliberately before anything reads the clock: an open shift outranks every
|
|
98
|
+
// schedule, including one that says this punch is hours past when the night should have ended.
|
|
99
|
+
if (open.onLocalDate)
|
|
100
|
+
return { businessDate: local, shift: shiftFor(local) };
|
|
101
|
+
if (open.onPreviousDate)
|
|
102
|
+
return { businessDate: yesterday, shift: shiftFor(yesterday) };
|
|
103
|
+
// (2) An arrival. The previous night still holds the small hours — somebody clocking in at 02:00
|
|
104
|
+
// is late for last night's shift, not early for tonight's — but only while it is scheduled to be
|
|
105
|
+
// running and only until the day's own shift starts, so overlapping schedules go to the one that
|
|
106
|
+
// began most recently.
|
|
107
|
+
const own = shiftFor(local);
|
|
108
|
+
const overnight = shiftFor(yesterday);
|
|
109
|
+
// A night already worked is not waiting for anybody. `open` is false either way once it is
|
|
110
|
+
// clocked out of, so without this an early arrival for the morning joined a finished night and
|
|
111
|
+
// rewrote its sheet — the one thing this function's own comment promised could not happen.
|
|
112
|
+
if (overnight && crossesMidnight(overnight) && !open.workedPreviousDate) {
|
|
113
|
+
const minute = minutesOfDayIn(instantMs, timeZone);
|
|
114
|
+
const dayStart = own ? toMinutes(own.start) : Number.POSITIVE_INFINITY;
|
|
115
|
+
if (minute < toMinutes(overnight.end) && minute < dayStart)
|
|
116
|
+
return { businessDate: yesterday, shift: overnight };
|
|
117
|
+
}
|
|
118
|
+
return { businessDate: local, shift: own };
|
|
37
119
|
}
|
|
38
120
|
/**
|
|
39
121
|
* Round a **worked total** to the policy's step.
|
|
@@ -81,6 +163,7 @@ export function scheduledMinutesOn(date, timeZone, shift) {
|
|
|
81
163
|
export function computeDay(opts) {
|
|
82
164
|
const { businessDate, timeZone, shift, excused } = opts;
|
|
83
165
|
const rounding = opts.rounding ?? NO_ROUNDING;
|
|
166
|
+
const capMinutes = opts.overtime?.capMinutes ?? null;
|
|
84
167
|
const punches = [...opts.punches].sort((a, b) => a.at - b.at);
|
|
85
168
|
const anomalies = [];
|
|
86
169
|
const scheduledMinutes = shift ? scheduledMinutesOn(businessDate, timeZone, shift) : 0;
|
|
@@ -91,6 +174,7 @@ export function computeDay(opts) {
|
|
|
91
174
|
workedMinutes: 0,
|
|
92
175
|
breakMinutes: 0,
|
|
93
176
|
overtimeMinutes: 0,
|
|
177
|
+
beyondCapMinutes: capMinutes === null ? null : 0,
|
|
94
178
|
lateMinutes: 0,
|
|
95
179
|
earlyLeaveMinutes: 0,
|
|
96
180
|
status: excused ? 'present' : scheduledMinutes > 0 ? 'absent' : 'present',
|
|
@@ -167,7 +251,22 @@ export function computeDay(opts) {
|
|
|
167
251
|
const scheduledEnd = zonedToInstant(endDate, shift.end, timeZone);
|
|
168
252
|
earlyLeaveMinutes = Math.max(0, Math.round((scheduledEnd - lastOut) / 60000) - shift.graceOutMinutes);
|
|
169
253
|
}
|
|
170
|
-
|
|
254
|
+
// The threshold and the cap an admin configured, applied here rather than validated on save and
|
|
255
|
+
// then ignored: `max(0, worked - scheduled)` obeyed neither, so a 30-minute threshold and the
|
|
256
|
+
// Turkish 270-hour annual cap both saved successfully and changed nothing.
|
|
257
|
+
const overtime = overtimeFor({
|
|
258
|
+
workedMinutes,
|
|
259
|
+
scheduledMinutes,
|
|
260
|
+
thresholdMinutes: opts.overtime?.thresholdMinutes ?? 0,
|
|
261
|
+
capMinutes,
|
|
262
|
+
alreadyCountedMinutes: opts.overtime?.alreadyCountedMinutes ?? 0,
|
|
263
|
+
});
|
|
264
|
+
const overtimeMinutes = overtime.overtimeMinutes;
|
|
265
|
+
const beyondCapMinutes = capMinutes === null ? null : overtime.beyondCapMinutes;
|
|
266
|
+
// The number is for a report to sum; this is what puts it in front of the person reviewing the
|
|
267
|
+
// day. Both are derived here, from the same value, so the two cannot disagree about one fact.
|
|
268
|
+
if (beyondCapMinutes)
|
|
269
|
+
anomalies.push('overtime_beyond_cap');
|
|
171
270
|
const status = anomalies.includes('missing_clock_out')
|
|
172
271
|
? 'pending'
|
|
173
272
|
: workedMinutes > 0
|
|
@@ -179,6 +278,7 @@ export function computeDay(opts) {
|
|
|
179
278
|
workedMinutes,
|
|
180
279
|
breakMinutes: Math.round(breakMs / 60000),
|
|
181
280
|
overtimeMinutes,
|
|
281
|
+
beyondCapMinutes,
|
|
182
282
|
lateMinutes,
|
|
183
283
|
earlyLeaveMinutes,
|
|
184
284
|
status,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"working-time.js","sourceRoot":"","sources":["../../src/policy/working-time.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAmC1F,MAAM,CAAC,MAAM,WAAW,GAAmB,EAAE,WAAW,EAAE,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"working-time.js","sourceRoot":"","sources":["../../src/policy/working-time.ts"],"names":[],"mappings":"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"}
|
package/dist/server/jobs.d.ts
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import type { JobDef } from '@kernhq/kernel';
|
|
2
|
-
import { inArray } from 'drizzle-orm';
|
|
3
|
-
import { offices } from './schema.js';
|
|
4
|
-
import { todayIn } from './services/db.js';
|
|
5
2
|
/**
|
|
6
3
|
* HR's scheduled work.
|
|
7
4
|
*
|
|
8
5
|
* One rule runs through all of it: **a cron expression fires in UTC, and this module's users do
|
|
9
6
|
* not live there.** A nightly job at 00:00 UTC closes an Amsterdam shift at 01:00 and an Istanbul
|
|
10
7
|
* one at 03:00, and gets Tehran's half-hour offset wrong in a way nobody would ever guess from the
|
|
11
|
-
* code. So the jobs run hourly and fan out per
|
|
12
|
-
* local boundary has passed.
|
|
8
|
+
* code. So the two calendar jobs — `accrue-leave` and `carry-forward` — run hourly and fan out per
|
|
9
|
+
* office, asking each whether *that office's* local boundary has passed.
|
|
10
|
+
*
|
|
11
|
+
* That was the comment for months while no handler read the offices table: `accrue-leave` fired
|
|
12
|
+
* once a month and asked Postgres for `date_trunc('month', now())`, which is the database session's
|
|
13
|
+
* timezone, so a New York office was credited January's accrual at 21:00 on 31 January local. A
|
|
14
|
+
* boundary is a date in a zone, so it is `todayIn(office.timezone)` and nothing else.
|
|
15
|
+
*
|
|
16
|
+
* Running hourly means a handler is entered twenty-four times for every boundary it acts on, so
|
|
17
|
+
* each is idempotent by construction rather than by scheduling: the ledger is asked what it already
|
|
18
|
+
* holds before anything is written.
|
|
19
|
+
*
|
|
20
|
+
* `auto-clock-out` is the exception, and says so where it lives: an elapsed hour is the same hour in
|
|
21
|
+
* every zone, so an office decides which *zone* its punch is stamped in, not when the sweep fires.
|
|
13
22
|
*/
|
|
14
23
|
export declare function hrJobs(): JobDef[];
|
|
15
|
-
export { inArray, offices, todayIn };
|
|
16
24
|
//# sourceMappingURL=jobs.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jobs.d.ts","sourceRoot":"","sources":["../../src/server/jobs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"jobs.d.ts","sourceRoot":"","sources":["../../src/server/jobs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAc,MAAM,gBAAgB,CAAA;AA6BxD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,MAAM,IAAI,MAAM,EAAE,CAqmBjC"}
|