@kernhq/module-hr 0.3.0 → 0.5.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.
Files changed (62) hide show
  1. package/dist/contract/attendance.d.ts +240 -0
  2. package/dist/contract/attendance.d.ts.map +1 -0
  3. package/dist/contract/attendance.js +154 -0
  4. package/dist/contract/attendance.js.map +1 -0
  5. package/dist/contract/capabilities.d.ts.map +1 -1
  6. package/dist/contract/capabilities.js +36 -0
  7. package/dist/contract/capabilities.js.map +1 -1
  8. package/dist/contract/events.d.ts +18 -0
  9. package/dist/contract/events.d.ts.map +1 -1
  10. package/dist/contract/events.js +18 -0
  11. package/dist/contract/events.js.map +1 -1
  12. package/dist/contract/index.d.ts +1 -0
  13. package/dist/contract/index.d.ts.map +1 -1
  14. package/dist/contract/index.js +1 -0
  15. package/dist/contract/index.js.map +1 -1
  16. package/dist/contract/permissions.d.ts +43 -0
  17. package/dist/contract/permissions.d.ts.map +1 -1
  18. package/dist/contract/permissions.js +50 -0
  19. package/dist/contract/permissions.js.map +1 -1
  20. package/dist/contract/router.d.ts +996 -0
  21. package/dist/contract/router.d.ts.map +1 -1
  22. package/dist/contract/router.js +165 -1
  23. package/dist/contract/router.js.map +1 -1
  24. package/dist/policy/time.d.ts +48 -0
  25. package/dist/policy/time.d.ts.map +1 -0
  26. package/dist/policy/time.js +144 -0
  27. package/dist/policy/time.js.map +1 -0
  28. package/dist/policy/working-time.d.ts +98 -0
  29. package/dist/policy/working-time.d.ts.map +1 -0
  30. package/dist/policy/working-time.js +190 -0
  31. package/dist/policy/working-time.js.map +1 -0
  32. package/dist/server/index.d.ts.map +1 -1
  33. package/dist/server/index.js +2 -0
  34. package/dist/server/index.js.map +1 -1
  35. package/dist/server/jobs.d.ts +16 -0
  36. package/dist/server/jobs.d.ts.map +1 -0
  37. package/dist/server/jobs.js +166 -0
  38. package/dist/server/jobs.js.map +1 -0
  39. package/dist/server/router.d.ts +1087 -0
  40. package/dist/server/router.d.ts.map +1 -1
  41. package/dist/server/router.js +521 -3
  42. package/dist/server/router.js.map +1 -1
  43. package/dist/server/schema.d.ts +1261 -1
  44. package/dist/server/schema.d.ts.map +1 -1
  45. package/dist/server/schema.js +116 -0
  46. package/dist/server/schema.js.map +1 -1
  47. package/dist/server/services/attendance.d.ts +150 -0
  48. package/dist/server/services/attendance.d.ts.map +1 -0
  49. package/dist/server/services/attendance.js +239 -0
  50. package/dist/server/services/attendance.js.map +1 -0
  51. package/dist/server/services/ledger.d.ts +2 -2
  52. package/migrations/0003_attendance.sql +206 -0
  53. package/migrations/meta/0003_snapshot.json +3681 -0
  54. package/migrations/meta/_journal.json +7 -0
  55. package/package.json +1 -1
  56. package/src/client/index.ts +18 -0
  57. package/src/contract/attendance.ts +180 -0
  58. package/src/contract/capabilities.ts +36 -0
  59. package/src/contract/events.ts +24 -0
  60. package/src/contract/index.ts +1 -0
  61. package/src/contract/permissions.ts +51 -0
  62. package/src/contract/router.ts +203 -1
@@ -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;AAuCvB;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2qFxC"}
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;AA8CvB;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuvGxC"}
@@ -3,10 +3,12 @@ import { implement } from '@orpc/server';
3
3
  import { and, asc, count, desc, eq, gte, ilike, inArray, isNull, lte, or, sql } from 'drizzle-orm';
4
4
  import { HrSettings, hrContract, hrEvents, MODULE_ID } from '../contract/index.js';
5
5
  import { countWorkingDays, workingDays } from '../policy/calendar.js';
6
+ import { businessDateFor } from '../policy/working-time.js';
6
7
  import { COUNTRY_PACKS, packDays } from './packs/index.js';
7
- import { approvalChains, approvalDecisions, approvalRequests, approvalSteps, calendarDays, calendars, costCenters, customFieldDefs, delegations, employments, leaveLedger, leaveRequestDays, leaveRequests, leaveTypes, legalEntities, officeAssignments, offices, orgUnits, people, peopleSensitive, personDocuments, personHistory, positions, } from './schema.js';
8
+ import { approvalChains, approvalDecisions, approvalRequests, approvalSteps, attendanceDays, calendarDays, calendars, costCenters, customFieldDefs, delegations, employments, leaveLedger, leaveRequestDays, leaveRequests, leaveTypes, legalEntities, officeAssignments, offices, orgUnits, people, peopleSensitive, personDocuments, personHistory, positions, punches, regularizations, scheduleAssignments, schedules, } from './schema.js';
8
9
  import { ApprovalService } from './services/approvals.js';
9
- import { inForceOn, todayIso } from './services/db.js';
10
+ import { AttendanceService } from './services/attendance.js';
11
+ import { inForceOn, todayIn, todayIso } from './services/db.js';
10
12
  import { LedgerService, MINUTES_PER_DAY, yearOf } from './services/ledger.js';
11
13
  import { PeopleService } from './services/people.js';
12
14
  import { DEFAULT_WORKING_WEEK, ResolveService } from './services/resolve.js';
@@ -30,6 +32,7 @@ export function implement_(kernel) {
30
32
  const svc = new PeopleService(kernel);
31
33
  const ledger = new LedgerService();
32
34
  const approvals = new ApprovalService(kernel);
35
+ const attendance = new AttendanceService();
33
36
  const db = kernel.database;
34
37
  const settingsOf = (workspaceId) => kernel.settings.module(workspaceId, MODULE_ID, HrSettings);
35
38
  const changed = (workspaceId, entity, id, op) => kernel.realtime.change(workspaceId, { module: MODULE_ID, entity, id, op });
@@ -76,7 +79,32 @@ export function implement_(kernel) {
76
79
  .select({ n: count() })
77
80
  .from(people)
78
81
  .where(and(...where));
79
- return { items: rows.map(PeopleService.toPerson), nextCursor: null, total: total?.n ?? 0 };
82
+ // One query for the whole page rather than a resolution per row: a directory of five
83
+ // hundred people would otherwise be five hundred ladder walks.
84
+ const assignments = rows.length
85
+ ? await tx
86
+ .select({
87
+ personId: officeAssignments.personId,
88
+ officeId: officeAssignments.officeId,
89
+ name: offices.name,
90
+ })
91
+ .from(officeAssignments)
92
+ .innerJoin(offices, eq(offices.id, officeAssignments.officeId))
93
+ .where(and(eq(officeAssignments.workspaceId, input.workspaceId), inArray(officeAssignments.personId, rows.map((r) => r.id)), eq(officeAssignments.isPrimary, true), isNull(officeAssignments.effectiveTo)))
94
+ : [];
95
+ const officeBy = new Map(assignments.map((a) => [a.personId, a]));
96
+ return {
97
+ items: rows.map((r) => ({
98
+ ...PeopleService.toPerson(r),
99
+ // Spreading into a fresh literal drops the branded WorkspaceId that flowed through
100
+ // `toPerson`, so it is restored rather than widened to `string`.
101
+ workspaceId: r.workspaceId,
102
+ officeId: officeBy.get(r.id)?.officeId ?? null,
103
+ officeName: officeBy.get(r.id)?.name ?? null,
104
+ })),
105
+ nextCursor: null,
106
+ total: total?.n ?? 0,
107
+ };
80
108
  })),
81
109
  get: scoped.people.get
82
110
  .use(requires('hr.person.view'))
@@ -1224,6 +1252,334 @@ export function implement_(kernel) {
1224
1252
  return { ok: true };
1225
1253
  }),
1226
1254
  },
1255
+ // ================================================================= attendance
1256
+ attendance: {
1257
+ state: scoped.attendance.state
1258
+ .use(cap('attendance'))
1259
+ .use(requires('hr.attendance.view'))
1260
+ .handler(({ input, context }) => db.withWorkspace(input.workspaceId, async (tx) => {
1261
+ const personId = await personFor(tx, input.workspaceId, context, input.personId);
1262
+ const { timezone, businessDate, schedule } = await clockContext(tx, input.workspaceId, personId);
1263
+ const rows = await attendance.punchesOn(tx, input.workspaceId, personId, businessDate);
1264
+ let open = null;
1265
+ let onBreak = false;
1266
+ let workedMs = 0;
1267
+ let breakOpen = null;
1268
+ for (const r of rows) {
1269
+ if (r.direction === 'in')
1270
+ open = r.at;
1271
+ else if (r.direction === 'out' && open) {
1272
+ workedMs += r.at.getTime() - open.getTime();
1273
+ open = null;
1274
+ }
1275
+ else if (r.direction === 'break_start') {
1276
+ breakOpen = r.at;
1277
+ onBreak = true;
1278
+ }
1279
+ else if (r.direction === 'break_end' && breakOpen) {
1280
+ workedMs -= r.at.getTime() - breakOpen.getTime();
1281
+ breakOpen = null;
1282
+ onBreak = false;
1283
+ }
1284
+ }
1285
+ // An open span counts up to now, so the widget shows time accruing rather than freezing
1286
+ // at the last completed pair.
1287
+ if (open)
1288
+ workedMs += Date.now() - open.getTime();
1289
+ if (breakOpen)
1290
+ workedMs -= Date.now() - breakOpen.getTime();
1291
+ void schedule;
1292
+ return {
1293
+ personId,
1294
+ businessDate,
1295
+ clockedIn: open !== null,
1296
+ onBreak,
1297
+ since: (open ?? breakOpen)?.toISOString() ?? null,
1298
+ workedMinutesToday: Math.max(0, Math.round(workedMs / 60000)),
1299
+ timezone,
1300
+ };
1301
+ })),
1302
+ clockIn: scoped.attendance.clockIn
1303
+ .use(cap('attendance'))
1304
+ .use(requires('hr.attendance.punch'))
1305
+ .handler(({ input, context }) => punch(input, context, 'in')),
1306
+ clockOut: scoped.attendance.clockOut
1307
+ .use(cap('attendance'))
1308
+ .use(requires('hr.attendance.punch'))
1309
+ .handler(({ input, context }) => punch(input, context, 'out')),
1310
+ breakStart: scoped.attendance.breakStart
1311
+ .use(cap('attendance'))
1312
+ .use(requires('hr.attendance.punch'))
1313
+ .handler(({ input, context }) => punch(input, context, 'break_start')),
1314
+ breakEnd: scoped.attendance.breakEnd
1315
+ .use(cap('attendance'))
1316
+ .use(requires('hr.attendance.punch'))
1317
+ .handler(({ input, context }) => punch(input, context, 'break_end')),
1318
+ punches: {
1319
+ list: scoped.attendance.punches.list
1320
+ .use(cap('attendance'))
1321
+ .use(requires('hr.attendance.view'))
1322
+ .handler(({ input, context }) => db.withWorkspace(input.workspaceId, async (tx) => {
1323
+ const personId = await personFor(tx, input.workspaceId, context, input.personId);
1324
+ const where = [
1325
+ eq(punches.workspaceId, input.workspaceId),
1326
+ eq(punches.personId, personId),
1327
+ gte(punches.businessDate, input.from),
1328
+ lte(punches.businessDate, input.to),
1329
+ ];
1330
+ if (!input.includeVoided)
1331
+ where.push(isNull(punches.voidedByPunchId));
1332
+ const rows = await tx
1333
+ .select()
1334
+ .from(punches)
1335
+ .where(and(...where))
1336
+ .orderBy(asc(punches.at))
1337
+ .limit(input.limit);
1338
+ return { items: rows.map(toPunch), nextCursor: null };
1339
+ })),
1340
+ void: scoped.attendance.punches.void
1341
+ .use(cap('attendance'))
1342
+ .use(requires('hr.attendance.manage'))
1343
+ .handler(async ({ input, context }) => {
1344
+ const affected = await db.withWorkspace(input.workspaceId, async (tx) => {
1345
+ const me = await svc.byUserId(tx, input.workspaceId, context.principal.userId ?? '');
1346
+ const { original } = await attendance.voidPunch(tx, input.workspaceId, input.punchId, input.reason, me?.id ?? null);
1347
+ // The day is derived, so voiding a punch means the sheet is stale until it is rebuilt.
1348
+ const { timezone, schedule } = await clockContext(tx, input.workspaceId, original.personId);
1349
+ await attendance.recomputeDay(tx, input.workspaceId, original.personId, original.businessDate, timezone, schedule);
1350
+ return original;
1351
+ });
1352
+ await changed(input.workspaceId, 'attendance_day', affected.personId, 'updated');
1353
+ return { ok: true };
1354
+ }),
1355
+ },
1356
+ days: {
1357
+ list: scoped.attendance.days.list
1358
+ .use(cap('attendance'))
1359
+ .use(requires('hr.attendance.view'))
1360
+ .handler(({ input, context }) => db.withWorkspace(input.workspaceId, async (tx) => {
1361
+ const where = [
1362
+ eq(attendanceDays.workspaceId, input.workspaceId),
1363
+ gte(attendanceDays.businessDate, input.from),
1364
+ lte(attendanceDays.businessDate, input.to),
1365
+ ];
1366
+ if (input.officeId) {
1367
+ const here = await tx
1368
+ .select({ personId: officeAssignments.personId })
1369
+ .from(officeAssignments)
1370
+ .where(and(eq(officeAssignments.workspaceId, input.workspaceId), eq(officeAssignments.officeId, input.officeId), isNull(officeAssignments.effectiveTo)));
1371
+ // Reading a whole office needs the team permission; reading yourself does not.
1372
+ await kernel.authz.require(context.principal, 'hr.attendance.view_team', {
1373
+ kind: 'workspace',
1374
+ id: input.workspaceId,
1375
+ workspaceId: input.workspaceId,
1376
+ });
1377
+ where.push(here.length
1378
+ ? inArray(attendanceDays.personId, here.map((h) => h.personId))
1379
+ : sql `false`);
1380
+ }
1381
+ else {
1382
+ const personId = await personFor(tx, input.workspaceId, context, input.personId);
1383
+ where.push(eq(attendanceDays.personId, personId));
1384
+ }
1385
+ const rows = await tx
1386
+ .select()
1387
+ .from(attendanceDays)
1388
+ .where(and(...where))
1389
+ .orderBy(asc(attendanceDays.businessDate))
1390
+ .limit(input.limit);
1391
+ return { items: rows.map(toAttendanceDay), nextCursor: null };
1392
+ })),
1393
+ recompute: scoped.attendance.days.recompute
1394
+ .use(cap('attendance'))
1395
+ .use(requires('hr.attendance.manage'))
1396
+ .handler(({ input, context }) => db.withWorkspace(input.workspaceId, async (tx) => {
1397
+ const personId = await personFor(tx, input.workspaceId, context, input.personId);
1398
+ const { timezone, schedule } = await clockContext(tx, input.workspaceId, personId);
1399
+ const dates = await attendance.datesWithPunches(tx, input.workspaceId, personId, input.from, input.to);
1400
+ let recomputed = 0;
1401
+ const skippedLocked = [];
1402
+ for (const date of dates) {
1403
+ const r = await attendance.recomputeDay(tx, input.workspaceId, personId, date, timezone, schedule);
1404
+ // Named rather than silently skipped: a recomputation that quietly declines to touch
1405
+ // a closed month looks identical to one that had nothing to do.
1406
+ if (r.locked)
1407
+ skippedLocked.push(date);
1408
+ else
1409
+ recomputed++;
1410
+ }
1411
+ return { recomputed, skippedLocked };
1412
+ })),
1413
+ },
1414
+ schedules: {
1415
+ list: scoped.attendance.schedules.list
1416
+ .use(cap('attendance'))
1417
+ .use(requires('hr.attendance.view'))
1418
+ .handler(({ input }) => db.withWorkspace(input.workspaceId, async (tx) => {
1419
+ const where = [eq(schedules.workspaceId, input.workspaceId)];
1420
+ if (!input.includeArchived)
1421
+ where.push(isNull(schedules.archivedAt));
1422
+ const rows = await tx
1423
+ .select()
1424
+ .from(schedules)
1425
+ .where(and(...where))
1426
+ .orderBy(asc(schedules.name));
1427
+ return rows.map(toSchedule);
1428
+ })),
1429
+ create: scoped.attendance.schedules.create
1430
+ .use(cap('attendance'))
1431
+ .use(requires('hr.attendance.manage'))
1432
+ .handler(async ({ input }) => {
1433
+ const row = await db.withWorkspace(input.workspaceId, async (tx) => {
1434
+ const [created] = await tx
1435
+ .insert(schedules)
1436
+ .values({
1437
+ id: uuidv7(),
1438
+ workspaceId: input.workspaceId,
1439
+ name: input.name,
1440
+ kind: input.kind,
1441
+ week: input.week,
1442
+ tzMode: input.tzMode,
1443
+ tz: input.tz ?? null,
1444
+ graceInMinutes: input.graceInMinutes,
1445
+ graceOutMinutes: input.graceOutMinutes,
1446
+ roundingStepMinutes: input.roundingStepMinutes,
1447
+ roundingDirection: input.roundingDirection,
1448
+ autoClockOutAfterMinutes: input.autoClockOutAfterMinutes ?? null,
1449
+ })
1450
+ .returning();
1451
+ return created;
1452
+ });
1453
+ await changed(input.workspaceId, 'schedule', row.id, 'created');
1454
+ return toSchedule(row);
1455
+ }),
1456
+ update: scoped.attendance.schedules.update
1457
+ .use(cap('attendance'))
1458
+ .use(requires('hr.attendance.manage'))
1459
+ .handler(async ({ input }) => {
1460
+ const row = await db.withWorkspace(input.workspaceId, async (tx) => {
1461
+ const { workspaceId, scheduleId, ...patch } = input;
1462
+ const set = { updatedAt: new Date() };
1463
+ for (const [k, v] of Object.entries(patch))
1464
+ if (v !== undefined)
1465
+ set[k] = v;
1466
+ const [updated] = await tx
1467
+ .update(schedules)
1468
+ .set(set)
1469
+ .where(and(eq(schedules.workspaceId, workspaceId), eq(schedules.id, scheduleId)))
1470
+ .returning();
1471
+ if (!updated)
1472
+ throw KernError.notFound('Schedule');
1473
+ return updated;
1474
+ });
1475
+ await changed(input.workspaceId, 'schedule', row.id, 'updated');
1476
+ return toSchedule(row);
1477
+ }),
1478
+ archive: scoped.attendance.schedules.archive
1479
+ .use(cap('attendance'))
1480
+ .use(requires('hr.attendance.manage'))
1481
+ .handler(async ({ input }) => {
1482
+ await db.withWorkspace(input.workspaceId, (tx) => tx
1483
+ .update(schedules)
1484
+ .set({ archivedAt: new Date() })
1485
+ .where(and(eq(schedules.workspaceId, input.workspaceId), eq(schedules.id, input.scheduleId))));
1486
+ await changed(input.workspaceId, 'schedule', input.scheduleId, 'deleted');
1487
+ return { ok: true };
1488
+ }),
1489
+ assign: scoped.attendance.schedules.assign
1490
+ .use(cap('attendance'))
1491
+ .use(requires('hr.attendance.manage'))
1492
+ .handler(async ({ input }) => {
1493
+ const rows = await db.withWorkspace(input.workspaceId, async (tx) => {
1494
+ // Effective-dated like everything else here: the old assignment is closed the day
1495
+ // before, so "which schedule was she on in March" stays answerable.
1496
+ await tx
1497
+ .update(scheduleAssignments)
1498
+ .set({ effectiveTo: sql `${input.effectiveFrom}::date - 1` })
1499
+ .where(and(eq(scheduleAssignments.workspaceId, input.workspaceId), eq(scheduleAssignments.personId, input.personId), isNull(scheduleAssignments.effectiveTo)));
1500
+ await tx.insert(scheduleAssignments).values({
1501
+ id: uuidv7(),
1502
+ workspaceId: input.workspaceId,
1503
+ personId: input.personId,
1504
+ scheduleId: input.scheduleId,
1505
+ effectiveFrom: input.effectiveFrom,
1506
+ });
1507
+ return tx
1508
+ .select()
1509
+ .from(scheduleAssignments)
1510
+ .where(and(eq(scheduleAssignments.workspaceId, input.workspaceId), eq(scheduleAssignments.personId, input.personId)))
1511
+ .orderBy(desc(scheduleAssignments.effectiveFrom));
1512
+ });
1513
+ await changed(input.workspaceId, 'schedule', input.scheduleId, 'updated');
1514
+ return rows.map(toScheduleAssignment);
1515
+ }),
1516
+ },
1517
+ regularizations: {
1518
+ list: scoped.attendance.regularizations.list
1519
+ .use(cap('attendance'))
1520
+ .use(requires('hr.attendance.view'))
1521
+ .handler(({ input, context }) => db.withWorkspace(input.workspaceId, async (tx) => {
1522
+ const personId = await personFor(tx, input.workspaceId, context, input.personId);
1523
+ const where = [
1524
+ eq(regularizations.workspaceId, input.workspaceId),
1525
+ eq(regularizations.personId, personId),
1526
+ ];
1527
+ if (input.status?.length)
1528
+ where.push(inArray(regularizations.status, input.status));
1529
+ const rows = await tx
1530
+ .select()
1531
+ .from(regularizations)
1532
+ .where(and(...where))
1533
+ .orderBy(desc(regularizations.businessDate))
1534
+ .limit(input.limit);
1535
+ return { items: rows.map(toRegularization), nextCursor: null };
1536
+ })),
1537
+ request: scoped.attendance.regularizations.request
1538
+ .use(cap('attendance'))
1539
+ .use(requires('hr.attendance.punch'))
1540
+ .handler(async ({ input, context }) => {
1541
+ const row = await db.withWorkspace(input.workspaceId, async (tx) => {
1542
+ const personId = await personFor(tx, input.workspaceId, context, input.personId);
1543
+ const [created] = await tx
1544
+ .insert(regularizations)
1545
+ .values({
1546
+ id: uuidv7(),
1547
+ workspaceId: input.workspaceId,
1548
+ personId,
1549
+ businessDate: input.businessDate,
1550
+ punchId: input.punchId ?? null,
1551
+ proposed: input.proposed,
1552
+ reason: input.reason,
1553
+ status: 'pending',
1554
+ })
1555
+ .returning();
1556
+ // The same engine leave uses. That reuse is the reason approvals were built keyed by
1557
+ // subject type rather than bolted onto leave_requests.
1558
+ const raised = await approvals.raise(tx, input.workspaceId, {
1559
+ subjectType: 'regularization',
1560
+ subjectId: created.id,
1561
+ summary: `Correction for ${input.businessDate}`,
1562
+ requesterPersonId: personId,
1563
+ requestedBy: context.principal.userId ?? null,
1564
+ on: input.businessDate,
1565
+ });
1566
+ await tx
1567
+ .update(regularizations)
1568
+ .set({ approvalRequestId: raised.request.id })
1569
+ .where(eq(regularizations.id, created.id));
1570
+ if (raised.autoApproved)
1571
+ await applyRegularization(tx, input.workspaceId, created.id);
1572
+ const [fresh] = await tx
1573
+ .select()
1574
+ .from(regularizations)
1575
+ .where(eq(regularizations.id, created.id));
1576
+ return fresh;
1577
+ });
1578
+ await changed(input.workspaceId, 'regularization', row.id, 'created');
1579
+ return toRegularization(row);
1580
+ }),
1581
+ },
1582
+ },
1227
1583
  // ================================================================= leave
1228
1584
  leave: {
1229
1585
  types: {
@@ -1620,6 +1976,15 @@ export function implement_(kernel) {
1620
1976
  .set({ status: 'rejected', decidedAt: new Date(), updatedAt: new Date() })
1621
1977
  .where(eq(leaveRequests.id, request.subjectId));
1622
1978
  }
1979
+ else if (request.subjectType === 'regularization') {
1980
+ if (result.status === 'approved')
1981
+ await applyRegularization(tx, input.workspaceId, request.subjectId);
1982
+ else if (result.status === 'rejected')
1983
+ await tx
1984
+ .update(regularizations)
1985
+ .set({ status: 'rejected' })
1986
+ .where(eq(regularizations.id, request.subjectId));
1987
+ }
1623
1988
  const [fresh] = await tx
1624
1989
  .select()
1625
1990
  .from(approvalRequests)
@@ -2273,6 +2638,123 @@ export function implement_(kernel) {
2273
2638
  })),
2274
2639
  };
2275
2640
  }
2641
+ /**
2642
+ * Everything a punch needs: the person's zone, today's business date, and their schedule.
2643
+ *
2644
+ * The zone comes from the resolution ladder — their primary office unless they have an override —
2645
+ * so a punch made on a business trip still counts towards the month they are employed in.
2646
+ */
2647
+ async function clockContext(tx, workspaceId, personId) {
2648
+ const today = todayIso();
2649
+ const resolution = await resolve.forPerson(tx, workspaceId, personId, today);
2650
+ const schedule = await attendance.scheduleFor(tx, workspaceId, personId, today);
2651
+ const businessDate = businessDateFor(Date.now(), resolution.timezone, schedule.shiftFor(todayIn(resolution.timezone)));
2652
+ return { timezone: resolution.timezone, businessDate, schedule, resolution };
2653
+ }
2654
+ /**
2655
+ * One punch, and the day rebuilt straight afterwards.
2656
+ *
2657
+ * Recomputing inline rather than in a background job: the clock widget shows a total, and a person
2658
+ * who clocks out wants to see it immediately. The day sheet is a projection, so doing it twice
2659
+ * costs nothing.
2660
+ */
2661
+ async function punch(input, context, direction) {
2662
+ const row = await db.withWorkspace(input.workspaceId, async (tx) => {
2663
+ const personId = await personFor(tx, input.workspaceId, context, input.personId);
2664
+ const { timezone, businessDate, schedule, resolution } = await clockContext(tx, input.workspaceId, personId);
2665
+ // Refuse the transitions that make no sense, with a sentence rather than a constraint error:
2666
+ // clocking in twice, or out when never in, is somebody double-tapping a button.
2667
+ const existing = await attendance.punchesOn(tx, input.workspaceId, personId, businessDate);
2668
+ const open = openState(existing);
2669
+ if (direction === 'in' && open.clockedIn)
2670
+ throw KernError.conflict('You are already clocked in.');
2671
+ if (direction === 'out' && !open.clockedIn)
2672
+ throw KernError.conflict('You are not clocked in.');
2673
+ if (direction === 'break_start' && !open.clockedIn)
2674
+ throw KernError.conflict('Clock in before starting a break.');
2675
+ if (direction === 'break_start' && open.onBreak)
2676
+ throw KernError.conflict('You are already on a break.');
2677
+ if (direction === 'break_end' && !open.onBreak)
2678
+ throw KernError.conflict('You are not on a break.');
2679
+ const punchRow = await attendance.record(tx, input.workspaceId, {
2680
+ personId,
2681
+ direction,
2682
+ timezone,
2683
+ method: input.method ?? 'web',
2684
+ clientReportedAt: input.clientReportedAt ?? null,
2685
+ officeId: resolution.primaryOfficeId,
2686
+ geo: input.geo ?? null,
2687
+ note: input.note ?? null,
2688
+ idempotencyKey: input.idempotencyKey ?? null,
2689
+ }, schedule);
2690
+ await attendance.recomputeDay(tx, input.workspaceId, personId, punchRow.businessDate, timezone, schedule);
2691
+ return punchRow;
2692
+ });
2693
+ await kernel.emit(hrEvents.punchRecorded, {
2694
+ punchId: row.id,
2695
+ workspaceId: input.workspaceId,
2696
+ personId: row.personId,
2697
+ direction: row.direction,
2698
+ businessDate: row.businessDate,
2699
+ }, { workspaceId: input.workspaceId, actorId: context.principal.userId });
2700
+ await changed(input.workspaceId, 'attendance_day', row.personId, 'updated');
2701
+ return toPunch(row);
2702
+ }
2703
+ /** Whether a person is currently clocked in or on a break, from their punches so far. */
2704
+ function openState(rows) {
2705
+ let clockedIn = false;
2706
+ let onBreak = false;
2707
+ for (const r of rows) {
2708
+ if (r.direction === 'in')
2709
+ clockedIn = true;
2710
+ else if (r.direction === 'out') {
2711
+ clockedIn = false;
2712
+ onBreak = false;
2713
+ }
2714
+ else if (r.direction === 'break_start')
2715
+ onBreak = true;
2716
+ else if (r.direction === 'break_end')
2717
+ onBreak = false;
2718
+ }
2719
+ return { clockedIn, onBreak };
2720
+ }
2721
+ /**
2722
+ * Apply an approved correction: write the proposed punches, void what they replace, rebuild.
2723
+ *
2724
+ * Nothing is edited. The original punch keeps its row and gains a pointer to what superseded it,
2725
+ * so a corrected timesheet and an edited one stay distinguishable — which is the entire reason
2726
+ * regularization exists rather than an update statement.
2727
+ */
2728
+ async function applyRegularization(tx, workspaceId, regularizationId) {
2729
+ const [row] = await tx
2730
+ .select()
2731
+ .from(regularizations)
2732
+ .where(and(eq(regularizations.workspaceId, workspaceId), eq(regularizations.id, regularizationId)))
2733
+ .limit(1);
2734
+ if (!row || row.status === 'approved')
2735
+ return;
2736
+ if (row.punchId)
2737
+ await attendance.voidPunch(tx, workspaceId, row.punchId, 'Regularized', null);
2738
+ const { timezone, schedule } = await clockContext(tx, workspaceId, row.personId);
2739
+ for (const proposal of row.proposed)
2740
+ await tx.insert(punches).values({
2741
+ id: uuidv7(),
2742
+ workspaceId,
2743
+ personId: row.personId,
2744
+ direction: proposal.direction,
2745
+ at: new Date(proposal.at),
2746
+ businessDate: row.businessDate,
2747
+ timezone,
2748
+ method: 'manual',
2749
+ trust: 'trusted',
2750
+ note: `Regularization ${row.id}`,
2751
+ });
2752
+ await attendance.recomputeDay(tx, workspaceId, row.personId, row.businessDate, timezone, schedule);
2753
+ await tx
2754
+ .update(regularizations)
2755
+ .set({ status: 'approved', appliedAt: new Date() })
2756
+ .where(eq(regularizations.id, row.id));
2757
+ }
2276
2758
  async function emitCalendarChanged(workspaceId, calendarId, from, to, actorId) {
2277
2759
  await kernel.emit(hrEvents.calendarChanged, { calendarId, workspaceId, from, to }, { workspaceId, actorId });
2278
2760
  await changed(workspaceId, 'calendar', calendarId, 'updated');
@@ -2344,4 +2826,40 @@ const toDelegation = (r) => ({
2344
2826
  subjectType: (r.subjectType ?? null),
2345
2827
  createdAt: r.createdAt.toISOString(),
2346
2828
  });
2829
+ const toPunch = (r) => ({
2830
+ ...r,
2831
+ direction: r.direction,
2832
+ method: r.method,
2833
+ trust: r.trust,
2834
+ geo: r.geo ?? null,
2835
+ at: r.at.toISOString(),
2836
+ clientReportedAt: r.clientReportedAt?.toISOString() ?? null,
2837
+ createdAt: r.createdAt.toISOString(),
2838
+ });
2839
+ const toAttendanceDay = (r) => ({
2840
+ ...r,
2841
+ status: r.status,
2842
+ firstIn: r.firstIn?.toISOString() ?? null,
2843
+ lastOut: r.lastOut?.toISOString() ?? null,
2844
+ computedAt: r.computedAt.toISOString(),
2845
+ });
2846
+ const toSchedule = (r) => ({
2847
+ ...r,
2848
+ kind: r.kind,
2849
+ week: r.week,
2850
+ tzMode: r.tzMode,
2851
+ roundingDirection: r.roundingDirection,
2852
+ archivedAt: r.archivedAt?.toISOString() ?? null,
2853
+ });
2854
+ const toScheduleAssignment = (r) => ({
2855
+ ...r,
2856
+ createdAt: r.createdAt.toISOString(),
2857
+ });
2858
+ const toRegularization = (r) => ({
2859
+ ...r,
2860
+ status: r.status,
2861
+ proposed: r.proposed,
2862
+ appliedAt: r.appliedAt?.toISOString() ?? null,
2863
+ createdAt: r.createdAt.toISOString(),
2864
+ });
2347
2865
  //# sourceMappingURL=router.js.map