@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
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/server/router.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,MAAM,EACX,KAAK,cAAc,EAMpB,MAAM,gBAAgB,CAAA;AA2DvB;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqyHxC"}
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/server/router.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,MAAM,EACX,KAAK,cAAc,EAMpB,MAAM,gBAAgB,CAAA;AA0DvB;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAw4HxC"}
@@ -5,12 +5,11 @@ import { HrSettings, hrContract, hrEvents, MODULE_ID } from '../contract/index.j
5
5
  import { AccrualConfig, CarryForwardConfig, OvertimeConfig, RoundingConfig, WorkingTimeConfig, } from '../contract/policies.js';
6
6
  import { accrueForPeriod } from '../policy/accrual.js';
7
7
  import { countWorkingDays, workingDays } from '../policy/calendar.js';
8
- import { businessDateFor } from '../policy/working-time.js';
9
- import { COUNTRY_PACKS, packDays } from './packs/index.js';
8
+ import { COUNTRY_PACKS, packDays, packFor } from './packs/index.js';
10
9
  import { approvalChains, approvalDecisions, approvalRequests, approvalSteps, attendanceDays, calendarDays, calendars, costCenters, customFieldDefs, delegations, employments, leaveLedger, leaveRequestDays, leaveRequests, leaveTypes, legalEntities, officeAssignments, offices, orgUnits, people, peopleSensitive, periods, personDocuments, personHistory, policies, policyAssignments, positions, punches, regularizations, scheduleAssignments, schedules, } from './schema.js';
11
10
  import { ApprovalService } from './services/approvals.js';
12
11
  import { AttendanceService } from './services/attendance.js';
13
- import { inForceOn, todayIn, todayIso } from './services/db.js';
12
+ import { inForceOn, todayIso } from './services/db.js';
14
13
  import { LedgerService, MINUTES_PER_DAY, yearOf } from './services/ledger.js';
15
14
  import { PeopleService } from './services/people.js';
16
15
  import { hashConfig, PolicyService } from './services/policies.js';
@@ -35,8 +34,8 @@ export function implement_(kernel) {
35
34
  const svc = new PeopleService(kernel);
36
35
  const ledger = new LedgerService();
37
36
  const approvals = new ApprovalService(kernel);
38
- const attendance = new AttendanceService();
39
37
  const policySvc = new PolicyService(resolve);
38
+ const attendance = new AttendanceService(resolve, policySvc);
40
39
  const db = kernel.database;
41
40
  const settingsOf = (workspaceId) => kernel.settings.module(workspaceId, MODULE_ID, HrSettings);
42
41
  const changed = (workspaceId, entity, id, op) => kernel.realtime.change(workspaceId, { module: MODULE_ID, entity, id, op });
@@ -1496,13 +1495,12 @@ export function implement_(kernel) {
1496
1495
  .where(eq(periods.id, period.id))
1497
1496
  .returning();
1498
1497
  // The day sheet carries its own flag so a recomputation does not have to join periods
1499
- // on every row — this is what actually stops a closed month moving.
1500
- const locked = await tx
1501
- .update(attendanceDays)
1502
- .set({ locked: true })
1503
- .where(and(eq(attendanceDays.workspaceId, input.workspaceId), gte(attendanceDays.businessDate, period.startsOn), lte(attendanceDays.businessDate, period.endsOn)))
1504
- .returning({ id: attendanceDays.id });
1505
- return { period: row, lockedDays: locked.length };
1498
+ // on every row — this is what actually stops a closed month moving. It is stamped
1499
+ // through `setPeriodLock`, which is also what keeps the flag scoped to the period's
1500
+ // legal entity: filtering on the date range alone closed January for every entity in
1501
+ // the workspace, while `isLocked` went on answering for one.
1502
+ const lockedDays = await attendance.setPeriodLock(tx, input.workspaceId, period, true);
1503
+ return { period: row, lockedDays };
1506
1504
  });
1507
1505
  await changed(input.workspaceId, 'period', input.periodId, 'updated');
1508
1506
  return { ...toPeriod(result.period), lockedDays: result.lockedDays };
@@ -1523,20 +1521,16 @@ export function implement_(kernel) {
1523
1521
  })
1524
1522
  .where(eq(periods.id, period.id))
1525
1523
  .returning();
1526
- const unlocked = await tx
1527
- .update(attendanceDays)
1528
- .set({ locked: false })
1529
- .where(and(eq(attendanceDays.workspaceId, input.workspaceId), gte(attendanceDays.businessDate, period.startsOn), lte(attendanceDays.businessDate, period.endsOn)))
1530
- .returning({ id: attendanceDays.id });
1524
+ const unlockedDays = await attendance.setPeriodLock(tx, input.workspaceId, period, false);
1531
1525
  kernel.log.warn({
1532
1526
  module: MODULE_ID,
1533
1527
  workspaceId: input.workspaceId,
1534
1528
  periodId: period.id,
1535
- days: unlocked.length,
1529
+ days: unlockedDays,
1536
1530
  actorId: context.principal.userId,
1537
1531
  reason: input.reason,
1538
1532
  }, 'payroll period reopened');
1539
- return { period: row, unlockedDays: unlocked.length };
1533
+ return { period: row, unlockedDays };
1540
1534
  });
1541
1535
  await changed(input.workspaceId, 'period', input.periodId, 'updated');
1542
1536
  return { ...toPeriod(result.period), unlockedDays: result.unlockedDays };
@@ -1549,13 +1543,16 @@ export function implement_(kernel) {
1549
1543
  .use(requires('hr.attendance.view'))
1550
1544
  .handler(({ input, context }) => db.withWorkspace(input.workspaceId, async (tx) => {
1551
1545
  const personId = await personFor(tx, input.workspaceId, context, input.personId);
1552
- const { timezone, businessDate, schedule } = await clockContext(tx, input.workspaceId, personId);
1553
- const rows = await attendance.punchesOn(tx, input.workspaceId, personId, businessDate);
1546
+ // The same attribution the next punch will be judged against, so what a person is shown
1547
+ // and what they are then allowed to do cannot disagree. Somebody who forgot to clock out
1548
+ // last night is told they are still on last night's shift, which is both true and the
1549
+ // thing they need to know before pressing anything.
1550
+ const { timezone, at, attribution } = await clockNow(tx, input.workspaceId, personId);
1554
1551
  let open = null;
1555
1552
  let onBreak = false;
1556
1553
  let workedMs = 0;
1557
1554
  let breakOpen = null;
1558
- for (const r of rows) {
1555
+ for (const r of attribution.punches) {
1559
1556
  if (r.direction === 'in')
1560
1557
  open = r.at;
1561
1558
  else if (r.direction === 'out' && open) {
@@ -1573,15 +1570,15 @@ export function implement_(kernel) {
1573
1570
  }
1574
1571
  }
1575
1572
  // An open span counts up to now, so the widget shows time accruing rather than freezing
1576
- // at the last completed pair.
1573
+ // at the last completed pair. `at` rather than a fresh `Date.now()`: one instant per
1574
+ // request, the one the attribution was made from.
1577
1575
  if (open)
1578
- workedMs += Date.now() - open.getTime();
1576
+ workedMs += at.getTime() - open.getTime();
1579
1577
  if (breakOpen)
1580
- workedMs -= Date.now() - breakOpen.getTime();
1581
- void schedule;
1578
+ workedMs -= at.getTime() - breakOpen.getTime();
1582
1579
  return {
1583
1580
  personId,
1584
- businessDate,
1581
+ businessDate: attribution.businessDate,
1585
1582
  clockedIn: open !== null,
1586
1583
  onBreak,
1587
1584
  since: (open ?? breakOpen)?.toISOString() ?? null,
@@ -1635,7 +1632,7 @@ export function implement_(kernel) {
1635
1632
  const me = await svc.byUserId(tx, input.workspaceId, context.principal.userId ?? '');
1636
1633
  const { original } = await attendance.voidPunch(tx, input.workspaceId, input.punchId, input.reason, me?.id ?? null);
1637
1634
  // The day is derived, so voiding a punch means the sheet is stale until it is rebuilt.
1638
- const { timezone, schedule } = await clockContext(tx, input.workspaceId, original.personId);
1635
+ const { timezone, schedule } = await personContext(tx, input.workspaceId, original.personId);
1639
1636
  await attendance.recomputeDay(tx, input.workspaceId, original.personId, original.businessDate, timezone, schedule);
1640
1637
  return original;
1641
1638
  });
@@ -1685,7 +1682,7 @@ export function implement_(kernel) {
1685
1682
  .use(requires('hr.attendance.manage'))
1686
1683
  .handler(({ input, context }) => db.withWorkspace(input.workspaceId, async (tx) => {
1687
1684
  const personId = await personFor(tx, input.workspaceId, context, input.personId);
1688
- const { timezone, schedule } = await clockContext(tx, input.workspaceId, personId);
1685
+ const { timezone, schedule } = await personContext(tx, input.workspaceId, personId);
1689
1686
  const dates = await attendance.datesWithPunches(tx, input.workspaceId, personId, input.from, input.to);
1690
1687
  let recomputed = 0;
1691
1688
  const skippedLocked = [];
@@ -2061,7 +2058,12 @@ export function implement_(kernel) {
2061
2058
  await ledger.lockAndRead(tx, input.workspaceId, personId, input.leaveTypeId, year);
2062
2059
  const sim = await simulate(tx, input.workspaceId, personId, input);
2063
2060
  if (sim.blockers.length)
2064
- throw KernError.conflict(sim.blockers[0].message, `hr.leave.${sim.blockers[0].code}`);
2061
+ // `details`, for the reason `refusePunch` gives: `KernError.conflict`'s second
2062
+ // argument is kept on the error object and never serialised, so the blocker code
2063
+ // this line has always meant to send was reaching nobody.
2064
+ throw new KernError('CONFLICT', sim.blockers[0].message, {
2065
+ reason: `hr.leave.${sim.blockers[0].code}`,
2066
+ });
2065
2067
  const [request] = await tx
2066
2068
  .insert(leaveRequests)
2067
2069
  .values({
@@ -2130,8 +2132,14 @@ export function implement_(kernel) {
2130
2132
  .handler(async ({ input, context }) => {
2131
2133
  const row = await db.withWorkspace(input.workspaceId, async (tx) => {
2132
2134
  const request = await loadRequest(tx, input.workspaceId, input.requestId);
2133
- if (request.status === 'cancelled' || request.status === 'withdrawn')
2134
- throw KernError.conflict('That request is already cancelled');
2135
+ // A reason beside the sentence, because the sentence is English and the reason is what
2136
+ // a client can translate. Two states, not one: "withdrawn" is the requester taking it
2137
+ // back and "cancelled" is somebody else doing so, and telling a person their own
2138
+ // withdrawal was "already cancelled" is a small lie about who did what.
2139
+ if (request.status === 'cancelled')
2140
+ throw KernError.conflict('That request is already cancelled.', 'hr.leave.already_cancelled');
2141
+ if (request.status === 'withdrawn')
2142
+ throw KernError.conflict('That request was already withdrawn.', 'hr.leave.already_withdrawn');
2135
2143
  const year = yearOf(request.startsOn);
2136
2144
  await ledger.lockAndRead(tx, input.workspaceId, request.personId, request.leaveTypeId, year);
2137
2145
  // Approved leave is *reversed*, not deleted. "She booked it and cancelled" and "she
@@ -2647,7 +2655,31 @@ export function implement_(kernel) {
2647
2655
  * company's own holidays during a routine yearly refresh, silently, months before anyone notices.
2648
2656
  * Showing exactly which days survive is how an administrator can believe the operation.
2649
2657
  */
2658
+ /**
2659
+ * The days a pack would add, change and drop — and the days it must never touch.
2660
+ *
2661
+ * A pack key nobody publishes is refused rather than treated as a pack with no days in it.
2662
+ * `packDays` answers `[]` for an unknown key, which made the diff for a typo "add nothing, change
2663
+ * nothing, **remove every national holiday**" — with Apply live beside it, and `keptCustom` still
2664
+ * promising the company's own days were safe, which was true and beside the point. The screen
2665
+ * could reach that state on its own: `COUNTRY_PACKS` is keyed `TR`, the editor prefilled the
2666
+ * calendar's country lowercased, and so a calendar made by hand offered to empty itself.
2667
+ *
2668
+ * This is the one operation in the module that deletes rows a customer did not ask it to, so it
2669
+ * fails closed: an unknown key is a mistake, never an instruction.
2670
+ */
2671
+ function requirePack(packKey) {
2672
+ // `packFor` states the rule and is tested beside the packs; this turns its refusal into the
2673
+ // error shape a client can render.
2674
+ try {
2675
+ packFor(packKey);
2676
+ }
2677
+ catch (error) {
2678
+ throw KernError.badRequest(error.message);
2679
+ }
2680
+ }
2650
2681
  async function diffPack(tx, workspaceId, calendarId, packKey, year) {
2682
+ requirePack(packKey);
2651
2683
  const from = `${year}-01-01`;
2652
2684
  const to = `${year}-12-31`;
2653
2685
  const existing = await tx
@@ -2946,17 +2978,55 @@ export function implement_(kernel) {
2946
2978
  };
2947
2979
  }
2948
2980
  /**
2949
- * Everything a punch needs: the person's zone, today's business date, and their schedule.
2981
+ * Everything a punch needs about a person: their zone, and the schedule that shapes their day.
2950
2982
  *
2951
2983
  * The zone comes from the resolution ladder — their primary office unless they have an override —
2952
2984
  * so a punch made on a business trip still counts towards the month they are employed in.
2985
+ *
2986
+ * Everything here is resolved **as of today**, which is what a punch is about. It is therefore
2987
+ * not the place to answer a question about a past date: this used to hand out today's legal
2988
+ * entity as well, and three callers applied it to business dates months back — so a person who
2989
+ * transferred entity had a filed month recomputed against the one they are in now. `recomputeDay`
2990
+ * asks that question of the day it is rebuilding.
2953
2991
  */
2954
- async function clockContext(tx, workspaceId, personId) {
2992
+ async function personContext(tx, workspaceId, personId) {
2955
2993
  const today = todayIso();
2956
2994
  const resolution = await resolve.forPerson(tx, workspaceId, personId, today);
2957
2995
  const schedule = await attendance.scheduleFor(tx, workspaceId, personId, today);
2958
- const businessDate = businessDateFor(Date.now(), resolution.timezone, schedule.shiftFor(todayIn(resolution.timezone)));
2959
- return { timezone: resolution.timezone, businessDate, schedule, resolution };
2996
+ return { timezone: resolution.timezone, schedule, resolution };
2997
+ }
2998
+ /**
2999
+ * The same, plus the one decision every clock procedure has to agree about: which shift *this
3000
+ * instant* belongs to, and what is already filed on it.
3001
+ *
3002
+ * The instant is taken once, here, and travels with its answer — so the guard, the row that is
3003
+ * written and the figures the widget shows all come out of a single attribution. They used to be
3004
+ * three: `clockContext` attributed `Date.now()`, the guard re-read the punches of whatever date
3005
+ * that produced, and `record` attributed its own `new Date()` again on the way to the insert.
3006
+ * Every version of the night-shift bug is those answers disagreeing, and a person being told they
3007
+ * are not clocked in at the end of a shift they have just worked.
3008
+ */
3009
+ async function clockNow(tx, workspaceId, personId) {
3010
+ const context = await personContext(tx, workspaceId, personId);
3011
+ const at = new Date();
3012
+ const attribution = await attendance.attribute(tx, workspaceId, personId, at.getTime(), context.timezone, context.schedule);
3013
+ return { ...context, at, attribution };
3014
+ }
3015
+ /**
3016
+ * A refused punch: the sentence for the reader, and a stable reason for the screen.
3017
+ *
3018
+ * The reason travels in `details`, not in `KernError.conflict`'s `reason` argument. Only
3019
+ * `details` is on the wire — `kernErrorToORPC` maps it to the oRPC error's `data` and drops
3020
+ * everything else — so a reason passed as `conflict(message, reason)` never leaves this process.
3021
+ * Reasons are dotted and namespaced like `hr.leave.*` above, and are API: renaming one silently
3022
+ * puts the English sentence back in front of every non-English reader.
3023
+ *
3024
+ * A `function`, like every other helper down here: these sit after the router's `return`, so a
3025
+ * `const` is never initialised and the first refusal throws a ReferenceError instead of a
3026
+ * conflict. `noUnreachable` is what catches that — nothing else does until a person punches.
3027
+ */
3028
+ function refusePunch(reason, message) {
3029
+ return new KernError('CONFLICT', message, { reason });
2960
3030
  }
2961
3031
  /**
2962
3032
  * One punch, and the day rebuilt straight afterwards.
@@ -2968,24 +3038,35 @@ export function implement_(kernel) {
2968
3038
  async function punch(input, context, direction) {
2969
3039
  const row = await db.withWorkspace(input.workspaceId, async (tx) => {
2970
3040
  const personId = await personFor(tx, input.workspaceId, context, input.personId);
2971
- const { timezone, businessDate, schedule, resolution } = await clockContext(tx, input.workspaceId, personId);
3041
+ const { timezone, schedule, resolution, at, attribution } = await clockNow(tx, input.workspaceId, personId);
2972
3042
  // Refuse the transitions that make no sense, with a sentence rather than a constraint error:
2973
3043
  // clocking in twice, or out when never in, is somebody double-tapping a button.
2974
- const existing = await attendance.punchesOn(tx, input.workspaceId, personId, businessDate);
2975
- const open = openState(existing);
3044
+ //
3045
+ // The sentence is written for a person, and it is written in English — a router has no locale.
3046
+ // So every refusal also carries a *reason*, which is what the clock widget translates; the
3047
+ // sentence is what it falls back to for a reason it has no string for, so a sixth refusal
3048
+ // added here reaches the reader in English rather than not at all.
3049
+ //
3050
+ // What it is judged against comes out of the attribution rather than being fetched again.
3051
+ // Re-reading `punchesOn(businessDate)` here is what made the guard circular: it could only
3052
+ // ever confirm the date the attribution had already chosen, so a wrong choice arrived as
3053
+ // "You are not clocked in." at the end of a shift somebody had just worked.
3054
+ const open = attribution.open;
2976
3055
  if (direction === 'in' && open.clockedIn)
2977
- throw KernError.conflict('You are already clocked in.');
3056
+ throw refusePunch('hr.clock.already_clocked_in', 'You are already clocked in.');
2978
3057
  if (direction === 'out' && !open.clockedIn)
2979
- throw KernError.conflict('You are not clocked in.');
3058
+ throw refusePunch('hr.clock.not_clocked_in', 'You are not clocked in.');
2980
3059
  if (direction === 'break_start' && !open.clockedIn)
2981
- throw KernError.conflict('Clock in before starting a break.');
3060
+ throw refusePunch('hr.clock.break_before_clock_in', 'Clock in before starting a break.');
2982
3061
  if (direction === 'break_start' && open.onBreak)
2983
- throw KernError.conflict('You are already on a break.');
3062
+ throw refusePunch('hr.clock.already_on_break', 'You are already on a break.');
2984
3063
  if (direction === 'break_end' && !open.onBreak)
2985
- throw KernError.conflict('You are not on a break.');
3064
+ throw refusePunch('hr.clock.not_on_break', 'You are not on a break.');
2986
3065
  const punchRow = await attendance.record(tx, input.workspaceId, {
2987
3066
  personId,
2988
3067
  direction,
3068
+ at,
3069
+ businessDate: attribution.businessDate,
2989
3070
  timezone,
2990
3071
  method: input.method ?? 'web',
2991
3072
  clientReportedAt: input.clientReportedAt ?? null,
@@ -2993,7 +3074,10 @@ export function implement_(kernel) {
2993
3074
  geo: input.geo ?? null,
2994
3075
  note: input.note ?? null,
2995
3076
  idempotencyKey: input.idempotencyKey ?? null,
2996
- }, schedule);
3077
+ });
3078
+ // A punch inside a closed month is still recorded — it is a fact about somebody's day, and
3079
+ // refusing it because payroll has been filed loses the fact to protect the report. What must
3080
+ // not move is the derived sheet, and `recomputeDay` is where that is decided.
2997
3081
  await attendance.recomputeDay(tx, input.workspaceId, personId, punchRow.businessDate, timezone, schedule);
2998
3082
  return punchRow;
2999
3083
  });
@@ -3007,24 +3091,6 @@ export function implement_(kernel) {
3007
3091
  await changed(input.workspaceId, 'attendance_day', row.personId, 'updated');
3008
3092
  return toPunch(row);
3009
3093
  }
3010
- /** Whether a person is currently clocked in or on a break, from their punches so far. */
3011
- function openState(rows) {
3012
- let clockedIn = false;
3013
- let onBreak = false;
3014
- for (const r of rows) {
3015
- if (r.direction === 'in')
3016
- clockedIn = true;
3017
- else if (r.direction === 'out') {
3018
- clockedIn = false;
3019
- onBreak = false;
3020
- }
3021
- else if (r.direction === 'break_start')
3022
- onBreak = true;
3023
- else if (r.direction === 'break_end')
3024
- onBreak = false;
3025
- }
3026
- return { clockedIn, onBreak };
3027
- }
3028
3094
  /**
3029
3095
  * Apply an approved correction: write the proposed punches, void what they replace, rebuild.
3030
3096
  *
@@ -3042,7 +3108,7 @@ export function implement_(kernel) {
3042
3108
  return;
3043
3109
  if (row.punchId)
3044
3110
  await attendance.voidPunch(tx, workspaceId, row.punchId, 'Regularized', null);
3045
- const { timezone, schedule } = await clockContext(tx, workspaceId, row.personId);
3111
+ const { timezone, schedule } = await personContext(tx, workspaceId, row.personId);
3046
3112
  for (const proposal of row.proposed)
3047
3113
  await tx.insert(punches).values({
3048
3114
  id: uuidv7(),
@@ -3121,6 +3187,30 @@ export function implement_(kernel) {
3121
3187
  .from(employments)
3122
3188
  .where(and(eq(employments.workspaceId, workspaceId), inArray(employments.personId, ids), inForceOn(employments.effectiveFrom, employments.effectiveTo, to)));
3123
3189
  const employmentBy = new Map(employmentRows.map((e) => [e.personId, e]));
3190
+ /**
3191
+ * Hours worked in the window, for the frequency that accrues against them.
3192
+ *
3193
+ * The preview has to answer with the same numbers the job will credit, so it reads the same
3194
+ * inputs. `per_hour_worked` was fed by neither for as long as it existed, which made the
3195
+ * preview quietly agree with a job that granted nothing. Only queried where a policy asks.
3196
+ */
3197
+ const hoursBy = new Map();
3198
+ if ([...resolved.values()].some((p) => p?.config?.frequency === 'per_hour_worked')) {
3199
+ const rows = await tx
3200
+ .select({
3201
+ personId: attendanceDays.personId,
3202
+ worked: sql `coalesce(sum(${attendanceDays.workedMinutes}), 0)`,
3203
+ scheduled: sql `coalesce(sum(${attendanceDays.scheduledMinutes}), 0)`,
3204
+ })
3205
+ .from(attendanceDays)
3206
+ .where(and(eq(attendanceDays.workspaceId, workspaceId), inArray(attendanceDays.personId, ids), gte(attendanceDays.businessDate, from), lte(attendanceDays.businessDate, to)))
3207
+ .groupBy(attendanceDays.personId);
3208
+ for (const r of rows)
3209
+ hoursBy.set(r.personId, {
3210
+ workedMinutes: Number(r.worked),
3211
+ scheduledMinutes: Number(r.scheduled),
3212
+ });
3213
+ }
3124
3214
  const types = await tx
3125
3215
  .select()
3126
3216
  .from(leaveTypes)
@@ -3175,6 +3265,7 @@ export function implement_(kernel) {
3175
3265
  hiredOn: person.hiredOn,
3176
3266
  terminatedOn: person.terminatedOn,
3177
3267
  fte: employment ? Number.parseFloat(employment.fte ?? '1') : 1,
3268
+ ...hoursBy.get(person.id),
3178
3269
  });
3179
3270
  const alreadyAccrued = already.has(`${person.id}:${type.id}`);
3180
3271
  rows.push({