@hed-hog/operations 0.0.302 → 0.0.304
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/README.md +200 -43
- package/dist/controllers/operations-approvals.controller.d.ts +9 -0
- package/dist/controllers/operations-approvals.controller.d.ts.map +1 -0
- package/dist/controllers/operations-approvals.controller.js +64 -0
- package/dist/controllers/operations-approvals.controller.js.map +1 -0
- package/dist/controllers/operations-collaborators.controller.d.ts +223 -0
- package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -0
- package/dist/controllers/operations-collaborators.controller.js +96 -0
- package/dist/controllers/operations-collaborators.controller.js.map +1 -0
- package/dist/controllers/operations-contracts.controller.d.ts +683 -0
- package/dist/controllers/operations-contracts.controller.d.ts.map +1 -0
- package/dist/controllers/operations-contracts.controller.js +198 -0
- package/dist/controllers/operations-contracts.controller.js.map +1 -0
- package/dist/controllers/operations-org-structure.controller.d.ts +108 -0
- package/dist/controllers/operations-org-structure.controller.d.ts.map +1 -0
- package/dist/controllers/operations-org-structure.controller.js +143 -0
- package/dist/controllers/operations-org-structure.controller.js.map +1 -0
- package/dist/controllers/operations-projects.controller.d.ts +169 -0
- package/dist/controllers/operations-projects.controller.d.ts.map +1 -0
- package/dist/controllers/operations-projects.controller.js +87 -0
- package/dist/controllers/operations-projects.controller.js.map +1 -0
- package/dist/controllers/operations-tasks.controller.d.ts +54 -0
- package/dist/controllers/operations-tasks.controller.d.ts.map +1 -0
- package/dist/controllers/operations-tasks.controller.js +79 -0
- package/dist/controllers/operations-tasks.controller.js.map +1 -0
- package/dist/controllers/operations-timesheets.controller.d.ts +99 -0
- package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -0
- package/dist/controllers/operations-timesheets.controller.js +154 -0
- package/dist/controllers/operations-timesheets.controller.js.map +1 -0
- package/dist/dto/create-collaborator-type.dto.d.ts +10 -0
- package/dist/dto/create-collaborator-type.dto.d.ts.map +1 -0
- package/dist/dto/create-collaborator-type.dto.js +56 -0
- package/dist/dto/create-collaborator-type.dto.js.map +1 -0
- package/dist/dto/create-collaborator.dto.d.ts +42 -0
- package/dist/dto/create-collaborator.dto.d.ts.map +1 -0
- package/dist/dto/create-collaborator.dto.js +228 -0
- package/dist/dto/create-collaborator.dto.js.map +1 -0
- package/dist/dto/create-schedule-adjustment-request.dto.d.ts +17 -0
- package/dist/dto/create-schedule-adjustment-request.dto.d.ts.map +1 -0
- package/dist/dto/create-schedule-adjustment-request.dto.js +89 -0
- package/dist/dto/create-schedule-adjustment-request.dto.js.map +1 -0
- package/dist/dto/create-task.dto.d.ts +8 -0
- package/dist/dto/create-task.dto.d.ts.map +1 -0
- package/dist/dto/create-task.dto.js +50 -0
- package/dist/dto/create-task.dto.js.map +1 -0
- package/dist/dto/create-time-off-request.dto.d.ts +9 -0
- package/dist/dto/create-time-off-request.dto.d.ts.map +1 -0
- package/dist/dto/create-time-off-request.dto.js +54 -0
- package/dist/dto/create-time-off-request.dto.js.map +1 -0
- package/dist/dto/create-timesheet-entry.dto.d.ts +12 -0
- package/dist/dto/create-timesheet-entry.dto.d.ts.map +1 -0
- package/dist/dto/create-timesheet-entry.dto.js +75 -0
- package/dist/dto/create-timesheet-entry.dto.js.map +1 -0
- package/dist/dto/list-collaborator-types.dto.d.ts +4 -0
- package/dist/dto/list-collaborator-types.dto.d.ts.map +1 -0
- package/dist/dto/list-collaborator-types.dto.js +29 -0
- package/dist/dto/list-collaborator-types.dto.js.map +1 -0
- package/dist/dto/list-collaborators.dto.d.ts +8 -0
- package/dist/dto/list-collaborators.dto.d.ts.map +1 -0
- package/dist/dto/list-collaborators.dto.js +42 -0
- package/dist/dto/list-collaborators.dto.js.map +1 -0
- package/dist/dto/list-project-options.dto.d.ts +4 -0
- package/dist/dto/list-project-options.dto.d.ts.map +1 -0
- package/dist/dto/list-project-options.dto.js +8 -0
- package/dist/dto/list-project-options.dto.js.map +1 -0
- package/dist/dto/list-tasks.dto.d.ts +7 -0
- package/dist/dto/list-tasks.dto.d.ts.map +1 -0
- package/dist/dto/list-tasks.dto.js +38 -0
- package/dist/dto/list-tasks.dto.js.map +1 -0
- package/dist/dto/list-timesheet-entries.dto.d.ts +10 -0
- package/dist/dto/list-timesheet-entries.dto.d.ts.map +1 -0
- package/dist/dto/list-timesheet-entries.dto.js +54 -0
- package/dist/dto/list-timesheet-entries.dto.js.map +1 -0
- package/dist/dto/update-collaborator-type.dto.d.ts +4 -0
- package/dist/dto/update-collaborator-type.dto.d.ts.map +1 -0
- package/dist/dto/update-collaborator-type.dto.js +8 -0
- package/dist/dto/update-collaborator-type.dto.js.map +1 -0
- package/dist/dto/update-collaborator.dto.d.ts +4 -0
- package/dist/dto/update-collaborator.dto.d.ts.map +1 -0
- package/dist/dto/update-collaborator.dto.js +8 -0
- package/dist/dto/update-collaborator.dto.js.map +1 -0
- package/dist/dto/update-task.dto.d.ts +8 -0
- package/dist/dto/update-task.dto.d.ts.map +1 -0
- package/dist/dto/update-task.dto.js +51 -0
- package/dist/dto/update-task.dto.js.map +1 -0
- package/dist/operations.controller.d.ts +0 -1045
- package/dist/operations.controller.d.ts.map +1 -1
- package/dist/operations.controller.js +0 -429
- package/dist/operations.controller.js.map +1 -1
- package/dist/operations.module.d.ts.map +1 -1
- package/dist/operations.module.js +23 -2
- package/dist/operations.module.js.map +1 -1
- package/dist/operations.service.d.ts +373 -8
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +1598 -111
- package/dist/operations.service.js.map +1 -1
- package/dist/operations.service.spec.js +315 -1
- package/dist/operations.service.spec.js.map +1 -1
- package/dist/services/shared/operations-access.service.d.ts +16 -0
- package/dist/services/shared/operations-access.service.d.ts.map +1 -0
- package/dist/services/shared/operations-access.service.js +48 -0
- package/dist/services/shared/operations-access.service.js.map +1 -0
- package/hedhog/data/dashboard.yaml +20 -0
- package/hedhog/data/dashboard_component.yaml +274 -0
- package/hedhog/data/dashboard_component_role.yaml +174 -0
- package/hedhog/data/dashboard_item.yaml +299 -0
- package/hedhog/data/dashboard_role.yaml +20 -0
- package/hedhog/data/menu.yaml +30 -13
- package/hedhog/data/operations_collaborator_type.yaml +76 -0
- package/hedhog/data/route.yaml +183 -0
- package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +231 -0
- package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +134 -49
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +772 -93
- package/hedhog/frontend/app/_components/department-select-with-create.tsx.ejs +38 -16
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +875 -632
- package/hedhog/frontend/app/_components/timesheet-task-create-sheet.tsx.ejs +213 -0
- package/hedhog/frontend/app/_lib/api.ts.ejs +30 -1
- package/hedhog/frontend/app/_lib/types.ts.ejs +142 -39
- package/hedhog/frontend/app/_lib/utils/format.ts.ejs +33 -2
- package/hedhog/frontend/app/approvals/page.tsx.ejs +116 -98
- package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +502 -0
- package/hedhog/frontend/app/collaborators/page.tsx.ejs +109 -68
- package/hedhog/frontend/app/contracts/page.tsx.ejs +99 -102
- package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +98 -102
- package/hedhog/frontend/app/departments/page.tsx.ejs +96 -75
- package/hedhog/frontend/app/projects/page.tsx.ejs +137 -127
- package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +244 -120
- package/hedhog/frontend/app/team/page.tsx.ejs +15 -2
- package/hedhog/frontend/app/time-off/page.tsx.ejs +158 -82
- package/hedhog/frontend/app/timesheets/page.tsx.ejs +814 -357
- package/hedhog/frontend/messages/en.json +243 -51
- package/hedhog/frontend/messages/pt.json +458 -268
- package/hedhog/table/operations_collaborator.yaml +26 -13
- package/hedhog/table/operations_collaborator_equity_participation.yaml +43 -0
- package/hedhog/table/operations_collaborator_type.yaml +33 -0
- package/hedhog/table/operations_job_title.yaml +24 -0
- package/hedhog/table/operations_project_assignment.yaml +9 -0
- package/hedhog/table/operations_project_role.yaml +39 -0
- package/hedhog/table/operations_task.yaml +30 -0
- package/hedhog/table/operations_timesheet_entry.yaml +12 -0
- package/package.json +6 -6
- package/src/controllers/operations-approvals.controller.ts +24 -0
- package/src/controllers/operations-collaborators.controller.ts +60 -0
- package/src/controllers/operations-contracts.controller.ts +138 -0
- package/src/controllers/operations-org-structure.controller.ts +92 -0
- package/src/controllers/operations-projects.controller.ts +50 -0
- package/src/controllers/operations-tasks.controller.ts +52 -0
- package/src/controllers/operations-timesheets.controller.ts +100 -0
- package/src/dto/create-collaborator-type.dto.ts +43 -0
- package/src/dto/create-collaborator.dto.ts +223 -0
- package/src/dto/create-schedule-adjustment-request.dto.ts +91 -0
- package/src/dto/create-task.dto.ts +35 -0
- package/src/dto/create-time-off-request.dto.ts +53 -0
- package/src/dto/create-timesheet-entry.dto.ts +67 -0
- package/src/dto/list-collaborator-types.dto.ts +15 -0
- package/src/dto/list-collaborators.dto.ts +30 -0
- package/src/dto/list-project-options.dto.ts +3 -0
- package/src/dto/list-tasks.dto.ts +25 -0
- package/src/dto/list-timesheet-entries.dto.ts +40 -0
- package/src/dto/update-collaborator-type.dto.ts +3 -0
- package/src/dto/update-collaborator.dto.ts +3 -0
- package/src/dto/update-task.dto.ts +36 -0
- package/src/operations.controller.ts +1 -278
- package/src/operations.module.ts +23 -2
- package/src/operations.service.spec.ts +450 -0
- package/src/operations.service.ts +4641 -2163
- package/src/services/shared/operations-access.service.ts +52 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { BadRequestException } from '@nestjs/common';
|
|
4
4
|
import { OperationsService } from './operations.service';
|
|
5
|
+
import { OperationsAccessService } from './services/shared/operations-access.service';
|
|
5
6
|
|
|
6
7
|
describe('OperationsService proposal integration', () => {
|
|
7
8
|
let service: OperationsService;
|
|
@@ -23,6 +24,7 @@ describe('OperationsService proposal integration', () => {
|
|
|
23
24
|
integrationApi as any,
|
|
24
25
|
{} as any,
|
|
25
26
|
{} as any,
|
|
27
|
+
new OperationsAccessService(),
|
|
26
28
|
);
|
|
27
29
|
|
|
28
30
|
jest.spyOn(service as any, 'generateContractCode').mockResolvedValue('CTR-001');
|
|
@@ -208,3 +210,451 @@ describe('OperationsService proposal integration', () => {
|
|
|
208
210
|
);
|
|
209
211
|
});
|
|
210
212
|
});
|
|
213
|
+
|
|
214
|
+
describe('OperationsService quick-entry timesheets', () => {
|
|
215
|
+
let service: OperationsService;
|
|
216
|
+
|
|
217
|
+
beforeEach(() => {
|
|
218
|
+
service = new OperationsService(
|
|
219
|
+
{
|
|
220
|
+
$transaction: jest.fn(),
|
|
221
|
+
} as any,
|
|
222
|
+
{} as any,
|
|
223
|
+
{
|
|
224
|
+
publishEvent: jest.fn().mockResolvedValue(undefined),
|
|
225
|
+
} as any,
|
|
226
|
+
{} as any,
|
|
227
|
+
{} as any,
|
|
228
|
+
new OperationsAccessService(),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
jest.spyOn(service as any, 'getActorContext').mockResolvedValue({
|
|
232
|
+
userId: 15,
|
|
233
|
+
roleSlugs: ['admin-operations-collaborator'],
|
|
234
|
+
collaboratorId: 7,
|
|
235
|
+
collaboratorName: 'Taylor Tester',
|
|
236
|
+
isDirector: false,
|
|
237
|
+
isSupervisor: false,
|
|
238
|
+
isCollaborator: true,
|
|
239
|
+
teamCollaboratorIds: [],
|
|
240
|
+
visibleCollaboratorIds: [7],
|
|
241
|
+
visibleProjectIds: [11],
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
afterEach(() => {
|
|
246
|
+
jest.restoreAllMocks();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('rejects quick entries with non-positive duration', async () => {
|
|
250
|
+
await expect(
|
|
251
|
+
service.createTimesheetEntry(15, {
|
|
252
|
+
projectId: 11,
|
|
253
|
+
workDate: '2026-04-09',
|
|
254
|
+
duration: 0,
|
|
255
|
+
unit: 'minutes',
|
|
256
|
+
} as any),
|
|
257
|
+
).rejects.toThrow(BadRequestException);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('rejects quick entries without a work date', async () => {
|
|
261
|
+
await expect(
|
|
262
|
+
service.createTimesheetEntry(15, {
|
|
263
|
+
projectId: 11,
|
|
264
|
+
duration: 30,
|
|
265
|
+
unit: 'minutes',
|
|
266
|
+
} as any),
|
|
267
|
+
).rejects.toThrow(BadRequestException);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('stores quick-entry duration in minutes and keeps hours compatible', async () => {
|
|
271
|
+
const tx = {
|
|
272
|
+
$queryRawUnsafe: jest.fn().mockResolvedValue([{ id: 91 }]),
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
(service as any).prisma.$transaction.mockImplementation(
|
|
276
|
+
async (callback: (client: unknown) => unknown) => callback(tx),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
jest.spyOn(service as any, 'resolveOwnedProjectAssignment').mockResolvedValue({
|
|
280
|
+
id: 33,
|
|
281
|
+
projectId: 11,
|
|
282
|
+
projectName: 'Project Atlas',
|
|
283
|
+
projectCode: 'OPS-11',
|
|
284
|
+
roleLabel: 'Engineer',
|
|
285
|
+
});
|
|
286
|
+
jest.spyOn(service as any, 'getOwnedTaskRecord').mockResolvedValue({
|
|
287
|
+
id: 44,
|
|
288
|
+
name: 'Implement API',
|
|
289
|
+
projectAssignmentId: 33,
|
|
290
|
+
projectId: 11,
|
|
291
|
+
projectName: 'Project Atlas',
|
|
292
|
+
projectCode: 'OPS-11',
|
|
293
|
+
});
|
|
294
|
+
jest.spyOn(service as any, 'getOrCreateTimesheetForWorkDate').mockResolvedValue(55);
|
|
295
|
+
jest.spyOn(service as any, 'refreshTimesheetTotal').mockResolvedValue(undefined);
|
|
296
|
+
jest.spyOn(service as any, 'getTimesheetEntryByIdForActor').mockResolvedValue({
|
|
297
|
+
id: 91,
|
|
298
|
+
durationMinutes: 90,
|
|
299
|
+
hours: 1.5,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const result = await service.createTimesheetEntry(15, {
|
|
303
|
+
projectId: 11,
|
|
304
|
+
taskId: 44,
|
|
305
|
+
workDate: '2026-04-09',
|
|
306
|
+
duration: 1.5,
|
|
307
|
+
unit: 'hours',
|
|
308
|
+
description: 'Worked on the API layer',
|
|
309
|
+
} as any);
|
|
310
|
+
|
|
311
|
+
expect(result).toEqual(
|
|
312
|
+
expect.objectContaining({
|
|
313
|
+
id: 91,
|
|
314
|
+
durationMinutes: 90,
|
|
315
|
+
hours: 1.5,
|
|
316
|
+
}),
|
|
317
|
+
);
|
|
318
|
+
expect(tx.$queryRawUnsafe).toHaveBeenCalledWith(
|
|
319
|
+
expect.stringContaining('INSERT INTO operations_timesheet_entry'),
|
|
320
|
+
55,
|
|
321
|
+
33,
|
|
322
|
+
44,
|
|
323
|
+
'Implement API',
|
|
324
|
+
'2026-04-09',
|
|
325
|
+
90,
|
|
326
|
+
1.5,
|
|
327
|
+
'Worked on the API layer',
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('uses the task assignment when full timesheet entries omit projectAssignmentId', async () => {
|
|
332
|
+
const tx = {
|
|
333
|
+
$queryRawUnsafe: jest.fn().mockResolvedValue([{ id: 55 }]),
|
|
334
|
+
$executeRawUnsafe: jest.fn().mockResolvedValue(undefined),
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
(service as any).prisma.$transaction.mockImplementation(
|
|
338
|
+
async (callback: (client: unknown) => unknown) => callback(tx),
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
jest.spyOn(service as any, 'getCollaboratorById').mockResolvedValue({
|
|
342
|
+
id: 7,
|
|
343
|
+
supervisorId: 18,
|
|
344
|
+
});
|
|
345
|
+
jest.spyOn(service as any, 'getOwnedTaskRecord').mockResolvedValue({
|
|
346
|
+
id: 44,
|
|
347
|
+
name: 'Implement API',
|
|
348
|
+
projectAssignmentId: 33,
|
|
349
|
+
projectId: 11,
|
|
350
|
+
projectName: 'Project Atlas',
|
|
351
|
+
projectCode: 'OPS-11',
|
|
352
|
+
});
|
|
353
|
+
jest.spyOn(service as any, 'refreshTimesheetTotal').mockResolvedValue(undefined);
|
|
354
|
+
jest.spyOn(service as any, 'listSingleTimesheet').mockResolvedValue({
|
|
355
|
+
id: 55,
|
|
356
|
+
status: 'draft',
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
await service.createTimesheet(15, {
|
|
360
|
+
weekStartDate: '2026-04-06',
|
|
361
|
+
weekEndDate: '2026-04-12',
|
|
362
|
+
entries: [
|
|
363
|
+
{
|
|
364
|
+
taskId: 44,
|
|
365
|
+
workDate: '2026-04-09',
|
|
366
|
+
duration: 2,
|
|
367
|
+
unit: 'hours',
|
|
368
|
+
description: 'Worked on the API layer',
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
} as any);
|
|
372
|
+
|
|
373
|
+
expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(
|
|
374
|
+
expect.stringContaining('INSERT INTO operations_timesheet_entry'),
|
|
375
|
+
55,
|
|
376
|
+
33,
|
|
377
|
+
44,
|
|
378
|
+
'Implement API',
|
|
379
|
+
'2026-04-09',
|
|
380
|
+
120,
|
|
381
|
+
2,
|
|
382
|
+
'Worked on the API layer',
|
|
383
|
+
);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('rejects submitting a timesheet without a resolved approver', async () => {
|
|
387
|
+
const tx = {
|
|
388
|
+
$executeRawUnsafe: jest.fn().mockResolvedValue(undefined),
|
|
389
|
+
$queryRawUnsafe: jest.fn().mockResolvedValue([{ id: 1 }]),
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
(service as any).prisma.$transaction.mockImplementation(
|
|
393
|
+
async (callback: (client: unknown) => unknown) => callback(tx),
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
jest.spyOn(service as any, 'getTimesheetById').mockResolvedValue({
|
|
397
|
+
id: 55,
|
|
398
|
+
collaboratorId: 7,
|
|
399
|
+
approverCollaboratorId: null,
|
|
400
|
+
status: 'draft',
|
|
401
|
+
});
|
|
402
|
+
jest.spyOn(service as any, 'getCollaboratorById').mockResolvedValue({
|
|
403
|
+
id: 7,
|
|
404
|
+
supervisorId: null,
|
|
405
|
+
});
|
|
406
|
+
jest.spyOn(service as any, 'upsertApproval').mockResolvedValue(undefined);
|
|
407
|
+
jest.spyOn(service as any, 'listSingleTimesheet').mockResolvedValue({
|
|
408
|
+
id: 55,
|
|
409
|
+
status: 'submitted',
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
await expect(service.submitTimesheet(15, 55)).rejects.toThrow(BadRequestException);
|
|
413
|
+
expect(tx.$executeRawUnsafe).not.toHaveBeenCalled();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('rejects submitting a timesheet without active entries', async () => {
|
|
417
|
+
const tx = {
|
|
418
|
+
$executeRawUnsafe: jest.fn().mockResolvedValue(undefined),
|
|
419
|
+
$queryRawUnsafe: jest.fn().mockResolvedValue([]),
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
(service as any).prisma.$transaction.mockImplementation(
|
|
423
|
+
async (callback: (client: unknown) => unknown) => callback(tx),
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
jest.spyOn(service as any, 'getTimesheetById').mockResolvedValue({
|
|
427
|
+
id: 56,
|
|
428
|
+
collaboratorId: 7,
|
|
429
|
+
approverCollaboratorId: 18,
|
|
430
|
+
status: 'draft',
|
|
431
|
+
});
|
|
432
|
+
jest.spyOn(service as any, 'getCollaboratorById').mockResolvedValue({
|
|
433
|
+
id: 7,
|
|
434
|
+
supervisorId: 18,
|
|
435
|
+
});
|
|
436
|
+
jest.spyOn(service as any, 'querySingle').mockResolvedValue({ exists: false });
|
|
437
|
+
jest.spyOn(service as any, 'upsertApproval').mockResolvedValue(undefined);
|
|
438
|
+
jest.spyOn(service as any, 'listSingleTimesheet').mockResolvedValue({
|
|
439
|
+
id: 56,
|
|
440
|
+
status: 'submitted',
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
await expect(service.submitTimesheet(15, 56)).rejects.toThrow(BadRequestException);
|
|
444
|
+
expect(tx.$executeRawUnsafe).not.toHaveBeenCalled();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('deletes only draft quick entries and refreshes the weekly total', async () => {
|
|
448
|
+
const tx = {
|
|
449
|
+
$executeRawUnsafe: jest.fn().mockResolvedValue(undefined),
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
(service as any).prisma.$transaction.mockImplementation(
|
|
453
|
+
async (callback: (client: unknown) => unknown) => callback(tx),
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
jest.spyOn(service as any, 'getTimesheetEntryByIdForActor').mockResolvedValue({
|
|
457
|
+
id: 91,
|
|
458
|
+
timesheetId: 55,
|
|
459
|
+
collaboratorId: 7,
|
|
460
|
+
status: 'draft',
|
|
461
|
+
});
|
|
462
|
+
jest.spyOn(service as any, 'refreshTimesheetTotal').mockResolvedValue(undefined);
|
|
463
|
+
|
|
464
|
+
await expect(service.removeTimesheetEntry(15, 91)).resolves.toEqual({
|
|
465
|
+
success: true,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(
|
|
469
|
+
expect.stringContaining('UPDATE operations_timesheet_entry'),
|
|
470
|
+
91,
|
|
471
|
+
);
|
|
472
|
+
expect((service as any).refreshTimesheetTotal).toHaveBeenCalledWith(tx, 55);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('rejects deleting entries from submitted timesheets', async () => {
|
|
476
|
+
jest.spyOn(service as any, 'getTimesheetEntryByIdForActor').mockResolvedValue({
|
|
477
|
+
id: 92,
|
|
478
|
+
timesheetId: 56,
|
|
479
|
+
collaboratorId: 7,
|
|
480
|
+
status: 'submitted',
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
await expect(service.removeTimesheetEntry(15, 92)).rejects.toThrow(
|
|
484
|
+
BadRequestException,
|
|
485
|
+
);
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
describe('OperationsService approval side effects', () => {
|
|
490
|
+
let service: OperationsService;
|
|
491
|
+
|
|
492
|
+
beforeEach(() => {
|
|
493
|
+
service = new OperationsService(
|
|
494
|
+
{
|
|
495
|
+
$transaction: jest.fn(),
|
|
496
|
+
} as any,
|
|
497
|
+
{} as any,
|
|
498
|
+
{
|
|
499
|
+
publishEvent: jest.fn().mockResolvedValue(undefined),
|
|
500
|
+
} as any,
|
|
501
|
+
{} as any,
|
|
502
|
+
{} as any,
|
|
503
|
+
new OperationsAccessService(),
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
jest.spyOn(service as any, 'getActorContext').mockResolvedValue({
|
|
507
|
+
userId: 22,
|
|
508
|
+
roleSlugs: ['admin-operations-supervisor'],
|
|
509
|
+
collaboratorId: 18,
|
|
510
|
+
collaboratorName: 'Sam Supervisor',
|
|
511
|
+
isDirector: false,
|
|
512
|
+
isSupervisor: true,
|
|
513
|
+
isCollaborator: false,
|
|
514
|
+
teamCollaboratorIds: [7],
|
|
515
|
+
visibleCollaboratorIds: [7, 18],
|
|
516
|
+
visibleProjectIds: [11],
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
afterEach(() => {
|
|
521
|
+
jest.restoreAllMocks();
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('applies approved permanent schedule adjustments to the active collaborator weekly schedule', async () => {
|
|
525
|
+
const tx = {
|
|
526
|
+
$executeRawUnsafe: jest.fn().mockResolvedValue(undefined),
|
|
527
|
+
$queryRawUnsafe: jest
|
|
528
|
+
.fn()
|
|
529
|
+
.mockResolvedValueOnce([
|
|
530
|
+
{
|
|
531
|
+
collaboratorId: 7,
|
|
532
|
+
requestScope: 'permanent',
|
|
533
|
+
},
|
|
534
|
+
])
|
|
535
|
+
.mockResolvedValueOnce([
|
|
536
|
+
{
|
|
537
|
+
weekday: 'monday',
|
|
538
|
+
isWorkingDay: true,
|
|
539
|
+
startTime: '08:00',
|
|
540
|
+
endTime: '17:00',
|
|
541
|
+
breakMinutes: 45,
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
weekday: 'friday',
|
|
545
|
+
isWorkingDay: false,
|
|
546
|
+
startTime: null,
|
|
547
|
+
endTime: null,
|
|
548
|
+
breakMinutes: 0,
|
|
549
|
+
},
|
|
550
|
+
]),
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
(service as any).prisma.$transaction.mockImplementation(
|
|
554
|
+
async (callback: (client: unknown) => unknown) => callback(tx),
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
jest.spyOn(service as any, 'querySingle')
|
|
558
|
+
.mockResolvedValueOnce({
|
|
559
|
+
id: 81,
|
|
560
|
+
targetType: 'schedule_adjustment_request',
|
|
561
|
+
targetId: 400,
|
|
562
|
+
requesterCollaboratorId: 7,
|
|
563
|
+
approverCollaboratorId: 18,
|
|
564
|
+
status: 'pending',
|
|
565
|
+
})
|
|
566
|
+
.mockResolvedValueOnce({
|
|
567
|
+
id: 81,
|
|
568
|
+
targetType: 'schedule_adjustment_request',
|
|
569
|
+
targetId: 400,
|
|
570
|
+
status: 'approved',
|
|
571
|
+
decidedAt: '2026-04-11T12:00:00.000Z',
|
|
572
|
+
decisionNote: 'Approved for the new weekly schedule.',
|
|
573
|
+
});
|
|
574
|
+
jest.spyOn(service as any, 'insertApprovalHistory').mockResolvedValue(undefined);
|
|
575
|
+
|
|
576
|
+
await expect(
|
|
577
|
+
service.approve(22, 81, {
|
|
578
|
+
note: 'Approved for the new weekly schedule.',
|
|
579
|
+
}),
|
|
580
|
+
).resolves.toEqual(
|
|
581
|
+
expect.objectContaining({
|
|
582
|
+
id: 81,
|
|
583
|
+
status: 'approved',
|
|
584
|
+
}),
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(
|
|
588
|
+
expect.stringContaining('UPDATE operations_schedule_adjustment_request'),
|
|
589
|
+
'approved',
|
|
590
|
+
18,
|
|
591
|
+
400,
|
|
592
|
+
);
|
|
593
|
+
expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(
|
|
594
|
+
expect.stringContaining('UPDATE operations_collaborator_schedule_day'),
|
|
595
|
+
7,
|
|
596
|
+
);
|
|
597
|
+
expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(
|
|
598
|
+
expect.stringContaining('INSERT INTO operations_collaborator_schedule_day'),
|
|
599
|
+
7,
|
|
600
|
+
'monday',
|
|
601
|
+
true,
|
|
602
|
+
'08:00',
|
|
603
|
+
'17:00',
|
|
604
|
+
45,
|
|
605
|
+
);
|
|
606
|
+
expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(
|
|
607
|
+
expect.stringContaining('INSERT INTO operations_collaborator_schedule_day'),
|
|
608
|
+
7,
|
|
609
|
+
'friday',
|
|
610
|
+
false,
|
|
611
|
+
null,
|
|
612
|
+
null,
|
|
613
|
+
0,
|
|
614
|
+
);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it('keeps the collaborator weekly schedule unchanged for approved temporary adjustments', async () => {
|
|
618
|
+
const tx = {
|
|
619
|
+
$executeRawUnsafe: jest.fn().mockResolvedValue(undefined),
|
|
620
|
+
$queryRawUnsafe: jest.fn().mockResolvedValueOnce([
|
|
621
|
+
{
|
|
622
|
+
collaboratorId: 7,
|
|
623
|
+
requestScope: 'temporary',
|
|
624
|
+
},
|
|
625
|
+
]),
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
(service as any).prisma.$transaction.mockImplementation(
|
|
629
|
+
async (callback: (client: unknown) => unknown) => callback(tx),
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
jest.spyOn(service as any, 'querySingle')
|
|
633
|
+
.mockResolvedValueOnce({
|
|
634
|
+
id: 82,
|
|
635
|
+
targetType: 'schedule_adjustment_request',
|
|
636
|
+
targetId: 401,
|
|
637
|
+
requesterCollaboratorId: 7,
|
|
638
|
+
approverCollaboratorId: 18,
|
|
639
|
+
status: 'pending',
|
|
640
|
+
})
|
|
641
|
+
.mockResolvedValueOnce({
|
|
642
|
+
id: 82,
|
|
643
|
+
targetType: 'schedule_adjustment_request',
|
|
644
|
+
targetId: 401,
|
|
645
|
+
status: 'approved',
|
|
646
|
+
decidedAt: '2026-04-11T12:05:00.000Z',
|
|
647
|
+
decisionNote: 'Approved as a temporary exception.',
|
|
648
|
+
});
|
|
649
|
+
jest.spyOn(service as any, 'insertApprovalHistory').mockResolvedValue(undefined);
|
|
650
|
+
|
|
651
|
+
await service.approve(22, 82, {
|
|
652
|
+
note: 'Approved as a temporary exception.',
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
expect(tx.$executeRawUnsafe).not.toHaveBeenCalledWith(
|
|
656
|
+
expect.stringContaining('UPDATE operations_collaborator_schedule_day'),
|
|
657
|
+
7,
|
|
658
|
+
);
|
|
659
|
+
});
|
|
660
|
+
});
|