@kernhq/module-hr 0.10.4 → 0.11.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.
@@ -198,7 +198,13 @@ const attendance = page('./AttendancePage.svelte')
198
198
  const leave = page('./LeavePage.svelte')
199
199
 
200
200
  /** A day sheet row, shaped enough for the `{#if}`s inside the month list. */
201
- const day = { overtimeMinutes: 0, anomalies: [] as unknown[] }
201
+ const day = { overtimeMinutes: 0, anomalies: [] as unknown[], businessDate: '2026-08-03' }
202
+ /**
203
+ * What an expandable row reads besides the day itself: whether a correction is waiting on this
204
+ * date, and whether this row is the open one. Both live outside the `{#each}`, so the resolver has
205
+ * to be handed them or every render of the month throws.
206
+ */
207
+ const row = { correctionDates: new Set<string>(), open: false }
202
208
 
203
209
  describe('the month on the attendance page', () => {
204
210
  /**
@@ -211,7 +217,7 @@ describe('the month on the attendance page', () => {
211
217
  })
212
218
 
213
219
  it('draws a day-shaped skeleton while the first fetch is in flight', () => {
214
- const body = attendance.render('loading', { loading: true, days: [], daysQuery: {}, day })
220
+ const body = attendance.render('loading', { loading: true, days: [], daysQuery: {}, day, ...row })
215
221
  expect(body).toContain('Skeleton')
216
222
  expect(body).not.toContain('EmptyState')
217
223
  })
@@ -227,6 +233,7 @@ describe('the month on the attendance page', () => {
227
233
  days: [],
228
234
  daysQuery: { isError: true },
229
235
  day,
236
+ ...row,
230
237
  })
231
238
  expect(body).toContain("t('attendance_error')")
232
239
  expect(body).toContain("t('retry')")
@@ -239,6 +246,7 @@ describe('the month on the attendance page', () => {
239
246
  days: [],
240
247
  daysQuery: { isError: false },
241
248
  day,
249
+ ...row,
242
250
  })
243
251
  expect(body).toContain("t('attendance_none')")
244
252
  expect(body).not.toContain("t('attendance_error')")
@@ -255,6 +263,7 @@ describe('the month on the attendance page', () => {
255
263
  days: [{ id: 'a' }],
256
264
  daysQuery: { isError: true },
257
265
  day,
266
+ ...row,
258
267
  })
259
268
  expect(body).toContain('{#each days as day (day.id)}')
260
269
  expect(body).toContain("t('attendance_stale')")
@@ -268,10 +277,28 @@ describe('the month on the attendance page', () => {
268
277
  days: [{ id: 'a' }],
269
278
  daysQuery: { isError: false },
270
279
  day,
280
+ ...row,
271
281
  })
272
282
  expect(body).toContain('{#each days as day (day.id)}')
273
283
  expect(body).not.toContain("t('attendance_stale')")
274
284
  })
285
+
286
+ /**
287
+ * The anomaly badge used to be `{day.anomalies.length}` — a number in a warning tone with no
288
+ * noun beside it and nothing to click. It counts things a person has to look at, so it says so,
289
+ * and the sentences behind it are one row-click away.
290
+ */
291
+ it('names what the anomaly badge counts rather than showing a bare number', () => {
292
+ const body = attendance.render('loading', {
293
+ loading: false,
294
+ days: [{ id: 'a' }],
295
+ daysQuery: { isError: false },
296
+ day: { ...day, anomalies: ['missing_clock_out'] },
297
+ ...row,
298
+ })
299
+ expect(body).toContain("t('att_anomalies_count', { count: day.anomalies.length })")
300
+ expect(body).not.toContain('<Badge tone="warning">{day.anomalies.length}</Badge>')
301
+ })
275
302
  })
276
303
 
277
304
  describe('the attendance tiles', () => {
@@ -50,6 +50,27 @@ export const hrKeys = {
50
50
  entities: (ws: string) => ['hr', 'entities', ws] as const,
51
51
  officePeople: (ws: string, officeId: string, primaryOnly: boolean) =>
52
52
  ['hr', 'office-people', ws, officeId, primaryOnly ? 'primary' : 'all'] as const,
53
+ /**
54
+ * The ledger is keyed by the leave type *and* the year as well as the person: the panel anchors
55
+ * its running balance on the balance the server computed for one entitlement year, so serving it
56
+ * another year's entries from cache would draw a column of numbers reconciling to nothing.
57
+ */
58
+ leaveLedger: (ws: string, personId: string, leaveTypeId: string, periodYear: number) =>
59
+ ['hr', 'leave-ledger', ws, personId, leaveTypeId, periodYear] as const,
60
+ employmentHistory: (ws: string, personId: string) => ['hr', 'employment-history', ws, personId] as const,
61
+ documents: (ws: string, personId: string) => ['hr', 'documents', ws, personId] as const,
62
+ /** Fetched only where the viewer holds `hr.person.view_sensitive`, so it is its own entry. */
63
+ sensitive: (ws: string, personId: string) => ['hr', 'sensitive', ws, personId] as const,
64
+ /**
65
+ * The org editor asks for archived units too, so it cannot share `orgUnits` with the chart —
66
+ * a toggle that changes what a query asks for has to change the key, or the cache answers the
67
+ * question it was asked last time and the switch appears to do nothing.
68
+ */
69
+ orgUnitsAll: (ws: string) => ['hr', 'org-units', ws, 'with-archived'] as const,
70
+ positions: (ws: string) => ['hr', 'positions', ws, 'with-archived'] as const,
71
+ /** Every period in one read: `periods.list` returns no cursor, so there is nothing to page. */
72
+ periods: (ws: string) => ['hr', 'periods', ws] as const,
73
+ approvalChains: (ws: string) => ['hr', 'approval-chains', ws] as const,
53
74
  } as const
54
75
 
55
76
  /** `YYYY-MM-DD` for a date, in the viewer's own zone rather than UTC. */