@kernhq/module-hr 0.7.0 → 0.9.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/capabilities.d.ts.map +1 -1
- package/dist/contract/capabilities.js +31 -0
- package/dist/contract/capabilities.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 +23 -0
- package/dist/contract/permissions.d.ts.map +1 -1
- package/dist/contract/permissions.js +27 -0
- package/dist/contract/permissions.js.map +1 -1
- package/dist/contract/policies.d.ts +256 -0
- package/dist/contract/policies.d.ts.map +1 -0
- package/dist/contract/policies.js +175 -0
- package/dist/contract/policies.js.map +1 -0
- package/dist/contract/router.d.ts +707 -0
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/contract/router.js +127 -0
- package/dist/contract/router.js.map +1 -1
- package/dist/policy/accrual.d.ts +127 -0
- package/dist/policy/accrual.d.ts.map +1 -0
- package/dist/policy/accrual.js +134 -0
- package/dist/policy/accrual.js.map +1 -0
- package/dist/server/jobs.d.ts.map +1 -1
- package/dist/server/jobs.js +186 -1
- package/dist/server/jobs.js.map +1 -1
- package/dist/server/router.d.ts +772 -0
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/router.js +462 -1
- package/dist/server/router.js.map +1 -1
- package/dist/server/schema.d.ts +612 -1
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +76 -0
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/policies.d.ts +53 -0
- package/dist/server/services/policies.d.ts.map +1 -0
- package/dist/server/services/policies.js +158 -0
- package/dist/server/services/policies.js.map +1 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +22 -2
- package/src/client/api-instance.ts +29 -0
- package/src/client/components/ClockControls.svelte +127 -0
- package/src/client/components/HrSidebar.svelte +73 -0
- package/src/client/components/LeaveRequestDialog.svelte +169 -0
- package/src/client/components/PersonFormDialog.svelte +184 -0
- package/src/client/components/PersonInline.svelte +49 -0
- package/src/client/components/PersonPanel.svelte +328 -0
- package/src/client/core-api.ts +34 -0
- package/src/client/i18n.ts +640 -0
- package/src/client/index.ts +3 -0
- package/src/client/mock.ts +447 -0
- package/src/client/module.ts +295 -0
- package/src/client/pages/ApprovalsPage.svelte +129 -0
- package/src/client/pages/AttendancePage.svelte +147 -0
- package/src/client/pages/DirectoryPage.svelte +428 -0
- package/src/client/pages/LeavePage.svelte +180 -0
- package/src/client/pages/OfficesPage.svelte +119 -0
- package/src/client/permissions.ts +31 -0
- package/src/client/query.test.ts +70 -0
- package/src/client/query.ts +69 -0
- package/src/client/settings/CalendarsSettings.svelte +21 -0
- package/src/client/settings/CapabilitiesSettings.svelte +122 -0
- package/src/client/settings/LeaveSettings.svelte +21 -0
- package/src/client/settings/OfficesSettings.svelte +21 -0
- package/src/client/settings/SchedulesSettings.svelte +21 -0
- package/src/client/widgets/ApprovalsWidget.svelte +86 -0
- package/src/client/widgets/ClockWidget.svelte +9 -0
- package/src/client/widgets/HeadcountWidget.svelte +29 -0
- package/src/client/widgets/LeaveBalanceWidget.svelte +67 -0
- package/src/client/widgets/WhosOutWidget.svelte +75 -0
- package/src/contract/capabilities.ts +31 -0
- package/src/contract/index.ts +1 -0
- package/src/contract/permissions.ts +28 -0
- package/src/contract/router.ts +148 -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;
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/server/router.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,MAAM,EACX,KAAK,cAAc,EAMpB,MAAM,gBAAgB,CAAA;AA2DvB;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmxHxC"}
|
package/dist/server/router.js
CHANGED
|
@@ -2,15 +2,18 @@ import { KernError, requires, requiresCapability, uuidv7, workspaceScoped, } fro
|
|
|
2
2
|
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
|
+
import { AccrualConfig, CarryForwardConfig, OvertimeConfig, RoundingConfig, WorkingTimeConfig, } from '../contract/policies.js';
|
|
6
|
+
import { accrueForPeriod } from '../policy/accrual.js';
|
|
5
7
|
import { countWorkingDays, workingDays } from '../policy/calendar.js';
|
|
6
8
|
import { businessDateFor } from '../policy/working-time.js';
|
|
7
9
|
import { COUNTRY_PACKS, packDays } from './packs/index.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';
|
|
10
|
+
import { approvalChains, approvalDecisions, approvalRequests, approvalSteps, attendanceDays, calendarDays, calendars, costCenters, customFieldDefs, delegations, employments, leaveLedger, leaveRequestDays, leaveRequests, leaveTypes, legalEntities, officeAssignments, offices, orgUnits, people, peopleSensitive, periods, personDocuments, personHistory, policies, policyAssignments, positions, punches, regularizations, scheduleAssignments, schedules, } from './schema.js';
|
|
9
11
|
import { ApprovalService } from './services/approvals.js';
|
|
10
12
|
import { AttendanceService } from './services/attendance.js';
|
|
11
13
|
import { inForceOn, todayIn, todayIso } from './services/db.js';
|
|
12
14
|
import { LedgerService, MINUTES_PER_DAY, yearOf } from './services/ledger.js';
|
|
13
15
|
import { PeopleService } from './services/people.js';
|
|
16
|
+
import { hashConfig, PolicyService } from './services/policies.js';
|
|
14
17
|
import { DEFAULT_WORKING_WEEK, ResolveService } from './services/resolve.js';
|
|
15
18
|
const os = implement(hrContract).$context();
|
|
16
19
|
/**
|
|
@@ -33,6 +36,7 @@ export function implement_(kernel) {
|
|
|
33
36
|
const ledger = new LedgerService();
|
|
34
37
|
const approvals = new ApprovalService(kernel);
|
|
35
38
|
const attendance = new AttendanceService();
|
|
39
|
+
const policySvc = new PolicyService(resolve);
|
|
36
40
|
const db = kernel.database;
|
|
37
41
|
const settingsOf = (workspaceId) => kernel.settings.module(workspaceId, MODULE_ID, HrSettings);
|
|
38
42
|
const changed = (workspaceId, entity, id, op) => kernel.realtime.change(workspaceId, { module: MODULE_ID, entity, id, op });
|
|
@@ -1252,6 +1256,292 @@ export function implement_(kernel) {
|
|
|
1252
1256
|
return { ok: true };
|
|
1253
1257
|
}),
|
|
1254
1258
|
},
|
|
1259
|
+
// ================================================================= policies
|
|
1260
|
+
policies: {
|
|
1261
|
+
list: scoped.policies.list
|
|
1262
|
+
.use(cap('leave_accrual'))
|
|
1263
|
+
.use(requires('hr.policy.view'))
|
|
1264
|
+
.handler(({ input }) => db.withWorkspace(input.workspaceId, async (tx) => {
|
|
1265
|
+
const where = [eq(policies.workspaceId, input.workspaceId)];
|
|
1266
|
+
if (input.kind)
|
|
1267
|
+
where.push(eq(policies.kind, input.kind));
|
|
1268
|
+
if (!input.includeArchived)
|
|
1269
|
+
where.push(isNull(policies.archivedAt));
|
|
1270
|
+
const rows = await tx
|
|
1271
|
+
.select()
|
|
1272
|
+
.from(policies)
|
|
1273
|
+
.where(and(...where))
|
|
1274
|
+
.orderBy(asc(policies.kind), asc(policies.name));
|
|
1275
|
+
return withAssignments(tx, input.workspaceId, rows);
|
|
1276
|
+
})),
|
|
1277
|
+
get: scoped.policies.get
|
|
1278
|
+
.use(cap('leave_accrual'))
|
|
1279
|
+
.use(requires('hr.policy.view'))
|
|
1280
|
+
.handler(({ input }) => db.withWorkspace(input.workspaceId, async (tx) => {
|
|
1281
|
+
const row = await loadPolicy(tx, input.workspaceId, input.policyId);
|
|
1282
|
+
return (await withAssignments(tx, input.workspaceId, [row]))[0];
|
|
1283
|
+
})),
|
|
1284
|
+
create: scoped.policies.create
|
|
1285
|
+
.use(cap('leave_accrual'))
|
|
1286
|
+
.use(requires('hr.policy.manage'))
|
|
1287
|
+
.handler(async ({ input }) => {
|
|
1288
|
+
const config = validatePolicyConfig(input.kind, input.config);
|
|
1289
|
+
const row = await db.withWorkspace(input.workspaceId, async (tx) => {
|
|
1290
|
+
const [created] = await tx
|
|
1291
|
+
.insert(policies)
|
|
1292
|
+
.values({
|
|
1293
|
+
id: uuidv7(),
|
|
1294
|
+
workspaceId: input.workspaceId,
|
|
1295
|
+
kind: input.kind,
|
|
1296
|
+
name: input.name,
|
|
1297
|
+
config,
|
|
1298
|
+
effectiveFrom: input.effectiveFrom,
|
|
1299
|
+
effectiveTo: input.effectiveTo ?? null,
|
|
1300
|
+
source: 'custom',
|
|
1301
|
+
configHash: hashConfig(config),
|
|
1302
|
+
})
|
|
1303
|
+
.returning();
|
|
1304
|
+
return created;
|
|
1305
|
+
});
|
|
1306
|
+
await changed(input.workspaceId, 'policy', row.id, 'created');
|
|
1307
|
+
return toPolicy(row);
|
|
1308
|
+
}),
|
|
1309
|
+
update: scoped.policies.update
|
|
1310
|
+
.use(cap('leave_accrual'))
|
|
1311
|
+
.use(requires('hr.policy.manage'))
|
|
1312
|
+
.handler(async ({ input }) => {
|
|
1313
|
+
const row = await db.withWorkspace(input.workspaceId, async (tx) => {
|
|
1314
|
+
const existing = await loadPolicy(tx, input.workspaceId, input.policyId);
|
|
1315
|
+
const set = { updatedAt: new Date() };
|
|
1316
|
+
if (input.name !== undefined)
|
|
1317
|
+
set.name = input.name;
|
|
1318
|
+
if (input.effectiveTo !== undefined)
|
|
1319
|
+
set.effectiveTo = input.effectiveTo;
|
|
1320
|
+
if (input.config !== undefined) {
|
|
1321
|
+
const config = validatePolicyConfig(existing.kind, input.config);
|
|
1322
|
+
set.config = config;
|
|
1323
|
+
// The hash changes with the config, which is what marks every row derived from the
|
|
1324
|
+
// old one as stale rather than leaving them silently wrong.
|
|
1325
|
+
set.configHash = hashConfig(config);
|
|
1326
|
+
}
|
|
1327
|
+
const [updated] = await tx
|
|
1328
|
+
.update(policies)
|
|
1329
|
+
.set(set)
|
|
1330
|
+
.where(and(eq(policies.workspaceId, input.workspaceId), eq(policies.id, input.policyId)))
|
|
1331
|
+
.returning();
|
|
1332
|
+
return updated;
|
|
1333
|
+
});
|
|
1334
|
+
await changed(input.workspaceId, 'policy', row.id, 'updated');
|
|
1335
|
+
return toPolicy(row);
|
|
1336
|
+
}),
|
|
1337
|
+
archive: scoped.policies.archive
|
|
1338
|
+
.use(cap('leave_accrual'))
|
|
1339
|
+
.use(requires('hr.policy.manage'))
|
|
1340
|
+
.handler(async ({ input }) => {
|
|
1341
|
+
// Archived, never deleted: ledger entries name the policy that produced them, and a
|
|
1342
|
+
// movement whose policy has vanished is a number nobody can explain.
|
|
1343
|
+
await db.withWorkspace(input.workspaceId, (tx) => tx
|
|
1344
|
+
.update(policies)
|
|
1345
|
+
.set({ archivedAt: new Date() })
|
|
1346
|
+
.where(and(eq(policies.workspaceId, input.workspaceId), eq(policies.id, input.policyId))));
|
|
1347
|
+
await changed(input.workspaceId, 'policy', input.policyId, 'deleted');
|
|
1348
|
+
return { ok: true };
|
|
1349
|
+
}),
|
|
1350
|
+
assign: scoped.policies.assign
|
|
1351
|
+
.use(cap('leave_accrual'))
|
|
1352
|
+
.use(requires('hr.policy.manage'))
|
|
1353
|
+
.handler(async ({ input }) => {
|
|
1354
|
+
const row = await db.withWorkspace(input.workspaceId, async (tx) => {
|
|
1355
|
+
await loadPolicy(tx, input.workspaceId, input.policyId);
|
|
1356
|
+
if (input.subjectKind !== 'workspace' && !input.subjectId)
|
|
1357
|
+
throw KernError.badRequest(`A ${input.subjectKind} assignment needs a subject.`);
|
|
1358
|
+
const [created] = await tx
|
|
1359
|
+
.insert(policyAssignments)
|
|
1360
|
+
.values({
|
|
1361
|
+
id: uuidv7(),
|
|
1362
|
+
workspaceId: input.workspaceId,
|
|
1363
|
+
policyId: input.policyId,
|
|
1364
|
+
subjectKind: input.subjectKind,
|
|
1365
|
+
subjectId: input.subjectKind === 'workspace' ? null : (input.subjectId ?? null),
|
|
1366
|
+
effectiveFrom: input.effectiveFrom,
|
|
1367
|
+
effectiveTo: input.effectiveTo ?? null,
|
|
1368
|
+
// The ladder, written once. Nothing else invents its own order.
|
|
1369
|
+
priority: PolicyService.priorityFor(input.subjectKind),
|
|
1370
|
+
})
|
|
1371
|
+
.returning();
|
|
1372
|
+
return created;
|
|
1373
|
+
});
|
|
1374
|
+
await changed(input.workspaceId, 'policy', input.policyId, 'updated');
|
|
1375
|
+
return toAssignment(row);
|
|
1376
|
+
}),
|
|
1377
|
+
unassign: scoped.policies.unassign
|
|
1378
|
+
.use(cap('leave_accrual'))
|
|
1379
|
+
.use(requires('hr.policy.manage'))
|
|
1380
|
+
.handler(async ({ input }) => {
|
|
1381
|
+
await db.withWorkspace(input.workspaceId, (tx) => tx
|
|
1382
|
+
.delete(policyAssignments)
|
|
1383
|
+
.where(and(eq(policyAssignments.workspaceId, input.workspaceId), eq(policyAssignments.id, input.assignmentId))));
|
|
1384
|
+
await changed(input.workspaceId, 'policy', input.assignmentId, 'deleted');
|
|
1385
|
+
return { ok: true };
|
|
1386
|
+
}),
|
|
1387
|
+
resolveFor: scoped.policies.resolveFor
|
|
1388
|
+
.use(cap('leave_accrual'))
|
|
1389
|
+
.use(requires('hr.policy.view'))
|
|
1390
|
+
.handler(({ input }) => db.withWorkspace(input.workspaceId, async (tx) => {
|
|
1391
|
+
const on = input.on ?? todayIso();
|
|
1392
|
+
const kinds = ['accrual', 'carry_forward', 'overtime', 'rounding', 'working_time'];
|
|
1393
|
+
const out = [];
|
|
1394
|
+
for (const kind of kinds)
|
|
1395
|
+
out.push(await policySvc.forPerson(tx, input.workspaceId, input.personId, kind, on));
|
|
1396
|
+
return out;
|
|
1397
|
+
})),
|
|
1398
|
+
},
|
|
1399
|
+
// ================================================================= accrual
|
|
1400
|
+
accrual: {
|
|
1401
|
+
preview: scoped.accrual.preview
|
|
1402
|
+
.use(cap('leave_accrual'))
|
|
1403
|
+
.use(requires('hr.policy.view'))
|
|
1404
|
+
.handler(({ input }) => db.withWorkspace(input.workspaceId, (tx) => accrualPreview(tx, input.workspaceId, input.from, input.to, input.personId))),
|
|
1405
|
+
run: scoped.accrual.run
|
|
1406
|
+
.use(cap('leave_accrual'))
|
|
1407
|
+
.use(requires('hr.policy.manage'))
|
|
1408
|
+
.handler(async ({ input, context }) => {
|
|
1409
|
+
const result = await db.withWorkspace(input.workspaceId, async (tx) => {
|
|
1410
|
+
const preview = await accrualPreview(tx, input.workspaceId, input.from, input.to, input.personId);
|
|
1411
|
+
let credited = 0;
|
|
1412
|
+
let totalMinutes = 0;
|
|
1413
|
+
for (const row of preview.rows) {
|
|
1414
|
+
// Idempotent per person, per type, per period: a job that double-credits when
|
|
1415
|
+
// somebody clicks twice is worse than one that never ran.
|
|
1416
|
+
if (row.alreadyAccrued || row.minutes <= 0)
|
|
1417
|
+
continue;
|
|
1418
|
+
const year = yearOf(input.from);
|
|
1419
|
+
await ledger.lockAndRead(tx, input.workspaceId, row.personId, row.leaveTypeId, year);
|
|
1420
|
+
await ledger.append(tx, input.workspaceId, {
|
|
1421
|
+
personId: row.personId,
|
|
1422
|
+
leaveTypeId: row.leaveTypeId,
|
|
1423
|
+
kind: 'accrual',
|
|
1424
|
+
amountMinutes: row.minutes,
|
|
1425
|
+
effectiveOn: input.to,
|
|
1426
|
+
periodYear: year,
|
|
1427
|
+
reason: row.reason,
|
|
1428
|
+
createdBy: context.principal.userId ?? null,
|
|
1429
|
+
});
|
|
1430
|
+
credited++;
|
|
1431
|
+
totalMinutes += row.minutes;
|
|
1432
|
+
}
|
|
1433
|
+
return { credited, skipped: preview.rows.length - credited, totalMinutes };
|
|
1434
|
+
});
|
|
1435
|
+
await changed(input.workspaceId, 'leave_balance', input.workspaceId, 'updated');
|
|
1436
|
+
return result;
|
|
1437
|
+
}),
|
|
1438
|
+
},
|
|
1439
|
+
// ================================================================= periods
|
|
1440
|
+
periods: {
|
|
1441
|
+
list: scoped.periods.list
|
|
1442
|
+
.use(cap('periods'))
|
|
1443
|
+
.use(requires('hr.period.manage'))
|
|
1444
|
+
.handler(({ input }) => db.withWorkspace(input.workspaceId, async (tx) => {
|
|
1445
|
+
const where = [eq(periods.workspaceId, input.workspaceId)];
|
|
1446
|
+
if (input.kind)
|
|
1447
|
+
where.push(eq(periods.kind, input.kind));
|
|
1448
|
+
const rows = await tx
|
|
1449
|
+
.select()
|
|
1450
|
+
.from(periods)
|
|
1451
|
+
.where(and(...where))
|
|
1452
|
+
.orderBy(desc(periods.startsOn))
|
|
1453
|
+
.limit(input.limit);
|
|
1454
|
+
return { items: rows.map(toPeriod), nextCursor: null };
|
|
1455
|
+
})),
|
|
1456
|
+
create: scoped.periods.create
|
|
1457
|
+
.use(cap('periods'))
|
|
1458
|
+
.use(requires('hr.period.manage'))
|
|
1459
|
+
.handler(async ({ input }) => {
|
|
1460
|
+
const row = await db.withWorkspace(input.workspaceId, async (tx) => {
|
|
1461
|
+
if (input.endsOn < input.startsOn)
|
|
1462
|
+
throw KernError.badRequest('A period cannot end before it starts.');
|
|
1463
|
+
const [created] = await tx
|
|
1464
|
+
.insert(periods)
|
|
1465
|
+
.values({
|
|
1466
|
+
id: uuidv7(),
|
|
1467
|
+
workspaceId: input.workspaceId,
|
|
1468
|
+
kind: input.kind,
|
|
1469
|
+
legalEntityId: input.legalEntityId ?? null,
|
|
1470
|
+
startsOn: input.startsOn,
|
|
1471
|
+
endsOn: input.endsOn,
|
|
1472
|
+
status: 'open',
|
|
1473
|
+
})
|
|
1474
|
+
.returning();
|
|
1475
|
+
return created;
|
|
1476
|
+
});
|
|
1477
|
+
await changed(input.workspaceId, 'period', row.id, 'created');
|
|
1478
|
+
return toPeriod(row);
|
|
1479
|
+
}),
|
|
1480
|
+
lock: scoped.periods.lock
|
|
1481
|
+
.use(cap('periods'))
|
|
1482
|
+
.use(requires('hr.period.manage'))
|
|
1483
|
+
.handler(async ({ input, context }) => {
|
|
1484
|
+
const result = await db.withWorkspace(input.workspaceId, async (tx) => {
|
|
1485
|
+
const period = await loadPeriod(tx, input.workspaceId, input.periodId);
|
|
1486
|
+
if (period.status === 'locked')
|
|
1487
|
+
throw KernError.conflict('That period is already locked.');
|
|
1488
|
+
const [row] = await tx
|
|
1489
|
+
.update(periods)
|
|
1490
|
+
.set({
|
|
1491
|
+
status: 'locked',
|
|
1492
|
+
lockedAt: new Date(),
|
|
1493
|
+
lockedBy: context.principal.userId ?? null,
|
|
1494
|
+
note: input.note ?? null,
|
|
1495
|
+
})
|
|
1496
|
+
.where(eq(periods.id, period.id))
|
|
1497
|
+
.returning();
|
|
1498
|
+
// The day sheet carries its own flag so a recomputation does not have to join periods
|
|
1499
|
+
// on every row — this is what actually stops a closed month moving.
|
|
1500
|
+
const locked = await tx
|
|
1501
|
+
.update(attendanceDays)
|
|
1502
|
+
.set({ locked: true })
|
|
1503
|
+
.where(and(eq(attendanceDays.workspaceId, input.workspaceId), gte(attendanceDays.businessDate, period.startsOn), lte(attendanceDays.businessDate, period.endsOn)))
|
|
1504
|
+
.returning({ id: attendanceDays.id });
|
|
1505
|
+
return { period: row, lockedDays: locked.length };
|
|
1506
|
+
});
|
|
1507
|
+
await changed(input.workspaceId, 'period', input.periodId, 'updated');
|
|
1508
|
+
return { ...toPeriod(result.period), lockedDays: result.lockedDays };
|
|
1509
|
+
}),
|
|
1510
|
+
unlock: scoped.periods.unlock
|
|
1511
|
+
.use(cap('periods'))
|
|
1512
|
+
.use(requires('hr.period.manage'))
|
|
1513
|
+
.handler(async ({ input, context }) => {
|
|
1514
|
+
const result = await db.withWorkspace(input.workspaceId, async (tx) => {
|
|
1515
|
+
const period = await loadPeriod(tx, input.workspaceId, input.periodId);
|
|
1516
|
+
const [row] = await tx
|
|
1517
|
+
.update(periods)
|
|
1518
|
+
.set({
|
|
1519
|
+
status: 'open',
|
|
1520
|
+
lockedAt: null,
|
|
1521
|
+
lockedBy: null,
|
|
1522
|
+
note: `Reopened: ${input.reason}`,
|
|
1523
|
+
})
|
|
1524
|
+
.where(eq(periods.id, period.id))
|
|
1525
|
+
.returning();
|
|
1526
|
+
const unlocked = await tx
|
|
1527
|
+
.update(attendanceDays)
|
|
1528
|
+
.set({ locked: false })
|
|
1529
|
+
.where(and(eq(attendanceDays.workspaceId, input.workspaceId), gte(attendanceDays.businessDate, period.startsOn), lte(attendanceDays.businessDate, period.endsOn)))
|
|
1530
|
+
.returning({ id: attendanceDays.id });
|
|
1531
|
+
kernel.log.warn({
|
|
1532
|
+
module: MODULE_ID,
|
|
1533
|
+
workspaceId: input.workspaceId,
|
|
1534
|
+
periodId: period.id,
|
|
1535
|
+
days: unlocked.length,
|
|
1536
|
+
actorId: context.principal.userId,
|
|
1537
|
+
reason: input.reason,
|
|
1538
|
+
}, 'payroll period reopened');
|
|
1539
|
+
return { period: row, unlockedDays: unlocked.length };
|
|
1540
|
+
});
|
|
1541
|
+
await changed(input.workspaceId, 'period', input.periodId, 'updated');
|
|
1542
|
+
return { ...toPeriod(result.period), unlockedDays: result.unlockedDays };
|
|
1543
|
+
}),
|
|
1544
|
+
},
|
|
1255
1545
|
// ================================================================= attendance
|
|
1256
1546
|
attendance: {
|
|
1257
1547
|
state: scoped.attendance.state
|
|
@@ -2755,6 +3045,160 @@ export function implement_(kernel) {
|
|
|
2755
3045
|
.set({ status: 'approved', appliedAt: new Date() })
|
|
2756
3046
|
.where(eq(regularizations.id, row.id));
|
|
2757
3047
|
}
|
|
3048
|
+
async function loadPolicy(tx, workspaceId, policyId) {
|
|
3049
|
+
const [row] = await tx
|
|
3050
|
+
.select()
|
|
3051
|
+
.from(policies)
|
|
3052
|
+
.where(and(eq(policies.workspaceId, workspaceId), eq(policies.id, policyId)))
|
|
3053
|
+
.limit(1);
|
|
3054
|
+
if (!row)
|
|
3055
|
+
throw KernError.notFound('Policy');
|
|
3056
|
+
return row;
|
|
3057
|
+
}
|
|
3058
|
+
async function loadPeriod(tx, workspaceId, periodId) {
|
|
3059
|
+
const [row] = await tx
|
|
3060
|
+
.select()
|
|
3061
|
+
.from(periods)
|
|
3062
|
+
.where(and(eq(periods.workspaceId, workspaceId), eq(periods.id, periodId)))
|
|
3063
|
+
.limit(1);
|
|
3064
|
+
if (!row)
|
|
3065
|
+
throw KernError.notFound('Period');
|
|
3066
|
+
return row;
|
|
3067
|
+
}
|
|
3068
|
+
/** Policies with the assignments that decide who they reach — one query for the whole list. */
|
|
3069
|
+
async function withAssignments(tx, workspaceId, rows) {
|
|
3070
|
+
if (!rows.length)
|
|
3071
|
+
return [];
|
|
3072
|
+
const assignments = await tx
|
|
3073
|
+
.select()
|
|
3074
|
+
.from(policyAssignments)
|
|
3075
|
+
.where(and(eq(policyAssignments.workspaceId, workspaceId), inArray(policyAssignments.policyId, rows.map((r) => r.id))));
|
|
3076
|
+
return rows.map((r) => ({
|
|
3077
|
+
...toPolicy(r),
|
|
3078
|
+
assignments: assignments.filter((a) => a.policyId === r.id).map(toAssignment),
|
|
3079
|
+
}));
|
|
3080
|
+
}
|
|
3081
|
+
/**
|
|
3082
|
+
* What an accrual run would credit, per person.
|
|
3083
|
+
*
|
|
3084
|
+
* **`run` calls this and credits what it returns**, so the preview and the thing it previews are
|
|
3085
|
+
* the same computation. A preview written separately is a preview that eventually disagrees with
|
|
3086
|
+
* the number that lands in somebody's balance.
|
|
3087
|
+
*
|
|
3088
|
+
* Everybody active is considered; the reason a person was skipped is returned rather than the
|
|
3089
|
+
* person being silently absent, because "why did she not accrue" is the question that follows.
|
|
3090
|
+
*/
|
|
3091
|
+
async function accrualPreview(tx, workspaceId, from, to, onlyPersonId) {
|
|
3092
|
+
const staff = await tx
|
|
3093
|
+
.select()
|
|
3094
|
+
.from(people)
|
|
3095
|
+
.where(and(eq(people.workspaceId, workspaceId), onlyPersonId ? eq(people.id, onlyPersonId) : inArray(people.status, ['active', 'on_leave'])));
|
|
3096
|
+
const rows = [];
|
|
3097
|
+
const skipped = [];
|
|
3098
|
+
if (!staff.length)
|
|
3099
|
+
return { periodFrom: from, periodTo: to, rows, totalMinutes: 0, skipped };
|
|
3100
|
+
const ids = staff.map((p) => p.id);
|
|
3101
|
+
const resolved = await policySvc.forPeople(tx, workspaceId, ids, 'accrual', to);
|
|
3102
|
+
const employmentRows = await tx
|
|
3103
|
+
.select()
|
|
3104
|
+
.from(employments)
|
|
3105
|
+
.where(and(eq(employments.workspaceId, workspaceId), inArray(employments.personId, ids), inForceOn(employments.effectiveFrom, employments.effectiveTo, to)));
|
|
3106
|
+
const employmentBy = new Map(employmentRows.map((e) => [e.personId, e]));
|
|
3107
|
+
const types = await tx
|
|
3108
|
+
.select()
|
|
3109
|
+
.from(leaveTypes)
|
|
3110
|
+
.where(and(eq(leaveTypes.workspaceId, workspaceId), isNull(leaveTypes.archivedAt)));
|
|
3111
|
+
const typeByKey = new Map(types.map((t) => [t.key, t]));
|
|
3112
|
+
// Everything already credited for this window, so a re-run credits nothing.
|
|
3113
|
+
const existing = await tx
|
|
3114
|
+
.select({ personId: leaveLedger.personId, leaveTypeId: leaveLedger.leaveTypeId })
|
|
3115
|
+
.from(leaveLedger)
|
|
3116
|
+
.where(and(eq(leaveLedger.workspaceId, workspaceId), eq(leaveLedger.kind, 'accrual'), eq(leaveLedger.effectiveOn, to), inArray(leaveLedger.personId, ids)));
|
|
3117
|
+
const already = new Set(existing.map((e) => `${e.personId}:${e.leaveTypeId}`));
|
|
3118
|
+
let totalMinutes = 0;
|
|
3119
|
+
for (const person of staff) {
|
|
3120
|
+
const policy = resolved.get(person.id);
|
|
3121
|
+
if (!policy?.config) {
|
|
3122
|
+
skipped.push({
|
|
3123
|
+
personId: person.id,
|
|
3124
|
+
displayName: person.displayName,
|
|
3125
|
+
reason: 'no accrual policy applies',
|
|
3126
|
+
});
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3129
|
+
const config = policy.config;
|
|
3130
|
+
const type = typeByKey.get(config.leaveTypeKey);
|
|
3131
|
+
if (!type) {
|
|
3132
|
+
skipped.push({
|
|
3133
|
+
personId: person.id,
|
|
3134
|
+
displayName: person.displayName,
|
|
3135
|
+
reason: `no leave type "${config.leaveTypeKey}"`,
|
|
3136
|
+
});
|
|
3137
|
+
continue;
|
|
3138
|
+
}
|
|
3139
|
+
if (!person.hiredOn) {
|
|
3140
|
+
skipped.push({
|
|
3141
|
+
personId: person.id,
|
|
3142
|
+
displayName: person.displayName,
|
|
3143
|
+
reason: 'no hire date',
|
|
3144
|
+
});
|
|
3145
|
+
continue;
|
|
3146
|
+
}
|
|
3147
|
+
const employment = employmentBy.get(person.id);
|
|
3148
|
+
const result = accrueForPeriod({
|
|
3149
|
+
policy: {
|
|
3150
|
+
frequency: config.frequency,
|
|
3151
|
+
daysPerYear: config.daysPerYear,
|
|
3152
|
+
minutesPerDay: config.minutesPerDay,
|
|
3153
|
+
seniorityTiers: config.seniorityTiers,
|
|
3154
|
+
waitingPeriodMonths: config.waitingPeriodMonths,
|
|
3155
|
+
roundToMinutes: config.roundToMinutes,
|
|
3156
|
+
},
|
|
3157
|
+
period: { from, to },
|
|
3158
|
+
hiredOn: person.hiredOn,
|
|
3159
|
+
terminatedOn: person.terminatedOn,
|
|
3160
|
+
fte: employment ? Number.parseFloat(employment.fte ?? '1') : 1,
|
|
3161
|
+
});
|
|
3162
|
+
const alreadyAccrued = already.has(`${person.id}:${type.id}`);
|
|
3163
|
+
rows.push({
|
|
3164
|
+
personId: person.id,
|
|
3165
|
+
displayName: person.displayName,
|
|
3166
|
+
leaveTypeId: type.id,
|
|
3167
|
+
leaveTypeName: type.name,
|
|
3168
|
+
minutes: result.minutes,
|
|
3169
|
+
days: Math.round((result.minutes / config.minutesPerDay) * 100) / 100,
|
|
3170
|
+
reason: result.reason,
|
|
3171
|
+
alreadyAccrued,
|
|
3172
|
+
});
|
|
3173
|
+
if (!alreadyAccrued)
|
|
3174
|
+
totalMinutes += result.minutes;
|
|
3175
|
+
}
|
|
3176
|
+
return { periodFrom: from, periodTo: to, rows, totalMinutes, skipped };
|
|
3177
|
+
}
|
|
3178
|
+
/**
|
|
3179
|
+
* Validate a config against the schema for its kind.
|
|
3180
|
+
*
|
|
3181
|
+
* A policy is data, and data from an API is not trusted because it arrived in the right-shaped
|
|
3182
|
+
* field. Each kind validates its own shape, which is what makes "policies as data" safe rather
|
|
3183
|
+
* than a jsonb column nobody checks.
|
|
3184
|
+
*/
|
|
3185
|
+
function validatePolicyConfig(kind, config) {
|
|
3186
|
+
const schema = kind === 'accrual'
|
|
3187
|
+
? AccrualConfig
|
|
3188
|
+
: kind === 'carry_forward'
|
|
3189
|
+
? CarryForwardConfig
|
|
3190
|
+
: kind === 'overtime'
|
|
3191
|
+
? OvertimeConfig
|
|
3192
|
+
: kind === 'rounding'
|
|
3193
|
+
? RoundingConfig
|
|
3194
|
+
: WorkingTimeConfig;
|
|
3195
|
+
const parsed = schema.safeParse(config);
|
|
3196
|
+
if (!parsed.success)
|
|
3197
|
+
throw KernError.badRequest(`Invalid ${kind} policy`, {
|
|
3198
|
+
issues: parsed.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`),
|
|
3199
|
+
});
|
|
3200
|
+
return parsed.data;
|
|
3201
|
+
}
|
|
2758
3202
|
async function emitCalendarChanged(workspaceId, calendarId, from, to, actorId) {
|
|
2759
3203
|
await kernel.emit(hrEvents.calendarChanged, { calendarId, workspaceId, from, to }, { workspaceId, actorId });
|
|
2760
3204
|
await changed(workspaceId, 'calendar', calendarId, 'updated');
|
|
@@ -2862,4 +3306,21 @@ const toRegularization = (r) => ({
|
|
|
2862
3306
|
appliedAt: r.appliedAt?.toISOString() ?? null,
|
|
2863
3307
|
createdAt: r.createdAt.toISOString(),
|
|
2864
3308
|
});
|
|
3309
|
+
const toPolicy = (r) => ({
|
|
3310
|
+
...r,
|
|
3311
|
+
kind: r.kind,
|
|
3312
|
+
source: r.source,
|
|
3313
|
+
config: r.config ?? {},
|
|
3314
|
+
archivedAt: r.archivedAt?.toISOString() ?? null,
|
|
3315
|
+
});
|
|
3316
|
+
const toAssignment = (r) => ({
|
|
3317
|
+
...r,
|
|
3318
|
+
subjectKind: r.subjectKind,
|
|
3319
|
+
});
|
|
3320
|
+
const toPeriod = (r) => ({
|
|
3321
|
+
...r,
|
|
3322
|
+
kind: r.kind,
|
|
3323
|
+
status: r.status,
|
|
3324
|
+
lockedAt: r.lockedAt?.toISOString() ?? null,
|
|
3325
|
+
});
|
|
2865
3326
|
//# sourceMappingURL=router.js.map
|