@kernhq/module-hr 0.9.3 → 0.10.1

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/approvals.d.ts +3 -0
  2. package/dist/contract/approvals.d.ts.map +1 -1
  3. package/dist/contract/approvals.js +13 -1
  4. package/dist/contract/approvals.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 +9 -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 +458 -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 +1896 -111
  28. package/dist/server/router.d.ts.map +1 -1
  29. package/dist/server/router.js +146 -63
  30. package/dist/server/router.js.map +1 -1
  31. package/dist/server/schema.d.ts +53 -0
  32. package/dist/server/schema.d.ts.map +1 -1
  33. package/dist/server/schema.js +101 -2
  34. package/dist/server/schema.js.map +1 -1
  35. package/dist/server/services/approvals.d.ts +14 -0
  36. package/dist/server/services/approvals.d.ts.map +1 -1
  37. package/dist/server/services/approvals.js +14 -3
  38. package/dist/server/services/approvals.js.map +1 -1
  39. package/dist/server/services/attendance.d.ts +120 -6
  40. package/dist/server/services/attendance.d.ts.map +1 -1
  41. package/dist/server/services/attendance.js +226 -13
  42. package/dist/server/services/attendance.js.map +1 -1
  43. package/dist/server/services/policies.d.ts +6 -0
  44. package/dist/server/services/policies.d.ts.map +1 -1
  45. package/dist/server/services/policies.js +6 -0
  46. package/dist/server/services/policies.js.map +1 -1
  47. package/migrations/0005_approval_requester.sql +30 -0
  48. package/migrations/0006_schedule_no_overlap.sql +85 -0
  49. package/migrations/0007_hot_path_indexes.sql +38 -0
  50. package/migrations/0009_beyond_cap_minutes.sql +15 -0
  51. package/migrations/meta/0005_snapshot.json +4047 -0
  52. package/migrations/meta/0007_snapshot.json +4225 -0
  53. package/migrations/meta/0009_snapshot.json +4231 -0
  54. package/migrations/meta/_journal.json +28 -0
  55. package/package.json +7 -7
  56. package/src/client/capabilities.ts +34 -0
  57. package/src/client/components/ClockControls.svelte +127 -20
  58. package/src/client/components/ClockControls.test.ts +446 -0
  59. package/src/client/components/DecisionDialog.svelte +90 -0
  60. package/src/client/components/DelegationDialog.svelte +219 -0
  61. package/src/client/components/LeaveRequestDialog.svelte +254 -25
  62. package/src/client/components/PersonFormDialog.svelte +53 -3
  63. package/src/client/components/PersonPanel.svelte +134 -26
  64. package/src/client/i18n.ts +5 -625
  65. package/src/client/index.ts +1 -20
  66. package/src/client/messages.test.ts +154 -0
  67. package/src/client/messages.ts +3388 -0
  68. package/src/client/mock.ts +1691 -162
  69. package/src/client/module.ts +17 -2
  70. package/src/client/pages/ApprovalsPage.svelte +332 -60
  71. package/src/client/pages/AttendancePage.svelte +106 -20
  72. package/src/client/pages/DirectoryPage.svelte +192 -55
  73. package/src/client/pages/LeavePage.svelte +280 -25
  74. package/src/client/pages/OfficesPage.svelte +181 -39
  75. package/src/client/pages/leave-and-attendance.test.ts +588 -0
  76. package/src/client/permissions.ts +2 -1
  77. package/src/client/query.ts +22 -1
  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 +43 -0
  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/approvals.ts +13 -1
  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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmxHxC"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAm2HxC"}
@@ -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 = [];
@@ -1849,6 +1846,7 @@ export function implement_(kernel) {
1849
1846
  subjectType: 'regularization',
1850
1847
  subjectId: created.id,
1851
1848
  summary: `Correction for ${input.businessDate}`,
1849
+ summaryParams: { date: input.businessDate },
1852
1850
  requesterPersonId: personId,
1853
1851
  requestedBy: context.principal.userId ?? null,
1854
1852
  on: input.businessDate,
@@ -2060,7 +2058,12 @@ export function implement_(kernel) {
2060
2058
  await ledger.lockAndRead(tx, input.workspaceId, personId, input.leaveTypeId, year);
2061
2059
  const sim = await simulate(tx, input.workspaceId, personId, input);
2062
2060
  if (sim.blockers.length)
2063
- 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
+ });
2064
2067
  const [request] = await tx
2065
2068
  .insert(leaveRequests)
2066
2069
  .values({
@@ -2097,6 +2100,7 @@ export function implement_(kernel) {
2097
2100
  subjectType: 'leave',
2098
2101
  subjectId: request.id,
2099
2102
  summary: `${sim.workingDays} day(s) from ${input.startsOn}`,
2103
+ summaryParams: { days: sim.workingDays, from: input.startsOn, to: input.endsOn },
2100
2104
  requesterPersonId: personId,
2101
2105
  requestedBy: context.principal.userId ?? null,
2102
2106
  on: input.startsOn,
@@ -2128,8 +2132,14 @@ export function implement_(kernel) {
2128
2132
  .handler(async ({ input, context }) => {
2129
2133
  const row = await db.withWorkspace(input.workspaceId, async (tx) => {
2130
2134
  const request = await loadRequest(tx, input.workspaceId, input.requestId);
2131
- if (request.status === 'cancelled' || request.status === 'withdrawn')
2132
- 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');
2133
2143
  const year = yearOf(request.startsOn);
2134
2144
  await ledger.lockAndRead(tx, input.workspaceId, request.personId, request.leaveTypeId, year);
2135
2145
  // Approved leave is *reversed*, not deleted. "She booked it and cancelled" and "she
@@ -2645,7 +2655,31 @@ export function implement_(kernel) {
2645
2655
  * company's own holidays during a routine yearly refresh, silently, months before anyone notices.
2646
2656
  * Showing exactly which days survive is how an administrator can believe the operation.
2647
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
+ }
2648
2681
  async function diffPack(tx, workspaceId, calendarId, packKey, year) {
2682
+ requirePack(packKey);
2649
2683
  const from = `${year}-01-01`;
2650
2684
  const to = `${year}-12-31`;
2651
2685
  const existing = await tx
@@ -2896,6 +2930,13 @@ export function implement_(kernel) {
2896
2930
  .where(and(eq(approvalChains.workspaceId, workspaceId), eq(approvalChains.subjectType, subjectType), eq(approvalChains.isDefault, true)));
2897
2931
  }
2898
2932
  /** An approval request with its steps and decisions, which is the only useful shape. */
2933
+ /**
2934
+ * One request, with its steps, its decisions and the requester's name.
2935
+ *
2936
+ * The name is resolved here rather than left to the client: an inbox is one call, and a client
2937
+ * that has to fetch the directory to read it will show ids for the moment the directory is in
2938
+ * flight — which is the moment somebody clicks approve.
2939
+ */
2899
2940
  async function hydrateApproval(tx, row) {
2900
2941
  const steps = await tx
2901
2942
  .select()
@@ -2906,10 +2947,18 @@ export function implement_(kernel) {
2906
2947
  const decisions = stepIds.length
2907
2948
  ? await tx.select().from(approvalDecisions).where(inArray(approvalDecisions.stepId, stepIds))
2908
2949
  : [];
2950
+ const [requester] = row.requesterPersonId
2951
+ ? await tx
2952
+ .select({ displayName: people.displayName })
2953
+ .from(people)
2954
+ .where(and(eq(people.workspaceId, row.workspaceId), eq(people.id, row.requesterPersonId)))
2955
+ .limit(1)
2956
+ : [];
2909
2957
  return {
2910
2958
  ...row,
2911
2959
  subjectType: row.subjectType,
2912
2960
  status: row.status,
2961
+ requesterName: requester?.displayName ?? null,
2913
2962
  requestedAt: row.requestedAt.toISOString(),
2914
2963
  decidedAt: row.decidedAt?.toISOString() ?? null,
2915
2964
  steps: steps.map((s) => ({
@@ -2929,17 +2978,55 @@ export function implement_(kernel) {
2929
2978
  };
2930
2979
  }
2931
2980
  /**
2932
- * 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.
2933
2982
  *
2934
2983
  * The zone comes from the resolution ladder — their primary office unless they have an override —
2935
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.
2936
2991
  */
2937
- async function clockContext(tx, workspaceId, personId) {
2992
+ async function personContext(tx, workspaceId, personId) {
2938
2993
  const today = todayIso();
2939
2994
  const resolution = await resolve.forPerson(tx, workspaceId, personId, today);
2940
2995
  const schedule = await attendance.scheduleFor(tx, workspaceId, personId, today);
2941
- const businessDate = businessDateFor(Date.now(), resolution.timezone, schedule.shiftFor(todayIn(resolution.timezone)));
2942
- 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 });
2943
3030
  }
2944
3031
  /**
2945
3032
  * One punch, and the day rebuilt straight afterwards.
@@ -2951,24 +3038,35 @@ export function implement_(kernel) {
2951
3038
  async function punch(input, context, direction) {
2952
3039
  const row = await db.withWorkspace(input.workspaceId, async (tx) => {
2953
3040
  const personId = await personFor(tx, input.workspaceId, context, input.personId);
2954
- const { timezone, businessDate, schedule, resolution } = await clockContext(tx, input.workspaceId, personId);
3041
+ const { timezone, schedule, resolution, at, attribution } = await clockNow(tx, input.workspaceId, personId);
2955
3042
  // Refuse the transitions that make no sense, with a sentence rather than a constraint error:
2956
3043
  // clocking in twice, or out when never in, is somebody double-tapping a button.
2957
- const existing = await attendance.punchesOn(tx, input.workspaceId, personId, businessDate);
2958
- 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;
2959
3055
  if (direction === 'in' && open.clockedIn)
2960
- throw KernError.conflict('You are already clocked in.');
3056
+ throw refusePunch('hr.clock.already_clocked_in', 'You are already clocked in.');
2961
3057
  if (direction === 'out' && !open.clockedIn)
2962
- throw KernError.conflict('You are not clocked in.');
3058
+ throw refusePunch('hr.clock.not_clocked_in', 'You are not clocked in.');
2963
3059
  if (direction === 'break_start' && !open.clockedIn)
2964
- throw KernError.conflict('Clock in before starting a break.');
3060
+ throw refusePunch('hr.clock.break_before_clock_in', 'Clock in before starting a break.');
2965
3061
  if (direction === 'break_start' && open.onBreak)
2966
- throw KernError.conflict('You are already on a break.');
3062
+ throw refusePunch('hr.clock.already_on_break', 'You are already on a break.');
2967
3063
  if (direction === 'break_end' && !open.onBreak)
2968
- throw KernError.conflict('You are not on a break.');
3064
+ throw refusePunch('hr.clock.not_on_break', 'You are not on a break.');
2969
3065
  const punchRow = await attendance.record(tx, input.workspaceId, {
2970
3066
  personId,
2971
3067
  direction,
3068
+ at,
3069
+ businessDate: attribution.businessDate,
2972
3070
  timezone,
2973
3071
  method: input.method ?? 'web',
2974
3072
  clientReportedAt: input.clientReportedAt ?? null,
@@ -2976,7 +3074,10 @@ export function implement_(kernel) {
2976
3074
  geo: input.geo ?? null,
2977
3075
  note: input.note ?? null,
2978
3076
  idempotencyKey: input.idempotencyKey ?? null,
2979
- }, 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.
2980
3081
  await attendance.recomputeDay(tx, input.workspaceId, personId, punchRow.businessDate, timezone, schedule);
2981
3082
  return punchRow;
2982
3083
  });
@@ -2990,24 +3091,6 @@ export function implement_(kernel) {
2990
3091
  await changed(input.workspaceId, 'attendance_day', row.personId, 'updated');
2991
3092
  return toPunch(row);
2992
3093
  }
2993
- /** Whether a person is currently clocked in or on a break, from their punches so far. */
2994
- function openState(rows) {
2995
- let clockedIn = false;
2996
- let onBreak = false;
2997
- for (const r of rows) {
2998
- if (r.direction === 'in')
2999
- clockedIn = true;
3000
- else if (r.direction === 'out') {
3001
- clockedIn = false;
3002
- onBreak = false;
3003
- }
3004
- else if (r.direction === 'break_start')
3005
- onBreak = true;
3006
- else if (r.direction === 'break_end')
3007
- onBreak = false;
3008
- }
3009
- return { clockedIn, onBreak };
3010
- }
3011
3094
  /**
3012
3095
  * Apply an approved correction: write the proposed punches, void what they replace, rebuild.
3013
3096
  *
@@ -3025,7 +3108,7 @@ export function implement_(kernel) {
3025
3108
  return;
3026
3109
  if (row.punchId)
3027
3110
  await attendance.voidPunch(tx, workspaceId, row.punchId, 'Regularized', null);
3028
- const { timezone, schedule } = await clockContext(tx, workspaceId, row.personId);
3111
+ const { timezone, schedule } = await personContext(tx, workspaceId, row.personId);
3029
3112
  for (const proposal of row.proposed)
3030
3113
  await tx.insert(punches).values({
3031
3114
  id: uuidv7(),