@kernhq/module-hr 0.3.0 → 0.4.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 (61) 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 +994 -0
  21. package/dist/contract/router.d.ts.map +1 -1
  22. package/dist/contract/router.js +158 -0
  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 +1085 -0
  40. package/dist/server/router.d.ts.map +1 -1
  41. package/dist/server/router.js +495 -2
  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/contract/attendance.ts +180 -0
  57. package/src/contract/capabilities.ts +36 -0
  58. package/src/contract/events.ts +24 -0
  59. package/src/contract/index.ts +1 -0
  60. package/src/contract/permissions.ts +51 -0
  61. package/src/contract/router.ts +196 -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;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAktGxC"}
@@ -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 });
@@ -1224,6 +1227,334 @@ export function implement_(kernel) {
1224
1227
  return { ok: true };
1225
1228
  }),
1226
1229
  },
1230
+ // ================================================================= attendance
1231
+ attendance: {
1232
+ state: scoped.attendance.state
1233
+ .use(cap('attendance'))
1234
+ .use(requires('hr.attendance.view'))
1235
+ .handler(({ input, context }) => db.withWorkspace(input.workspaceId, async (tx) => {
1236
+ const personId = await personFor(tx, input.workspaceId, context, input.personId);
1237
+ const { timezone, businessDate, schedule } = await clockContext(tx, input.workspaceId, personId);
1238
+ const rows = await attendance.punchesOn(tx, input.workspaceId, personId, businessDate);
1239
+ let open = null;
1240
+ let onBreak = false;
1241
+ let workedMs = 0;
1242
+ let breakOpen = null;
1243
+ for (const r of rows) {
1244
+ if (r.direction === 'in')
1245
+ open = r.at;
1246
+ else if (r.direction === 'out' && open) {
1247
+ workedMs += r.at.getTime() - open.getTime();
1248
+ open = null;
1249
+ }
1250
+ else if (r.direction === 'break_start') {
1251
+ breakOpen = r.at;
1252
+ onBreak = true;
1253
+ }
1254
+ else if (r.direction === 'break_end' && breakOpen) {
1255
+ workedMs -= r.at.getTime() - breakOpen.getTime();
1256
+ breakOpen = null;
1257
+ onBreak = false;
1258
+ }
1259
+ }
1260
+ // An open span counts up to now, so the widget shows time accruing rather than freezing
1261
+ // at the last completed pair.
1262
+ if (open)
1263
+ workedMs += Date.now() - open.getTime();
1264
+ if (breakOpen)
1265
+ workedMs -= Date.now() - breakOpen.getTime();
1266
+ void schedule;
1267
+ return {
1268
+ personId,
1269
+ businessDate,
1270
+ clockedIn: open !== null,
1271
+ onBreak,
1272
+ since: (open ?? breakOpen)?.toISOString() ?? null,
1273
+ workedMinutesToday: Math.max(0, Math.round(workedMs / 60000)),
1274
+ timezone,
1275
+ };
1276
+ })),
1277
+ clockIn: scoped.attendance.clockIn
1278
+ .use(cap('attendance'))
1279
+ .use(requires('hr.attendance.punch'))
1280
+ .handler(({ input, context }) => punch(input, context, 'in')),
1281
+ clockOut: scoped.attendance.clockOut
1282
+ .use(cap('attendance'))
1283
+ .use(requires('hr.attendance.punch'))
1284
+ .handler(({ input, context }) => punch(input, context, 'out')),
1285
+ breakStart: scoped.attendance.breakStart
1286
+ .use(cap('attendance'))
1287
+ .use(requires('hr.attendance.punch'))
1288
+ .handler(({ input, context }) => punch(input, context, 'break_start')),
1289
+ breakEnd: scoped.attendance.breakEnd
1290
+ .use(cap('attendance'))
1291
+ .use(requires('hr.attendance.punch'))
1292
+ .handler(({ input, context }) => punch(input, context, 'break_end')),
1293
+ punches: {
1294
+ list: scoped.attendance.punches.list
1295
+ .use(cap('attendance'))
1296
+ .use(requires('hr.attendance.view'))
1297
+ .handler(({ input, context }) => db.withWorkspace(input.workspaceId, async (tx) => {
1298
+ const personId = await personFor(tx, input.workspaceId, context, input.personId);
1299
+ const where = [
1300
+ eq(punches.workspaceId, input.workspaceId),
1301
+ eq(punches.personId, personId),
1302
+ gte(punches.businessDate, input.from),
1303
+ lte(punches.businessDate, input.to),
1304
+ ];
1305
+ if (!input.includeVoided)
1306
+ where.push(isNull(punches.voidedByPunchId));
1307
+ const rows = await tx
1308
+ .select()
1309
+ .from(punches)
1310
+ .where(and(...where))
1311
+ .orderBy(asc(punches.at))
1312
+ .limit(input.limit);
1313
+ return { items: rows.map(toPunch), nextCursor: null };
1314
+ })),
1315
+ void: scoped.attendance.punches.void
1316
+ .use(cap('attendance'))
1317
+ .use(requires('hr.attendance.manage'))
1318
+ .handler(async ({ input, context }) => {
1319
+ const affected = await db.withWorkspace(input.workspaceId, async (tx) => {
1320
+ const me = await svc.byUserId(tx, input.workspaceId, context.principal.userId ?? '');
1321
+ const { original } = await attendance.voidPunch(tx, input.workspaceId, input.punchId, input.reason, me?.id ?? null);
1322
+ // The day is derived, so voiding a punch means the sheet is stale until it is rebuilt.
1323
+ const { timezone, schedule } = await clockContext(tx, input.workspaceId, original.personId);
1324
+ await attendance.recomputeDay(tx, input.workspaceId, original.personId, original.businessDate, timezone, schedule);
1325
+ return original;
1326
+ });
1327
+ await changed(input.workspaceId, 'attendance_day', affected.personId, 'updated');
1328
+ return { ok: true };
1329
+ }),
1330
+ },
1331
+ days: {
1332
+ list: scoped.attendance.days.list
1333
+ .use(cap('attendance'))
1334
+ .use(requires('hr.attendance.view'))
1335
+ .handler(({ input, context }) => db.withWorkspace(input.workspaceId, async (tx) => {
1336
+ const where = [
1337
+ eq(attendanceDays.workspaceId, input.workspaceId),
1338
+ gte(attendanceDays.businessDate, input.from),
1339
+ lte(attendanceDays.businessDate, input.to),
1340
+ ];
1341
+ if (input.officeId) {
1342
+ const here = await tx
1343
+ .select({ personId: officeAssignments.personId })
1344
+ .from(officeAssignments)
1345
+ .where(and(eq(officeAssignments.workspaceId, input.workspaceId), eq(officeAssignments.officeId, input.officeId), isNull(officeAssignments.effectiveTo)));
1346
+ // Reading a whole office needs the team permission; reading yourself does not.
1347
+ await kernel.authz.require(context.principal, 'hr.attendance.view_team', {
1348
+ kind: 'workspace',
1349
+ id: input.workspaceId,
1350
+ workspaceId: input.workspaceId,
1351
+ });
1352
+ where.push(here.length
1353
+ ? inArray(attendanceDays.personId, here.map((h) => h.personId))
1354
+ : sql `false`);
1355
+ }
1356
+ else {
1357
+ const personId = await personFor(tx, input.workspaceId, context, input.personId);
1358
+ where.push(eq(attendanceDays.personId, personId));
1359
+ }
1360
+ const rows = await tx
1361
+ .select()
1362
+ .from(attendanceDays)
1363
+ .where(and(...where))
1364
+ .orderBy(asc(attendanceDays.businessDate))
1365
+ .limit(input.limit);
1366
+ return { items: rows.map(toAttendanceDay), nextCursor: null };
1367
+ })),
1368
+ recompute: scoped.attendance.days.recompute
1369
+ .use(cap('attendance'))
1370
+ .use(requires('hr.attendance.manage'))
1371
+ .handler(({ input, context }) => db.withWorkspace(input.workspaceId, async (tx) => {
1372
+ const personId = await personFor(tx, input.workspaceId, context, input.personId);
1373
+ const { timezone, schedule } = await clockContext(tx, input.workspaceId, personId);
1374
+ const dates = await attendance.datesWithPunches(tx, input.workspaceId, personId, input.from, input.to);
1375
+ let recomputed = 0;
1376
+ const skippedLocked = [];
1377
+ for (const date of dates) {
1378
+ const r = await attendance.recomputeDay(tx, input.workspaceId, personId, date, timezone, schedule);
1379
+ // Named rather than silently skipped: a recomputation that quietly declines to touch
1380
+ // a closed month looks identical to one that had nothing to do.
1381
+ if (r.locked)
1382
+ skippedLocked.push(date);
1383
+ else
1384
+ recomputed++;
1385
+ }
1386
+ return { recomputed, skippedLocked };
1387
+ })),
1388
+ },
1389
+ schedules: {
1390
+ list: scoped.attendance.schedules.list
1391
+ .use(cap('attendance'))
1392
+ .use(requires('hr.attendance.view'))
1393
+ .handler(({ input }) => db.withWorkspace(input.workspaceId, async (tx) => {
1394
+ const where = [eq(schedules.workspaceId, input.workspaceId)];
1395
+ if (!input.includeArchived)
1396
+ where.push(isNull(schedules.archivedAt));
1397
+ const rows = await tx
1398
+ .select()
1399
+ .from(schedules)
1400
+ .where(and(...where))
1401
+ .orderBy(asc(schedules.name));
1402
+ return rows.map(toSchedule);
1403
+ })),
1404
+ create: scoped.attendance.schedules.create
1405
+ .use(cap('attendance'))
1406
+ .use(requires('hr.attendance.manage'))
1407
+ .handler(async ({ input }) => {
1408
+ const row = await db.withWorkspace(input.workspaceId, async (tx) => {
1409
+ const [created] = await tx
1410
+ .insert(schedules)
1411
+ .values({
1412
+ id: uuidv7(),
1413
+ workspaceId: input.workspaceId,
1414
+ name: input.name,
1415
+ kind: input.kind,
1416
+ week: input.week,
1417
+ tzMode: input.tzMode,
1418
+ tz: input.tz ?? null,
1419
+ graceInMinutes: input.graceInMinutes,
1420
+ graceOutMinutes: input.graceOutMinutes,
1421
+ roundingStepMinutes: input.roundingStepMinutes,
1422
+ roundingDirection: input.roundingDirection,
1423
+ autoClockOutAfterMinutes: input.autoClockOutAfterMinutes ?? null,
1424
+ })
1425
+ .returning();
1426
+ return created;
1427
+ });
1428
+ await changed(input.workspaceId, 'schedule', row.id, 'created');
1429
+ return toSchedule(row);
1430
+ }),
1431
+ update: scoped.attendance.schedules.update
1432
+ .use(cap('attendance'))
1433
+ .use(requires('hr.attendance.manage'))
1434
+ .handler(async ({ input }) => {
1435
+ const row = await db.withWorkspace(input.workspaceId, async (tx) => {
1436
+ const { workspaceId, scheduleId, ...patch } = input;
1437
+ const set = { updatedAt: new Date() };
1438
+ for (const [k, v] of Object.entries(patch))
1439
+ if (v !== undefined)
1440
+ set[k] = v;
1441
+ const [updated] = await tx
1442
+ .update(schedules)
1443
+ .set(set)
1444
+ .where(and(eq(schedules.workspaceId, workspaceId), eq(schedules.id, scheduleId)))
1445
+ .returning();
1446
+ if (!updated)
1447
+ throw KernError.notFound('Schedule');
1448
+ return updated;
1449
+ });
1450
+ await changed(input.workspaceId, 'schedule', row.id, 'updated');
1451
+ return toSchedule(row);
1452
+ }),
1453
+ archive: scoped.attendance.schedules.archive
1454
+ .use(cap('attendance'))
1455
+ .use(requires('hr.attendance.manage'))
1456
+ .handler(async ({ input }) => {
1457
+ await db.withWorkspace(input.workspaceId, (tx) => tx
1458
+ .update(schedules)
1459
+ .set({ archivedAt: new Date() })
1460
+ .where(and(eq(schedules.workspaceId, input.workspaceId), eq(schedules.id, input.scheduleId))));
1461
+ await changed(input.workspaceId, 'schedule', input.scheduleId, 'deleted');
1462
+ return { ok: true };
1463
+ }),
1464
+ assign: scoped.attendance.schedules.assign
1465
+ .use(cap('attendance'))
1466
+ .use(requires('hr.attendance.manage'))
1467
+ .handler(async ({ input }) => {
1468
+ const rows = await db.withWorkspace(input.workspaceId, async (tx) => {
1469
+ // Effective-dated like everything else here: the old assignment is closed the day
1470
+ // before, so "which schedule was she on in March" stays answerable.
1471
+ await tx
1472
+ .update(scheduleAssignments)
1473
+ .set({ effectiveTo: sql `${input.effectiveFrom}::date - 1` })
1474
+ .where(and(eq(scheduleAssignments.workspaceId, input.workspaceId), eq(scheduleAssignments.personId, input.personId), isNull(scheduleAssignments.effectiveTo)));
1475
+ await tx.insert(scheduleAssignments).values({
1476
+ id: uuidv7(),
1477
+ workspaceId: input.workspaceId,
1478
+ personId: input.personId,
1479
+ scheduleId: input.scheduleId,
1480
+ effectiveFrom: input.effectiveFrom,
1481
+ });
1482
+ return tx
1483
+ .select()
1484
+ .from(scheduleAssignments)
1485
+ .where(and(eq(scheduleAssignments.workspaceId, input.workspaceId), eq(scheduleAssignments.personId, input.personId)))
1486
+ .orderBy(desc(scheduleAssignments.effectiveFrom));
1487
+ });
1488
+ await changed(input.workspaceId, 'schedule', input.scheduleId, 'updated');
1489
+ return rows.map(toScheduleAssignment);
1490
+ }),
1491
+ },
1492
+ regularizations: {
1493
+ list: scoped.attendance.regularizations.list
1494
+ .use(cap('attendance'))
1495
+ .use(requires('hr.attendance.view'))
1496
+ .handler(({ input, context }) => db.withWorkspace(input.workspaceId, async (tx) => {
1497
+ const personId = await personFor(tx, input.workspaceId, context, input.personId);
1498
+ const where = [
1499
+ eq(regularizations.workspaceId, input.workspaceId),
1500
+ eq(regularizations.personId, personId),
1501
+ ];
1502
+ if (input.status?.length)
1503
+ where.push(inArray(regularizations.status, input.status));
1504
+ const rows = await tx
1505
+ .select()
1506
+ .from(regularizations)
1507
+ .where(and(...where))
1508
+ .orderBy(desc(regularizations.businessDate))
1509
+ .limit(input.limit);
1510
+ return { items: rows.map(toRegularization), nextCursor: null };
1511
+ })),
1512
+ request: scoped.attendance.regularizations.request
1513
+ .use(cap('attendance'))
1514
+ .use(requires('hr.attendance.punch'))
1515
+ .handler(async ({ input, context }) => {
1516
+ const row = await db.withWorkspace(input.workspaceId, async (tx) => {
1517
+ const personId = await personFor(tx, input.workspaceId, context, input.personId);
1518
+ const [created] = await tx
1519
+ .insert(regularizations)
1520
+ .values({
1521
+ id: uuidv7(),
1522
+ workspaceId: input.workspaceId,
1523
+ personId,
1524
+ businessDate: input.businessDate,
1525
+ punchId: input.punchId ?? null,
1526
+ proposed: input.proposed,
1527
+ reason: input.reason,
1528
+ status: 'pending',
1529
+ })
1530
+ .returning();
1531
+ // The same engine leave uses. That reuse is the reason approvals were built keyed by
1532
+ // subject type rather than bolted onto leave_requests.
1533
+ const raised = await approvals.raise(tx, input.workspaceId, {
1534
+ subjectType: 'regularization',
1535
+ subjectId: created.id,
1536
+ summary: `Correction for ${input.businessDate}`,
1537
+ requesterPersonId: personId,
1538
+ requestedBy: context.principal.userId ?? null,
1539
+ on: input.businessDate,
1540
+ });
1541
+ await tx
1542
+ .update(regularizations)
1543
+ .set({ approvalRequestId: raised.request.id })
1544
+ .where(eq(regularizations.id, created.id));
1545
+ if (raised.autoApproved)
1546
+ await applyRegularization(tx, input.workspaceId, created.id);
1547
+ const [fresh] = await tx
1548
+ .select()
1549
+ .from(regularizations)
1550
+ .where(eq(regularizations.id, created.id));
1551
+ return fresh;
1552
+ });
1553
+ await changed(input.workspaceId, 'regularization', row.id, 'created');
1554
+ return toRegularization(row);
1555
+ }),
1556
+ },
1557
+ },
1227
1558
  // ================================================================= leave
1228
1559
  leave: {
1229
1560
  types: {
@@ -1620,6 +1951,15 @@ export function implement_(kernel) {
1620
1951
  .set({ status: 'rejected', decidedAt: new Date(), updatedAt: new Date() })
1621
1952
  .where(eq(leaveRequests.id, request.subjectId));
1622
1953
  }
1954
+ else if (request.subjectType === 'regularization') {
1955
+ if (result.status === 'approved')
1956
+ await applyRegularization(tx, input.workspaceId, request.subjectId);
1957
+ else if (result.status === 'rejected')
1958
+ await tx
1959
+ .update(regularizations)
1960
+ .set({ status: 'rejected' })
1961
+ .where(eq(regularizations.id, request.subjectId));
1962
+ }
1623
1963
  const [fresh] = await tx
1624
1964
  .select()
1625
1965
  .from(approvalRequests)
@@ -2273,6 +2613,123 @@ export function implement_(kernel) {
2273
2613
  })),
2274
2614
  };
2275
2615
  }
2616
+ /**
2617
+ * Everything a punch needs: the person's zone, today's business date, and their schedule.
2618
+ *
2619
+ * The zone comes from the resolution ladder — their primary office unless they have an override —
2620
+ * so a punch made on a business trip still counts towards the month they are employed in.
2621
+ */
2622
+ async function clockContext(tx, workspaceId, personId) {
2623
+ const today = todayIso();
2624
+ const resolution = await resolve.forPerson(tx, workspaceId, personId, today);
2625
+ const schedule = await attendance.scheduleFor(tx, workspaceId, personId, today);
2626
+ const businessDate = businessDateFor(Date.now(), resolution.timezone, schedule.shiftFor(todayIn(resolution.timezone)));
2627
+ return { timezone: resolution.timezone, businessDate, schedule, resolution };
2628
+ }
2629
+ /**
2630
+ * One punch, and the day rebuilt straight afterwards.
2631
+ *
2632
+ * Recomputing inline rather than in a background job: the clock widget shows a total, and a person
2633
+ * who clocks out wants to see it immediately. The day sheet is a projection, so doing it twice
2634
+ * costs nothing.
2635
+ */
2636
+ async function punch(input, context, direction) {
2637
+ const row = await db.withWorkspace(input.workspaceId, async (tx) => {
2638
+ const personId = await personFor(tx, input.workspaceId, context, input.personId);
2639
+ const { timezone, businessDate, schedule, resolution } = await clockContext(tx, input.workspaceId, personId);
2640
+ // Refuse the transitions that make no sense, with a sentence rather than a constraint error:
2641
+ // clocking in twice, or out when never in, is somebody double-tapping a button.
2642
+ const existing = await attendance.punchesOn(tx, input.workspaceId, personId, businessDate);
2643
+ const open = openState(existing);
2644
+ if (direction === 'in' && open.clockedIn)
2645
+ throw KernError.conflict('You are already clocked in.');
2646
+ if (direction === 'out' && !open.clockedIn)
2647
+ throw KernError.conflict('You are not clocked in.');
2648
+ if (direction === 'break_start' && !open.clockedIn)
2649
+ throw KernError.conflict('Clock in before starting a break.');
2650
+ if (direction === 'break_start' && open.onBreak)
2651
+ throw KernError.conflict('You are already on a break.');
2652
+ if (direction === 'break_end' && !open.onBreak)
2653
+ throw KernError.conflict('You are not on a break.');
2654
+ const punchRow = await attendance.record(tx, input.workspaceId, {
2655
+ personId,
2656
+ direction,
2657
+ timezone,
2658
+ method: input.method ?? 'web',
2659
+ clientReportedAt: input.clientReportedAt ?? null,
2660
+ officeId: resolution.primaryOfficeId,
2661
+ geo: input.geo ?? null,
2662
+ note: input.note ?? null,
2663
+ idempotencyKey: input.idempotencyKey ?? null,
2664
+ }, schedule);
2665
+ await attendance.recomputeDay(tx, input.workspaceId, personId, punchRow.businessDate, timezone, schedule);
2666
+ return punchRow;
2667
+ });
2668
+ await kernel.emit(hrEvents.punchRecorded, {
2669
+ punchId: row.id,
2670
+ workspaceId: input.workspaceId,
2671
+ personId: row.personId,
2672
+ direction: row.direction,
2673
+ businessDate: row.businessDate,
2674
+ }, { workspaceId: input.workspaceId, actorId: context.principal.userId });
2675
+ await changed(input.workspaceId, 'attendance_day', row.personId, 'updated');
2676
+ return toPunch(row);
2677
+ }
2678
+ /** Whether a person is currently clocked in or on a break, from their punches so far. */
2679
+ function openState(rows) {
2680
+ let clockedIn = false;
2681
+ let onBreak = false;
2682
+ for (const r of rows) {
2683
+ if (r.direction === 'in')
2684
+ clockedIn = true;
2685
+ else if (r.direction === 'out') {
2686
+ clockedIn = false;
2687
+ onBreak = false;
2688
+ }
2689
+ else if (r.direction === 'break_start')
2690
+ onBreak = true;
2691
+ else if (r.direction === 'break_end')
2692
+ onBreak = false;
2693
+ }
2694
+ return { clockedIn, onBreak };
2695
+ }
2696
+ /**
2697
+ * Apply an approved correction: write the proposed punches, void what they replace, rebuild.
2698
+ *
2699
+ * Nothing is edited. The original punch keeps its row and gains a pointer to what superseded it,
2700
+ * so a corrected timesheet and an edited one stay distinguishable — which is the entire reason
2701
+ * regularization exists rather than an update statement.
2702
+ */
2703
+ async function applyRegularization(tx, workspaceId, regularizationId) {
2704
+ const [row] = await tx
2705
+ .select()
2706
+ .from(regularizations)
2707
+ .where(and(eq(regularizations.workspaceId, workspaceId), eq(regularizations.id, regularizationId)))
2708
+ .limit(1);
2709
+ if (!row || row.status === 'approved')
2710
+ return;
2711
+ if (row.punchId)
2712
+ await attendance.voidPunch(tx, workspaceId, row.punchId, 'Regularized', null);
2713
+ const { timezone, schedule } = await clockContext(tx, workspaceId, row.personId);
2714
+ for (const proposal of row.proposed)
2715
+ await tx.insert(punches).values({
2716
+ id: uuidv7(),
2717
+ workspaceId,
2718
+ personId: row.personId,
2719
+ direction: proposal.direction,
2720
+ at: new Date(proposal.at),
2721
+ businessDate: row.businessDate,
2722
+ timezone,
2723
+ method: 'manual',
2724
+ trust: 'trusted',
2725
+ note: `Regularization ${row.id}`,
2726
+ });
2727
+ await attendance.recomputeDay(tx, workspaceId, row.personId, row.businessDate, timezone, schedule);
2728
+ await tx
2729
+ .update(regularizations)
2730
+ .set({ status: 'approved', appliedAt: new Date() })
2731
+ .where(eq(regularizations.id, row.id));
2732
+ }
2276
2733
  async function emitCalendarChanged(workspaceId, calendarId, from, to, actorId) {
2277
2734
  await kernel.emit(hrEvents.calendarChanged, { calendarId, workspaceId, from, to }, { workspaceId, actorId });
2278
2735
  await changed(workspaceId, 'calendar', calendarId, 'updated');
@@ -2344,4 +2801,40 @@ const toDelegation = (r) => ({
2344
2801
  subjectType: (r.subjectType ?? null),
2345
2802
  createdAt: r.createdAt.toISOString(),
2346
2803
  });
2804
+ const toPunch = (r) => ({
2805
+ ...r,
2806
+ direction: r.direction,
2807
+ method: r.method,
2808
+ trust: r.trust,
2809
+ geo: r.geo ?? null,
2810
+ at: r.at.toISOString(),
2811
+ clientReportedAt: r.clientReportedAt?.toISOString() ?? null,
2812
+ createdAt: r.createdAt.toISOString(),
2813
+ });
2814
+ const toAttendanceDay = (r) => ({
2815
+ ...r,
2816
+ status: r.status,
2817
+ firstIn: r.firstIn?.toISOString() ?? null,
2818
+ lastOut: r.lastOut?.toISOString() ?? null,
2819
+ computedAt: r.computedAt.toISOString(),
2820
+ });
2821
+ const toSchedule = (r) => ({
2822
+ ...r,
2823
+ kind: r.kind,
2824
+ week: r.week,
2825
+ tzMode: r.tzMode,
2826
+ roundingDirection: r.roundingDirection,
2827
+ archivedAt: r.archivedAt?.toISOString() ?? null,
2828
+ });
2829
+ const toScheduleAssignment = (r) => ({
2830
+ ...r,
2831
+ createdAt: r.createdAt.toISOString(),
2832
+ });
2833
+ const toRegularization = (r) => ({
2834
+ ...r,
2835
+ status: r.status,
2836
+ proposed: r.proposed,
2837
+ appliedAt: r.appliedAt?.toISOString() ?? null,
2838
+ createdAt: r.createdAt.toISOString(),
2839
+ });
2347
2840
  //# sourceMappingURL=router.js.map