@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.
- package/dist/contract/attendance.d.ts +240 -0
- package/dist/contract/attendance.d.ts.map +1 -0
- package/dist/contract/attendance.js +154 -0
- package/dist/contract/attendance.js.map +1 -0
- package/dist/contract/capabilities.d.ts.map +1 -1
- package/dist/contract/capabilities.js +36 -0
- package/dist/contract/capabilities.js.map +1 -1
- package/dist/contract/events.d.ts +18 -0
- package/dist/contract/events.d.ts.map +1 -1
- package/dist/contract/events.js +18 -0
- package/dist/contract/events.js.map +1 -1
- package/dist/contract/index.d.ts +1 -0
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +1 -0
- package/dist/contract/index.js.map +1 -1
- package/dist/contract/permissions.d.ts +43 -0
- package/dist/contract/permissions.d.ts.map +1 -1
- package/dist/contract/permissions.js +50 -0
- package/dist/contract/permissions.js.map +1 -1
- package/dist/contract/router.d.ts +996 -0
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/contract/router.js +165 -1
- package/dist/contract/router.js.map +1 -1
- package/dist/policy/time.d.ts +48 -0
- package/dist/policy/time.d.ts.map +1 -0
- package/dist/policy/time.js +144 -0
- package/dist/policy/time.js.map +1 -0
- package/dist/policy/working-time.d.ts +98 -0
- package/dist/policy/working-time.d.ts.map +1 -0
- package/dist/policy/working-time.js +190 -0
- package/dist/policy/working-time.js.map +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/jobs.d.ts +16 -0
- package/dist/server/jobs.d.ts.map +1 -0
- package/dist/server/jobs.js +166 -0
- package/dist/server/jobs.js.map +1 -0
- package/dist/server/router.d.ts +1087 -0
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/router.js +521 -3
- package/dist/server/router.js.map +1 -1
- package/dist/server/schema.d.ts +1261 -1
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +116 -0
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/attendance.d.ts +150 -0
- package/dist/server/services/attendance.d.ts.map +1 -0
- package/dist/server/services/attendance.js +239 -0
- package/dist/server/services/attendance.js.map +1 -0
- package/dist/server/services/ledger.d.ts +2 -2
- package/migrations/0003_attendance.sql +206 -0
- package/migrations/meta/0003_snapshot.json +3681 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/src/client/index.ts +18 -0
- package/src/contract/attendance.ts +180 -0
- package/src/contract/capabilities.ts +36 -0
- package/src/contract/events.ts +24 -0
- package/src/contract/index.ts +1 -0
- package/src/contract/permissions.ts +51 -0
- 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;
|
|
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"}
|
package/dist/server/router.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|