@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.
Files changed (90) hide show
  1. package/dist/contract/attendance.d.ts +1 -0
  2. package/dist/contract/attendance.d.ts.map +1 -1
  3. package/dist/contract/attendance.js +9 -0
  4. package/dist/contract/attendance.js.map +1 -1
  5. package/dist/contract/capabilities.d.ts +16 -0
  6. package/dist/contract/capabilities.d.ts.map +1 -1
  7. package/dist/contract/capabilities.js +16 -0
  8. package/dist/contract/capabilities.js.map +1 -1
  9. package/dist/contract/router.d.ts +1 -0
  10. package/dist/contract/router.d.ts.map +1 -1
  11. package/dist/policy/accrual.d.ts +35 -4
  12. package/dist/policy/accrual.d.ts.map +1 -1
  13. package/dist/policy/accrual.js +141 -15
  14. package/dist/policy/accrual.js.map +1 -1
  15. package/dist/policy/working-time.d.ts +115 -9
  16. package/dist/policy/working-time.d.ts.map +1 -1
  17. package/dist/policy/working-time.js +120 -20
  18. package/dist/policy/working-time.js.map +1 -1
  19. package/dist/server/jobs.d.ts +14 -6
  20. package/dist/server/jobs.d.ts.map +1 -1
  21. package/dist/server/jobs.js +489 -144
  22. package/dist/server/jobs.js.map +1 -1
  23. package/dist/server/packs/index.d.ts +14 -0
  24. package/dist/server/packs/index.d.ts.map +1 -1
  25. package/dist/server/packs/index.js +19 -0
  26. package/dist/server/packs/index.js.map +1 -1
  27. package/dist/server/router.d.ts +1888 -111
  28. package/dist/server/router.d.ts.map +1 -1
  29. package/dist/server/router.js +154 -63
  30. package/dist/server/router.js.map +1 -1
  31. package/dist/server/schema.d.ts +17 -0
  32. package/dist/server/schema.d.ts.map +1 -1
  33. package/dist/server/schema.js +81 -2
  34. package/dist/server/schema.js.map +1 -1
  35. package/dist/server/services/approvals.d.ts +0 -2
  36. package/dist/server/services/approvals.d.ts.map +1 -1
  37. package/dist/server/services/approvals.js +12 -4
  38. package/dist/server/services/approvals.js.map +1 -1
  39. package/dist/server/services/attendance.d.ts +120 -8
  40. package/dist/server/services/attendance.d.ts.map +1 -1
  41. package/dist/server/services/attendance.js +226 -14
  42. package/dist/server/services/attendance.js.map +1 -1
  43. package/dist/server/services/ledger.d.ts +0 -2
  44. package/dist/server/services/ledger.d.ts.map +1 -1
  45. package/dist/server/services/ledger.js +1 -2
  46. package/dist/server/services/ledger.js.map +1 -1
  47. package/dist/server/services/policies.d.ts +6 -2
  48. package/dist/server/services/policies.d.ts.map +1 -1
  49. package/dist/server/services/policies.js +7 -2
  50. package/dist/server/services/policies.js.map +1 -1
  51. package/migrations/0005_approval_requester.sql +11 -1
  52. package/migrations/0006_schedule_no_overlap.sql +85 -0
  53. package/migrations/0007_hot_path_indexes.sql +38 -0
  54. package/migrations/0009_beyond_cap_minutes.sql +15 -0
  55. package/migrations/meta/0005_snapshot.json +4047 -0
  56. package/migrations/meta/0007_snapshot.json +4225 -0
  57. package/migrations/meta/0009_snapshot.json +4231 -0
  58. package/migrations/meta/_journal.json +21 -0
  59. package/package.json +7 -7
  60. package/src/client/components/ClockControls.svelte +127 -20
  61. package/src/client/components/ClockControls.test.ts +446 -0
  62. package/src/client/components/DelegationDialog.svelte +12 -3
  63. package/src/client/components/LeaveRequestDialog.svelte +254 -25
  64. package/src/client/components/PersonFormDialog.svelte +53 -3
  65. package/src/client/components/PersonPanel.svelte +134 -26
  66. package/src/client/i18n.ts +5 -927
  67. package/src/client/messages.test.ts +154 -0
  68. package/src/client/messages.ts +3388 -0
  69. package/src/client/mock.ts +1576 -161
  70. package/src/client/module.ts +15 -1
  71. package/src/client/pages/ApprovalsPage.svelte +120 -46
  72. package/src/client/pages/AttendancePage.svelte +106 -20
  73. package/src/client/pages/DirectoryPage.svelte +192 -55
  74. package/src/client/pages/LeavePage.svelte +280 -25
  75. package/src/client/pages/OfficesPage.svelte +26 -8
  76. package/src/client/pages/leave-and-attendance.test.ts +588 -0
  77. package/src/client/query.ts +12 -0
  78. package/src/client/settings/CalendarsSettings.svelte +1363 -12
  79. package/src/client/settings/CapabilitiesSettings.svelte +276 -19
  80. package/src/client/settings/GeneralSettings.svelte +420 -0
  81. package/src/client/settings/LeaveSettings.svelte +884 -12
  82. package/src/client/settings/OfficesSettings.svelte +1182 -12
  83. package/src/client/settings/SchedulesSettings.svelte +1149 -12
  84. package/src/client/summary.ts +14 -7
  85. package/src/client/widgets/ApprovalsWidget.svelte +133 -20
  86. package/src/client/widgets/HeadcountWidget.svelte +74 -8
  87. package/src/client/widgets/LeaveBalanceWidget.svelte +40 -5
  88. package/src/client/widgets/WhosOutWidget.svelte +47 -8
  89. package/src/contract/attendance.ts +9 -0
  90. 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
- * Which business day a punch belongs to.
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
- * For a day shift, the local date. For a night shift, **the date the shift started**: somebody
64
- * clocking out at 06:00 on Tuesday finished Monday's shift, and putting those minutes on Tuesday
65
- * leaves Monday short and Tuesday long — which is how a month's total comes out right while every
66
- * individual day is wrong.
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
- * The zone is the person's, from their primary office. A punch on a business trip records the zone
69
- * it happened in for audit but is still attributed here, or a week abroad splits somebody's month
70
- * across two calendars.
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 businessDateFor(instantMs: number, timeZone: string, shift: ShiftSpec | null): IsoDate;
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;AAGnD;;;;;;GAMG;AAEH,wGAAwG;AACxG,MAAM,WAAW,SAAS;IACxB,cAAc;IACd,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,GAAG,EAAE,MAAM,CAAA;IACX,qDAAqD;IACrD,YAAY,EAAE,MAAM,CAAA;IACpB,uDAAuD;IACvD,cAAc,EAAE,MAAM,CAAA;IACtB,kEAAkE;IAClE,eAAe,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,sFAAsF;IACtF,WAAW,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,SAAS,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,CAAA;CAC/C;AAED,eAAO,MAAM,WAAW,EAAE,cAAyD,CAAA;AAEnF,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,KAAK,GAAG,aAAa,GAAG,WAAW,CAAA;AAEvE,MAAM,WAAW,UAAU;IACzB,iEAAiE;IACjE,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,cAAc,CAAA;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,OAAO,CAAA;IACrB,gBAAgB,EAAE,MAAM,CAAA;IACxB,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,eAAe,EAAE,MAAM,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAA;IACpD,2EAA2E;IAC3E,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,SAAS,KAAG,OAAyD,CAAA;AAO5G;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,GAAG,OAAO,CAWrG;AAwBD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,CAK5F;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE;IAC/B,YAAY,EAAE,OAAO,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,SAAS,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,UAAU,EAAE,CAAA;IACrB,QAAQ,CAAC,EAAE,cAAc,CAAA;IACzB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,GAAG,cAAc,CA8GjB"}
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;AAMnD;;;;;;GAMG;AAEH,wGAAwG;AACxG,MAAM,WAAW,SAAS;IACxB,cAAc;IACd,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,GAAG,EAAE,MAAM,CAAA;IACX,qDAAqD;IACrD,YAAY,EAAE,MAAM,CAAA;IACpB,uDAAuD;IACvD,cAAc,EAAE,MAAM,CAAA;IACtB,kEAAkE;IAClE,eAAe,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,sFAAsF;IACtF,WAAW,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,SAAS,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,CAAA;CAC/C;AAED,eAAO,MAAM,WAAW,EAAE,cAAyD,CAAA;AAEnF;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,gBAAgB,EAAE,MAAM,CAAA;IACxB,mEAAmE;IACnE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,wEAAwE;IACxE,qBAAqB,EAAE,MAAM,CAAA;CAC9B;AAED,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,KAAK,GAAG,aAAa,GAAG,WAAW,CAAA;AAEvE,MAAM,WAAW,UAAU;IACzB,iEAAiE;IACjE,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,cAAc,CAAA;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,OAAO,CAAA;IACrB,gBAAgB,EAAE,MAAM,CAAA;IACxB,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,eAAe,EAAE,MAAM,CAAA;IACvB;;;;;;;;;;OAUG;IACH,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAA;IACpD,2EAA2E;IAC3E,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,SAAS,KAAG,OAAyD,CAAA;AAO5G;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,uDAAuD;IACvD,WAAW,EAAE,OAAO,CAAA;IACpB,qFAAqF;IACrF,cAAc,EAAE,OAAO,CAAA;IACvB;;;;;;;;OAQG;IACH,kBAAkB,EAAE,OAAO,CAAA;CAC5B;AAED,0FAA0F;AAC1F,eAAO,MAAM,YAAY,EAAE,UAI1B,CAAA;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,KAAK,CAAC;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG;IAChE,SAAS,EAAE,OAAO,CAAA;IAClB,OAAO,EAAE,OAAO,CAAA;CACjB,CAYA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,SAAS,GAAG,IAAI,EAC7C,IAAI,EAAE,UAAU,GACf;IAAE,YAAY,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAAA;CAAE,CAyBpD;AAwBD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,CAK5F;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE;IAC/B,YAAY,EAAE,OAAO,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,SAAS,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,UAAU,EAAE,CAAA;IACrB,QAAQ,CAAC,EAAE,cAAc,CAAA;IACzB,sFAAsF;IACtF,QAAQ,CAAC,EAAE,cAAc,CAAA;IACzB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,GAAG,cAAc,CA+HjB"}
@@ -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 business day a punch belongs to.
49
+ * Which shift a punch belongs to, and the business date that follows from it.
16
50
  *
17
- * For a day shift, the local date. For a night shift, **the date the shift started**: somebody
18
- * clocking out at 06:00 on Tuesday finished Monday's shift, and putting those minutes on Tuesday
19
- * leaves Monday short and Tuesday long which is how a month's total comes out right while every
20
- * individual day is wrong.
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
- * The zone is the person's, from their primary office. A punch on a business trip records the zone
23
- * it happened in for audit but is still attributed here, or a week abroad splits somebody's month
24
- * across two calendars.
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 businessDateFor(instantMs, timeZone, shift) {
27
- const localDate = dateIn(instantMs, timeZone);
28
- if (!shift || !crossesMidnight(shift))
29
- return localDate;
30
- const minutes = minutesOfDayIn(instantMs, timeZone);
31
- const startMin = toMinutes(shift.start);
32
- const endMin = toMinutes(shift.end);
33
- // Anything in the small hours up to a margin past the shift's end belongs to the previous day, so
34
- // a late clock-out is still attributed to the shift it actually finished.
35
- const tail = Math.min(startMin, endMin + 240);
36
- return minutes < tail ? previousDate(localDate) : localDate;
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
- const overtimeMinutes = Math.max(0, workedMinutes - scheduledMinutes);
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":"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"}
1
+ {"version":3,"file":"working-time.js","sourceRoot":"","sources":["../../src/policy/working-time.ts"],"names":[],"mappings":"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"}
@@ -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 office, deciding for each whether *that office's*
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,EAAU,MAAM,gBAAgB,CAAA;AACpD,OAAO,EAAW,OAAO,EAAoB,MAAM,aAAa,CAAA;AAEhE,OAAO,EAKL,OAAO,EAIR,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAK1C;;;;;;;;GAQG;AACH,wBAAgB,MAAM,IAAI,MAAM,EAAE,CAwYjC;AAeD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAA"}
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"}