@hed-hog/operations 0.0.294 → 0.0.296
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/operations.controller.d.ts +415 -0
- package/dist/operations.controller.d.ts.map +1 -0
- package/dist/operations.controller.js +333 -0
- package/dist/operations.controller.js.map +1 -0
- package/dist/operations.module.d.ts.map +1 -1
- package/dist/operations.module.js +4 -3
- package/dist/operations.module.js.map +1 -1
- package/dist/operations.service.d.ts +589 -153
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +2229 -100
- package/dist/operations.service.js.map +1 -1
- package/hedhog/data/menu.yaml +198 -251
- package/hedhog/data/role.yaml +23 -14
- package/hedhog/data/route.yaml +317 -143
- package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +310 -0
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +631 -0
- package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +132 -0
- package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +558 -0
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +291 -0
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +689 -0
- package/hedhog/frontend/app/_lib/api.ts.ejs +32 -0
- package/hedhog/frontend/app/_lib/hooks/use-operations-access.ts.ejs +44 -0
- package/hedhog/frontend/app/_lib/types.ts.ejs +360 -0
- package/hedhog/frontend/app/_lib/utils/format.ts.ejs +129 -25
- package/hedhog/frontend/app/_lib/utils/forms.ts.ejs +14 -0
- package/hedhog/frontend/app/approvals/page.tsx.ejs +386 -147
- package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +11 -0
- package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +11 -0
- package/hedhog/frontend/app/collaborators/new/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/collaborators/page.tsx.ejs +261 -0
- package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +11 -0
- package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +11 -108
- package/hedhog/frontend/app/contracts/new/page.tsx.ejs +17 -0
- package/hedhog/frontend/app/contracts/page.tsx.ejs +262 -181
- package/hedhog/frontend/app/page.tsx.ejs +319 -177
- package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +11 -0
- package/hedhog/frontend/app/projects/[id]/page.tsx.ejs +11 -936
- package/hedhog/frontend/app/projects/new/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/projects/page.tsx.ejs +236 -1074
- package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +418 -0
- package/hedhog/frontend/app/team/page.tsx.ejs +339 -0
- package/hedhog/frontend/app/time-off/page.tsx.ejs +328 -0
- package/hedhog/frontend/app/timesheets/page.tsx.ejs +636 -126
- package/hedhog/frontend/messages/en.json +648 -454
- package/hedhog/frontend/messages/pt.json +647 -454
- package/hedhog/table/operations_approval.yaml +49 -0
- package/hedhog/table/operations_approval_history.yaml +29 -0
- package/hedhog/table/{operations_employee.yaml → operations_collaborator.yaml} +67 -64
- package/hedhog/table/operations_collaborator_schedule_day.yaml +34 -0
- package/hedhog/table/operations_contract.yaml +100 -48
- package/hedhog/table/operations_contract_document.yaml +39 -0
- package/hedhog/table/operations_contract_financial_term.yaml +40 -0
- package/hedhog/table/operations_contract_history.yaml +27 -0
- package/hedhog/table/operations_contract_party.yaml +46 -0
- package/hedhog/table/operations_contract_revision.yaml +38 -0
- package/hedhog/table/operations_contract_signature.yaml +38 -0
- package/hedhog/table/operations_project.yaml +54 -50
- package/hedhog/table/{operations_allocation.yaml → operations_project_assignment.yaml} +55 -52
- package/hedhog/table/operations_schedule_adjustment_day.yaml +34 -0
- package/hedhog/table/operations_schedule_adjustment_request.yaml +53 -0
- package/hedhog/table/operations_time_off_request.yaml +57 -0
- package/hedhog/table/operations_timesheet.yaml +41 -36
- package/hedhog/table/operations_timesheet_entry.yaml +40 -50
- package/package.json +7 -6
- package/src/operations.controller.ts +182 -0
- package/src/operations.module.ts +22 -21
- package/src/operations.service.ts +3595 -137
- package/hedhog/data/operations_career_level.yaml +0 -102
- package/hedhog/data/operations_career_track.yaml +0 -8
- package/hedhog/data/operations_certification.yaml +0 -38
- package/hedhog/data/operations_evaluation_cycle.yaml +0 -18
- package/hedhog/data/operations_performance_criterion.yaml +0 -48
- package/hedhog/frontend/app/_components/allocation-calendar.tsx.ejs +0 -56
- package/hedhog/frontend/app/_components/kanban-board.tsx.ejs +0 -626
- package/hedhog/frontend/app/_components/timesheet-entry-dialog.tsx.ejs +0 -142
- package/hedhog/frontend/app/_lib/hooks/use-operations-data.ts.ejs +0 -41
- package/hedhog/frontend/app/_lib/hooks/use-operations-growth-data.ts.ejs +0 -63
- package/hedhog/frontend/app/_lib/mocks/allocations.mock.ts.ejs +0 -74
- package/hedhog/frontend/app/_lib/mocks/contracts.mock.ts.ejs +0 -74
- package/hedhog/frontend/app/_lib/mocks/operations-growth.mock.ts.ejs +0 -824
- package/hedhog/frontend/app/_lib/mocks/projects.mock.ts.ejs +0 -455
- package/hedhog/frontend/app/_lib/mocks/tasks.mock.ts.ejs +0 -117
- package/hedhog/frontend/app/_lib/mocks/timesheets.mock.ts.ejs +0 -84
- package/hedhog/frontend/app/_lib/mocks/users.mock.ts.ejs +0 -67
- package/hedhog/frontend/app/_lib/services/contracts.service.ts.ejs +0 -10
- package/hedhog/frontend/app/_lib/services/operations-growth.service.ts.ejs +0 -31
- package/hedhog/frontend/app/_lib/services/projects.service.ts.ejs +0 -10
- package/hedhog/frontend/app/_lib/services/tasks.service.ts.ejs +0 -10
- package/hedhog/frontend/app/_lib/services/timesheets.service.ts.ejs +0 -10
- package/hedhog/frontend/app/_lib/types/operations-growth.ts.ejs +0 -209
- package/hedhog/frontend/app/_lib/types/operations.ts.ejs +0 -156
- package/hedhog/frontend/app/_lib/utils/growth.ts.ejs +0 -62
- package/hedhog/frontend/app/_lib/utils/metrics.ts.ejs +0 -103
- package/hedhog/frontend/app/_lib/utils/status.ts.ejs +0 -80
- package/hedhog/frontend/app/allocations/page.tsx.ejs +0 -155
- package/hedhog/frontend/app/career/page.tsx.ejs +0 -143
- package/hedhog/frontend/app/certifications/page.tsx.ejs +0 -202
- package/hedhog/frontend/app/evaluations/page.tsx.ejs +0 -278
- package/hedhog/frontend/app/goals/page.tsx.ejs +0 -171
- package/hedhog/frontend/app/growth/page.tsx.ejs +0 -288
- package/hedhog/frontend/app/manager/page.tsx.ejs +0 -175
- package/hedhog/frontend/app/rewards/page.tsx.ejs +0 -196
- package/hedhog/frontend/app/tasks/page.tsx.ejs +0 -999
- package/hedhog/table/operations_calibration_item.yaml +0 -61
- package/hedhog/table/operations_calibration_session.yaml +0 -25
- package/hedhog/table/operations_career_level.yaml +0 -75
- package/hedhog/table/operations_career_track.yaml +0 -21
- package/hedhog/table/operations_certification.yaml +0 -48
- package/hedhog/table/operations_employee_certification.yaml +0 -43
- package/hedhog/table/operations_employee_connect.yaml +0 -61
- package/hedhog/table/operations_employee_evaluation.yaml +0 -113
- package/hedhog/table/operations_employee_evaluation_item.yaml +0 -39
- package/hedhog/table/operations_employee_profile.yaml +0 -80
- package/hedhog/table/operations_employee_skill_matrix.yaml +0 -30
- package/hedhog/table/operations_evaluation_cycle.yaml +0 -31
- package/hedhog/table/operations_goal.yaml +0 -67
- package/hedhog/table/operations_goal_progress.yaml +0 -31
- package/hedhog/table/operations_performance_criterion.yaml +0 -29
- package/hedhog/table/operations_promotion_readiness.yaml +0 -49
- package/hedhog/table/operations_promotion_recommendation.yaml +0 -63
- package/hedhog/table/operations_public_recognition.yaml +0 -46
- package/hedhog/table/operations_reward.yaml +0 -100
- package/hedhog/table/operations_score_event.yaml +0 -81
- package/hedhog/table/operations_task.yaml +0 -60
- package/src/operations-data.controller.ts +0 -54
- package/src/operations-growth.controller.ts +0 -44
|
@@ -5,130 +5,2259 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
5
5
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
6
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
7
|
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
8
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
12
|
exports.OperationsService = void 0;
|
|
13
|
+
const api_prisma_1 = require("@hed-hog/api-prisma");
|
|
14
|
+
const core_1 = require("@hed-hog/core");
|
|
10
15
|
const common_1 = require("@nestjs/common");
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
{ id: 2, code: 'CTR-002', name: 'BluePeak Migration', status: 'renewal' },
|
|
15
|
-
],
|
|
16
|
-
projects: [
|
|
17
|
-
{ id: 1, code: 'PRJ-OPS-1', name: 'Phoenix Platform Rollout', status: 'active' },
|
|
18
|
-
{ id: 2, code: 'PRJ-OPS-2', name: 'Northwind Service Desk', status: 'at_risk' },
|
|
19
|
-
],
|
|
20
|
-
allocations: [
|
|
21
|
-
{ id: 1, employeeId: 1, projectId: 1, weeklyHours: 30, allocationPercent: 75 },
|
|
22
|
-
{ id: 2, employeeId: 2, projectId: 2, weeklyHours: 20, allocationPercent: 50 },
|
|
23
|
-
],
|
|
24
|
-
tasks: [
|
|
25
|
-
{ id: 1, title: 'Deploy onboarding workflow', status: 'review' },
|
|
26
|
-
{ id: 2, title: 'Refine support KPI dashboard', status: 'in_progress' },
|
|
27
|
-
],
|
|
28
|
-
timesheets: [
|
|
29
|
-
{ id: 1, employeeId: 1, status: 'submitted', cycleStartDate: '2026-03-09' },
|
|
30
|
-
{ id: 2, employeeId: 2, status: 'approved', cycleStartDate: '2026-03-09' },
|
|
31
|
-
],
|
|
32
|
-
approvals: [
|
|
33
|
-
{ id: 1, employeeId: 1, status: 'pending', hours: 8 },
|
|
34
|
-
{ id: 2, employeeId: 3, status: 'pending', hours: 6.5 },
|
|
35
|
-
],
|
|
36
|
-
};
|
|
37
|
-
const growthCollections = {
|
|
38
|
-
growth: {
|
|
39
|
-
topSignals: ['score_events', 'career_tracks', 'goals', 'recognitions'],
|
|
40
|
-
note: 'Foundation endpoint for the employee growth dashboard.',
|
|
41
|
-
},
|
|
42
|
-
evaluations: [
|
|
43
|
-
{ id: 1, employeeId: 1, cycle: '2026 H1 Growth Cycle', status: 'finalized', generatedScore: 84 },
|
|
44
|
-
{ id: 2, employeeId: 2, cycle: '2026 Delivery Sprint Review', status: 'submitted', generatedScore: 72 },
|
|
45
|
-
],
|
|
46
|
-
goals: [
|
|
47
|
-
{ id: 1, employeeId: 1, title: 'Reduce rework under 5%', status: 'active', progressPercent: 68 },
|
|
48
|
-
{ id: 2, employeeId: 2, title: 'Close Scrum certification', status: 'at_risk', progressPercent: 35 },
|
|
49
|
-
],
|
|
50
|
-
certifications: [
|
|
51
|
-
{ id: 1, slug: 'scrum-master-foundations', status: 'achieved' },
|
|
52
|
-
{ id: 2, slug: 'advanced-quality-assurance', status: 'planned' },
|
|
53
|
-
],
|
|
54
|
-
rewards: [
|
|
55
|
-
{ id: 1, employeeId: 1, rewardType: 'bonus', status: 'granted' },
|
|
56
|
-
{ id: 2, employeeId: 3, rewardType: 'recognition', status: 'granted' },
|
|
57
|
-
],
|
|
58
|
-
career: [
|
|
59
|
-
{ id: 1, track: 'Delivery', currentLevel: 'Consultant', nextLevel: 'Senior Consultant' },
|
|
60
|
-
{ id: 2, track: 'Specialist', currentLevel: 'Specialist II', nextLevel: 'Principal Specialist' },
|
|
61
|
-
],
|
|
62
|
-
manager: {
|
|
63
|
-
summary: {
|
|
64
|
-
promotionEligible: 2,
|
|
65
|
-
goalsAtRisk: 3,
|
|
66
|
-
pendingCertifications: 4,
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
};
|
|
16
|
+
const COLLABORATOR_ROLE = 'admin-operations-collaborator';
|
|
17
|
+
const SUPERVISOR_ROLE = 'admin-operations-supervisor';
|
|
18
|
+
const DIRECTOR_ROLE = 'admin-operations-director';
|
|
70
19
|
let OperationsService = class OperationsService {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
getDashboard() {
|
|
20
|
+
constructor(prisma, integrationApi) {
|
|
21
|
+
this.prisma = prisma;
|
|
22
|
+
this.integrationApi = integrationApi;
|
|
23
|
+
}
|
|
24
|
+
async getDashboard(userId) {
|
|
25
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
26
|
+
const actor = await this.getActorContext(userId);
|
|
27
|
+
const projectFilter = this.buildIdFilter(actor.visibleProjectIds, 'p.id', actor.isDirector);
|
|
28
|
+
const collaboratorFilter = this.buildIdFilter(actor.visibleCollaboratorIds, 't.collaborator_id', actor.isDirector);
|
|
29
|
+
const timeOffFilter = this.buildIdFilter(actor.visibleCollaboratorIds, 'tor.collaborator_id', actor.isDirector);
|
|
30
|
+
const scheduleFilter = this.buildIdFilter(actor.visibleCollaboratorIds, 'sar.collaborator_id', actor.isDirector);
|
|
31
|
+
const approvalFilter = actor.isDirector
|
|
32
|
+
? { clause: 'a.deleted_at IS NULL', params: [] }
|
|
33
|
+
: actor.collaboratorId
|
|
34
|
+
? {
|
|
35
|
+
clause: 'a.deleted_at IS NULL AND a.approver_collaborator_id = $1',
|
|
36
|
+
params: [actor.collaboratorId],
|
|
37
|
+
}
|
|
38
|
+
: { clause: '1 = 0', params: [] };
|
|
39
|
+
const [projects, timesheets, timeOff, schedules, approvals, recentTimesheets] = await Promise.all([
|
|
40
|
+
this.querySingle(`SELECT COUNT(*)::text AS total,
|
|
41
|
+
COUNT(*) FILTER (WHERE status = 'active')::text AS active
|
|
42
|
+
FROM operations_project p
|
|
43
|
+
WHERE p.deleted_at IS NULL AND ${projectFilter.clause}`, projectFilter.params),
|
|
44
|
+
this.querySingle(`SELECT COUNT(*)::text AS total,
|
|
45
|
+
COUNT(*) FILTER (WHERE status = 'submitted')::text AS submitted
|
|
46
|
+
FROM operations_timesheet t
|
|
47
|
+
WHERE t.deleted_at IS NULL AND ${collaboratorFilter.clause}`, collaboratorFilter.params),
|
|
48
|
+
this.querySingle(`SELECT COUNT(*)::text AS total,
|
|
49
|
+
COUNT(*) FILTER (WHERE status = 'submitted')::text AS submitted
|
|
50
|
+
FROM operations_time_off_request tor
|
|
51
|
+
WHERE tor.deleted_at IS NULL AND ${timeOffFilter.clause}`, timeOffFilter.params),
|
|
52
|
+
this.querySingle(`SELECT COUNT(*)::text AS total,
|
|
53
|
+
COUNT(*) FILTER (WHERE status = 'submitted')::text AS submitted
|
|
54
|
+
FROM operations_schedule_adjustment_request sar
|
|
55
|
+
WHERE sar.deleted_at IS NULL AND ${scheduleFilter.clause}`, scheduleFilter.params),
|
|
56
|
+
this.querySingle(`SELECT COUNT(*) FILTER (WHERE status = 'pending')::text AS pending
|
|
57
|
+
FROM operations_approval a
|
|
58
|
+
WHERE ${approvalFilter.clause}`, approvalFilter.params),
|
|
59
|
+
this.queryRows(`SELECT t.id,
|
|
60
|
+
c.display_name AS "collaboratorName",
|
|
61
|
+
t.week_start_date AS "weekStartDate",
|
|
62
|
+
t.week_end_date AS "weekEndDate",
|
|
63
|
+
t.total_hours AS "totalHours",
|
|
64
|
+
t.status
|
|
65
|
+
FROM operations_timesheet t
|
|
66
|
+
JOIN operations_collaborator c ON c.id = t.collaborator_id
|
|
67
|
+
WHERE t.deleted_at IS NULL AND ${collaboratorFilter.clause}
|
|
68
|
+
ORDER BY COALESCE(t.submitted_at, t.updated_at) DESC
|
|
69
|
+
LIMIT 5`, collaboratorFilter.params),
|
|
70
|
+
]);
|
|
76
71
|
return {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
actor: {
|
|
73
|
+
roleScope: actor.isDirector
|
|
74
|
+
? 'full'
|
|
75
|
+
: actor.isSupervisor
|
|
76
|
+
? 'team'
|
|
77
|
+
: 'self',
|
|
78
|
+
collaboratorId: actor.collaboratorId,
|
|
79
|
+
collaboratorName: actor.collaboratorName,
|
|
80
|
+
teamSize: actor.teamCollaboratorIds.length,
|
|
81
|
+
},
|
|
82
|
+
cards: {
|
|
83
|
+
projectsTotal: Number((_a = projects === null || projects === void 0 ? void 0 : projects.total) !== null && _a !== void 0 ? _a : 0),
|
|
84
|
+
activeProjects: Number((_b = projects === null || projects === void 0 ? void 0 : projects.active) !== null && _b !== void 0 ? _b : 0),
|
|
85
|
+
visibleTimesheets: Number((_c = timesheets === null || timesheets === void 0 ? void 0 : timesheets.total) !== null && _c !== void 0 ? _c : 0),
|
|
86
|
+
pendingTimesheets: Number((_d = timesheets === null || timesheets === void 0 ? void 0 : timesheets.submitted) !== null && _d !== void 0 ? _d : 0),
|
|
87
|
+
timeOffRequests: Number((_e = timeOff === null || timeOff === void 0 ? void 0 : timeOff.total) !== null && _e !== void 0 ? _e : 0),
|
|
88
|
+
scheduleAdjustmentRequests: Number((_f = schedules === null || schedules === void 0 ? void 0 : schedules.total) !== null && _f !== void 0 ? _f : 0),
|
|
89
|
+
pendingApprovals: Number((_g = approvals === null || approvals === void 0 ? void 0 : approvals.pending) !== null && _g !== void 0 ? _g : 0),
|
|
90
|
+
},
|
|
91
|
+
recentTimesheets,
|
|
80
92
|
};
|
|
81
93
|
}
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
async listCollaborators(userId) {
|
|
95
|
+
const actor = await this.getActorContext(userId);
|
|
96
|
+
this.ensureCollaborator(actor);
|
|
97
|
+
const filter = this.buildIdFilter(actor.visibleCollaboratorIds, 'c.id', actor.isDirector);
|
|
98
|
+
return this.queryRows(`SELECT c.id,
|
|
99
|
+
c.user_id AS "userId",
|
|
100
|
+
c.code,
|
|
101
|
+
c.collaborator_type AS "collaboratorType",
|
|
102
|
+
c.display_name AS "displayName",
|
|
103
|
+
c.department,
|
|
104
|
+
c.title,
|
|
105
|
+
c.level_label AS "levelLabel",
|
|
106
|
+
c.weekly_capacity_hours AS "weeklyCapacityHours",
|
|
107
|
+
c.status,
|
|
108
|
+
c.joined_at AS "joinedAt",
|
|
109
|
+
c.left_at AS "leftAt",
|
|
110
|
+
c.notes,
|
|
111
|
+
s.id AS "supervisorId",
|
|
112
|
+
s.display_name AS "supervisorName",
|
|
113
|
+
hiring_contract.id AS "contractId",
|
|
114
|
+
hiring_contract.status AS "contractStatus",
|
|
115
|
+
COUNT(DISTINCT pa.id)::int AS "activeAssignments"
|
|
116
|
+
FROM operations_collaborator c
|
|
117
|
+
LEFT JOIN operations_collaborator s
|
|
118
|
+
ON s.id = c.supervisor_collaborator_id
|
|
119
|
+
LEFT JOIN operations_project_assignment pa
|
|
120
|
+
ON pa.collaborator_id = c.id
|
|
121
|
+
AND pa.deleted_at IS NULL
|
|
122
|
+
AND pa.status IN ('planned', 'active')
|
|
123
|
+
LEFT JOIN LATERAL (
|
|
124
|
+
SELECT oc.id, oc.status
|
|
125
|
+
FROM operations_contract oc
|
|
126
|
+
WHERE oc.related_collaborator_id = c.id
|
|
127
|
+
AND oc.deleted_at IS NULL
|
|
128
|
+
ORDER BY CASE WHEN oc.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
|
|
129
|
+
oc.created_at DESC
|
|
130
|
+
LIMIT 1
|
|
131
|
+
) hiring_contract ON TRUE
|
|
132
|
+
WHERE c.deleted_at IS NULL
|
|
133
|
+
AND ${filter.clause}
|
|
134
|
+
GROUP BY c.id, s.id, hiring_contract.id, hiring_contract.status
|
|
135
|
+
ORDER BY c.display_name ASC`, filter.params);
|
|
84
136
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
137
|
+
async getMyCollaborator(userId) {
|
|
138
|
+
const actor = await this.getActorContext(userId);
|
|
139
|
+
if (!actor.collaboratorId) {
|
|
140
|
+
throw new common_1.NotFoundException('No collaborator profile linked to this user.');
|
|
141
|
+
}
|
|
142
|
+
return this.getCollaboratorByIdForUser(userId, actor.collaboratorId);
|
|
88
143
|
}
|
|
89
|
-
|
|
90
|
-
|
|
144
|
+
async getCollaboratorByIdForUser(userId, collaboratorId) {
|
|
145
|
+
const actor = await this.getActorContext(userId);
|
|
146
|
+
this.ensureCollaboratorAccess(actor, collaboratorId);
|
|
147
|
+
return this.getCollaboratorDetails(collaboratorId);
|
|
91
148
|
}
|
|
92
|
-
|
|
93
|
-
var _a;
|
|
94
|
-
|
|
149
|
+
async getTeam(userId) {
|
|
150
|
+
var _a, _b;
|
|
151
|
+
const actor = await this.getActorContext(userId);
|
|
152
|
+
this.ensureSupervisor(actor);
|
|
153
|
+
if (!actor.teamCollaboratorIds.length) {
|
|
154
|
+
return {
|
|
155
|
+
teamMembers: [],
|
|
156
|
+
projectCount: 0,
|
|
157
|
+
pendingApprovals: 0,
|
|
158
|
+
pendingItems: {
|
|
159
|
+
timesheets: 0,
|
|
160
|
+
timeOffRequests: 0,
|
|
161
|
+
scheduleAdjustmentRequests: 0,
|
|
162
|
+
},
|
|
163
|
+
teamProjects: [],
|
|
164
|
+
pendingApprovalQueue: [],
|
|
165
|
+
pendingTimeOffRequests: [],
|
|
166
|
+
pendingScheduleAdjustmentRequests: [],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const teamFilter = this.buildIdFilter(actor.teamCollaboratorIds, 'c.id', false);
|
|
170
|
+
const teamMembers = await this.queryRows(`SELECT c.id,
|
|
171
|
+
c.user_id AS "userId",
|
|
172
|
+
c.code,
|
|
173
|
+
c.display_name AS "displayName",
|
|
174
|
+
c.collaborator_type AS "collaboratorType",
|
|
175
|
+
c.department,
|
|
176
|
+
c.title,
|
|
177
|
+
c.status,
|
|
178
|
+
COUNT(DISTINCT pa.id)::int AS "activeAssignments",
|
|
179
|
+
COUNT(DISTINCT a.id) FILTER (WHERE a.status = 'pending')::int AS "pendingApprovals",
|
|
180
|
+
COUNT(DISTINCT tor.id) FILTER (WHERE tor.status = 'submitted')::int AS "pendingTimeOffRequests",
|
|
181
|
+
COUNT(DISTINCT sar.id) FILTER (WHERE sar.status = 'submitted')::int AS "pendingScheduleAdjustmentRequests"
|
|
182
|
+
FROM operations_collaborator c
|
|
183
|
+
LEFT JOIN operations_project_assignment pa
|
|
184
|
+
ON pa.collaborator_id = c.id
|
|
185
|
+
AND pa.deleted_at IS NULL
|
|
186
|
+
AND pa.status IN ('planned', 'active')
|
|
187
|
+
LEFT JOIN operations_approval a
|
|
188
|
+
ON a.requester_collaborator_id = c.id
|
|
189
|
+
AND a.deleted_at IS NULL
|
|
190
|
+
AND a.status = 'pending'
|
|
191
|
+
LEFT JOIN operations_time_off_request tor
|
|
192
|
+
ON tor.collaborator_id = c.id
|
|
193
|
+
AND tor.deleted_at IS NULL
|
|
194
|
+
AND tor.status = 'submitted'
|
|
195
|
+
LEFT JOIN operations_schedule_adjustment_request sar
|
|
196
|
+
ON sar.collaborator_id = c.id
|
|
197
|
+
AND sar.deleted_at IS NULL
|
|
198
|
+
AND sar.status = 'submitted'
|
|
199
|
+
WHERE c.deleted_at IS NULL AND ${teamFilter.clause}
|
|
200
|
+
GROUP BY c.id
|
|
201
|
+
ORDER BY c.display_name ASC`, teamFilter.params);
|
|
202
|
+
const [teamProjects, pendingApprovalQueue, pendingTimeOffRequests, pendingScheduleAdjustmentRequests] = await Promise.all([
|
|
203
|
+
this.queryRows(`SELECT p.id,
|
|
204
|
+
p.code,
|
|
205
|
+
p.name,
|
|
206
|
+
p.client_name AS "clientName",
|
|
207
|
+
p.status,
|
|
208
|
+
COUNT(DISTINCT pa.collaborator_id)::int AS "teamSize",
|
|
209
|
+
COUNT(DISTINCT t.id) FILTER (WHERE t.status = 'submitted')::int AS "pendingTimesheets"
|
|
210
|
+
FROM operations_project p
|
|
211
|
+
JOIN operations_project_assignment pa
|
|
212
|
+
ON pa.project_id = p.id
|
|
213
|
+
AND pa.deleted_at IS NULL
|
|
214
|
+
AND pa.status IN ('planned', 'active')
|
|
215
|
+
LEFT JOIN operations_timesheet_entry te
|
|
216
|
+
ON te.project_assignment_id = pa.id
|
|
217
|
+
AND te.deleted_at IS NULL
|
|
218
|
+
LEFT JOIN operations_timesheet t
|
|
219
|
+
ON t.id = te.timesheet_id
|
|
220
|
+
AND t.deleted_at IS NULL
|
|
221
|
+
WHERE p.deleted_at IS NULL
|
|
222
|
+
AND pa.collaborator_id = ANY($1::int[])
|
|
223
|
+
GROUP BY p.id
|
|
224
|
+
ORDER BY p.name ASC`, [actor.teamCollaboratorIds]),
|
|
225
|
+
this.queryRows(`SELECT a.id,
|
|
226
|
+
a.target_type AS "targetType",
|
|
227
|
+
a.target_id AS "targetId",
|
|
228
|
+
a.requester_collaborator_id AS "requesterCollaboratorId",
|
|
229
|
+
requester.display_name AS "requesterName",
|
|
230
|
+
a.approver_collaborator_id AS "approverCollaboratorId",
|
|
231
|
+
approver.display_name AS "approverName",
|
|
232
|
+
a.status,
|
|
233
|
+
a.submitted_at AS "submittedAt",
|
|
234
|
+
a.decided_at AS "decidedAt",
|
|
235
|
+
a.decision_note AS "decisionNote",
|
|
236
|
+
t.week_start_date AS "timesheetWeekStartDate",
|
|
237
|
+
t.week_end_date AS "timesheetWeekEndDate",
|
|
238
|
+
t.total_hours AS "timesheetTotalHours",
|
|
239
|
+
COALESCE(
|
|
240
|
+
STRING_AGG(DISTINCT p.name, ', ') FILTER (WHERE p.name IS NOT NULL),
|
|
241
|
+
''
|
|
242
|
+
) AS "timesheetProjectNames",
|
|
243
|
+
tor.request_type AS "timeOffType",
|
|
244
|
+
tor.start_date AS "timeOffStartDate",
|
|
245
|
+
tor.end_date AS "timeOffEndDate",
|
|
246
|
+
tor.reason AS "timeOffReason",
|
|
247
|
+
sar.request_scope AS "scheduleRequestScope",
|
|
248
|
+
sar.effective_start_date AS "scheduleStartDate",
|
|
249
|
+
sar.effective_end_date AS "scheduleEndDate",
|
|
250
|
+
sar.reason AS "scheduleReason"
|
|
251
|
+
FROM operations_approval a
|
|
252
|
+
JOIN operations_collaborator requester
|
|
253
|
+
ON requester.id = a.requester_collaborator_id
|
|
254
|
+
LEFT JOIN operations_collaborator approver
|
|
255
|
+
ON approver.id = a.approver_collaborator_id
|
|
256
|
+
LEFT JOIN operations_timesheet t
|
|
257
|
+
ON a.target_type = 'timesheet'
|
|
258
|
+
AND t.id = a.target_id
|
|
259
|
+
LEFT JOIN operations_timesheet_entry te
|
|
260
|
+
ON te.timesheet_id = t.id
|
|
261
|
+
AND te.deleted_at IS NULL
|
|
262
|
+
LEFT JOIN operations_project_assignment pa
|
|
263
|
+
ON pa.id = te.project_assignment_id
|
|
264
|
+
LEFT JOIN operations_project p
|
|
265
|
+
ON p.id = pa.project_id
|
|
266
|
+
LEFT JOIN operations_time_off_request tor
|
|
267
|
+
ON a.target_type = 'time_off_request'
|
|
268
|
+
AND tor.id = a.target_id
|
|
269
|
+
LEFT JOIN operations_schedule_adjustment_request sar
|
|
270
|
+
ON a.target_type = 'schedule_adjustment_request'
|
|
271
|
+
AND sar.id = a.target_id
|
|
272
|
+
WHERE a.deleted_at IS NULL
|
|
273
|
+
AND a.status = 'pending'
|
|
274
|
+
AND a.approver_collaborator_id = $1
|
|
275
|
+
GROUP BY a.id, requester.id, approver.id, t.id, tor.id, sar.id
|
|
276
|
+
ORDER BY a.submitted_at DESC, a.id DESC`, [actor.collaboratorId]),
|
|
277
|
+
this.queryRows(`SELECT tor.id,
|
|
278
|
+
tor.collaborator_id AS "collaboratorId",
|
|
279
|
+
c.display_name AS "collaboratorName",
|
|
280
|
+
tor.request_type AS "requestType",
|
|
281
|
+
tor.start_date AS "startDate",
|
|
282
|
+
tor.end_date AS "endDate",
|
|
283
|
+
tor.total_days AS "totalDays",
|
|
284
|
+
tor.status,
|
|
285
|
+
tor.reason,
|
|
286
|
+
tor.submitted_at AS "submittedAt",
|
|
287
|
+
tor.reviewed_at AS "reviewedAt",
|
|
288
|
+
approval.decision_note AS "approverNote"
|
|
289
|
+
FROM operations_time_off_request tor
|
|
290
|
+
JOIN operations_collaborator c ON c.id = tor.collaborator_id
|
|
291
|
+
LEFT JOIN operations_approval approval
|
|
292
|
+
ON approval.target_type = 'time_off_request'
|
|
293
|
+
AND approval.target_id = tor.id
|
|
294
|
+
AND approval.deleted_at IS NULL
|
|
295
|
+
WHERE tor.deleted_at IS NULL
|
|
296
|
+
AND tor.status = 'submitted'
|
|
297
|
+
AND tor.collaborator_id = ANY($1::int[])
|
|
298
|
+
ORDER BY tor.start_date ASC, tor.id ASC`, [actor.teamCollaboratorIds]),
|
|
299
|
+
this.queryRows(`SELECT sar.id,
|
|
300
|
+
sar.collaborator_id AS "collaboratorId",
|
|
301
|
+
c.display_name AS "collaboratorName",
|
|
302
|
+
sar.request_scope AS "requestScope",
|
|
303
|
+
sar.effective_start_date AS "effectiveStartDate",
|
|
304
|
+
sar.effective_end_date AS "effectiveEndDate",
|
|
305
|
+
sar.status,
|
|
306
|
+
sar.reason,
|
|
307
|
+
sar.submitted_at AS "submittedAt",
|
|
308
|
+
sar.reviewed_at AS "reviewedAt",
|
|
309
|
+
approval.decision_note AS "approverNote"
|
|
310
|
+
FROM operations_schedule_adjustment_request sar
|
|
311
|
+
JOIN operations_collaborator c ON c.id = sar.collaborator_id
|
|
312
|
+
LEFT JOIN operations_approval approval
|
|
313
|
+
ON approval.target_type = 'schedule_adjustment_request'
|
|
314
|
+
AND approval.target_id = sar.id
|
|
315
|
+
AND approval.deleted_at IS NULL
|
|
316
|
+
WHERE sar.deleted_at IS NULL
|
|
317
|
+
AND sar.status = 'submitted'
|
|
318
|
+
AND sar.collaborator_id = ANY($1::int[])
|
|
319
|
+
ORDER BY sar.effective_start_date ASC, sar.id ASC`, [actor.teamCollaboratorIds]),
|
|
320
|
+
]);
|
|
321
|
+
const pendingItemCounts = {
|
|
322
|
+
timesheets: pendingApprovalQueue.filter((item) => item.targetType === 'timesheet')
|
|
323
|
+
.length,
|
|
324
|
+
timeOffRequests: pendingTimeOffRequests.length,
|
|
325
|
+
scheduleAdjustmentRequests: pendingScheduleAdjustmentRequests.length,
|
|
326
|
+
};
|
|
327
|
+
return {
|
|
328
|
+
teamMembers,
|
|
329
|
+
projectCount: (await this.getAssignedProjectIds(actor.teamCollaboratorIds))
|
|
330
|
+
.length,
|
|
331
|
+
pendingApprovals: Number((_b = (_a = (await this.querySingle(`SELECT COUNT(*)::text AS total
|
|
332
|
+
FROM operations_approval
|
|
333
|
+
WHERE deleted_at IS NULL
|
|
334
|
+
AND status = 'pending'
|
|
335
|
+
AND approver_collaborator_id = $1`, [actor.collaboratorId]))) === null || _a === void 0 ? void 0 : _a.total) !== null && _b !== void 0 ? _b : 0),
|
|
336
|
+
pendingItems: pendingItemCounts,
|
|
337
|
+
teamProjects,
|
|
338
|
+
pendingApprovalQueue,
|
|
339
|
+
pendingTimeOffRequests,
|
|
340
|
+
pendingScheduleAdjustmentRequests,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
async createCollaborator(userId, data) {
|
|
344
|
+
const actor = await this.getActorContext(userId);
|
|
345
|
+
this.ensureDirector(actor);
|
|
346
|
+
this.requireFields(data, ['userId', 'code', 'displayName']);
|
|
347
|
+
const collaboratorId = await this.prisma.$transaction(async (tx) => {
|
|
348
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
|
|
349
|
+
const created = (await tx.$queryRawUnsafe(`INSERT INTO operations_collaborator (
|
|
350
|
+
user_id,
|
|
351
|
+
supervisor_collaborator_id,
|
|
352
|
+
code,
|
|
353
|
+
collaborator_type,
|
|
354
|
+
display_name,
|
|
355
|
+
department,
|
|
356
|
+
title,
|
|
357
|
+
level_label,
|
|
358
|
+
weekly_capacity_hours,
|
|
359
|
+
status,
|
|
360
|
+
joined_at,
|
|
361
|
+
left_at,
|
|
362
|
+
notes,
|
|
363
|
+
created_at,
|
|
364
|
+
updated_at
|
|
365
|
+
) VALUES (
|
|
366
|
+
$1, $2, $3, COALESCE($4, 'other'), $5, $6, $7, $8, $9,
|
|
367
|
+
COALESCE($10, 'active'), $11, $12, $13, NOW(), NOW()
|
|
368
|
+
)
|
|
369
|
+
RETURNING id`, data.userId, (_a = data.supervisorCollaboratorId) !== null && _a !== void 0 ? _a : null, data.code, (_b = data.collaboratorType) !== null && _b !== void 0 ? _b : 'other', data.displayName, (_c = data.department) !== null && _c !== void 0 ? _c : null, (_d = data.title) !== null && _d !== void 0 ? _d : null, (_e = data.levelLabel) !== null && _e !== void 0 ? _e : null, (_f = data.weeklyCapacityHours) !== null && _f !== void 0 ? _f : null, (_g = data.status) !== null && _g !== void 0 ? _g : 'active', (_h = data.joinedAt) !== null && _h !== void 0 ? _h : null, (_j = data.leftAt) !== null && _j !== void 0 ? _j : null, (_k = data.notes) !== null && _k !== void 0 ? _k : null));
|
|
370
|
+
const createdCollaboratorId = (_l = created[0]) === null || _l === void 0 ? void 0 : _l.id;
|
|
371
|
+
await this.replaceCollaboratorScheduleDays(tx, createdCollaboratorId, data.weeklySchedule);
|
|
372
|
+
if (data.autoGenerateContractDraft !== false) {
|
|
373
|
+
await this.createHiringContractDraft(tx, actor.userId, {
|
|
374
|
+
collaboratorId: createdCollaboratorId,
|
|
375
|
+
collaboratorCode: data.code,
|
|
376
|
+
displayName: data.displayName,
|
|
377
|
+
collaboratorType: (_m = data.collaboratorType) !== null && _m !== void 0 ? _m : 'other',
|
|
378
|
+
supervisorCollaboratorId: (_o = data.supervisorCollaboratorId) !== null && _o !== void 0 ? _o : null,
|
|
379
|
+
startDate: (_p = data.joinedAt) !== null && _p !== void 0 ? _p : null,
|
|
380
|
+
weeklyCapacityHours: (_q = data.weeklyCapacityHours) !== null && _q !== void 0 ? _q : null,
|
|
381
|
+
compensationAmount: (_r = data.compensationAmount) !== null && _r !== void 0 ? _r : null,
|
|
382
|
+
description: (_t = (_s = data.contractDescription) !== null && _s !== void 0 ? _s : data.notes) !== null && _t !== void 0 ? _t : null,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
return createdCollaboratorId;
|
|
386
|
+
});
|
|
387
|
+
return this.getCollaboratorByIdForUser(userId, collaboratorId);
|
|
388
|
+
}
|
|
389
|
+
async updateCollaborator(userId, collaboratorId, data) {
|
|
390
|
+
const actor = await this.getActorContext(userId);
|
|
391
|
+
this.ensureDirector(actor);
|
|
392
|
+
await this.getCollaboratorById(collaboratorId);
|
|
393
|
+
const updates = [];
|
|
394
|
+
const params = [];
|
|
395
|
+
this.pushUpdate(updates, params, 'supervisor_collaborator_id', data.supervisorCollaboratorId);
|
|
396
|
+
this.pushUpdate(updates, params, 'code', data.code);
|
|
397
|
+
this.pushUpdate(updates, params, 'collaborator_type', data.collaboratorType);
|
|
398
|
+
this.pushUpdate(updates, params, 'display_name', data.displayName);
|
|
399
|
+
this.pushUpdate(updates, params, 'department', data.department);
|
|
400
|
+
this.pushUpdate(updates, params, 'title', data.title);
|
|
401
|
+
this.pushUpdate(updates, params, 'level_label', data.levelLabel);
|
|
402
|
+
this.pushUpdate(updates, params, 'weekly_capacity_hours', data.weeklyCapacityHours);
|
|
403
|
+
this.pushUpdate(updates, params, 'status', data.status);
|
|
404
|
+
this.pushUpdate(updates, params, 'joined_at', data.joinedAt);
|
|
405
|
+
this.pushUpdate(updates, params, 'left_at', data.leftAt);
|
|
406
|
+
this.pushUpdate(updates, params, 'notes', data.notes);
|
|
407
|
+
await this.prisma.$transaction(async (tx) => {
|
|
408
|
+
if (updates.length) {
|
|
409
|
+
params.push(collaboratorId);
|
|
410
|
+
await tx.$executeRawUnsafe(`UPDATE operations_collaborator
|
|
411
|
+
SET ${updates.join(', ')},
|
|
412
|
+
updated_at = NOW()
|
|
413
|
+
WHERE id = $${params.length}`, ...params);
|
|
414
|
+
}
|
|
415
|
+
if (data.weeklySchedule) {
|
|
416
|
+
await this.replaceCollaboratorScheduleDays(tx, collaboratorId, data.weeklySchedule);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
return this.getCollaboratorByIdForUser(userId, collaboratorId);
|
|
420
|
+
}
|
|
421
|
+
async listProjects(userId) {
|
|
422
|
+
const actor = await this.getActorContext(userId);
|
|
423
|
+
const filter = this.buildIdFilter(actor.visibleProjectIds, 'p.id', actor.isDirector);
|
|
424
|
+
const assignmentParams = [];
|
|
425
|
+
const ownAssignmentSelect = actor.collaboratorId
|
|
426
|
+
? `MAX(CASE WHEN pa.collaborator_id = ${this.param(assignmentParams, actor.collaboratorId)} THEN pa.id END)::int AS "myAssignmentId",
|
|
427
|
+
MAX(CASE WHEN pa.collaborator_id = ${this.param(assignmentParams, actor.collaboratorId)} THEN pa.role_label END) AS "myRoleLabel",`
|
|
428
|
+
: `NULL::int AS "myAssignmentId",
|
|
429
|
+
NULL::varchar AS "myRoleLabel",`;
|
|
430
|
+
return this.queryRows(`SELECT p.id,
|
|
431
|
+
p.contract_id AS "contractId",
|
|
432
|
+
p.manager_collaborator_id AS "managerCollaboratorId",
|
|
433
|
+
p.code,
|
|
434
|
+
p.name,
|
|
435
|
+
p.client_name AS "clientName",
|
|
436
|
+
p.summary,
|
|
437
|
+
p.status,
|
|
438
|
+
p.progress_percent AS "progressPercent",
|
|
439
|
+
p.delivery_model AS "deliveryModel",
|
|
440
|
+
p.budget_amount AS "budgetAmount",
|
|
441
|
+
p.start_date AS "startDate",
|
|
442
|
+
p.end_date AS "endDate",
|
|
443
|
+
c.name AS "contractName",
|
|
444
|
+
c.status AS "contractStatus",
|
|
445
|
+
m.display_name AS "managerName",
|
|
446
|
+
${ownAssignmentSelect}
|
|
447
|
+
COUNT(DISTINCT pa.id)::int AS "teamSize"
|
|
448
|
+
FROM operations_project p
|
|
449
|
+
LEFT JOIN operations_contract c ON c.id = p.contract_id
|
|
450
|
+
LEFT JOIN operations_collaborator m ON m.id = p.manager_collaborator_id
|
|
451
|
+
LEFT JOIN operations_project_assignment pa
|
|
452
|
+
ON pa.project_id = p.id
|
|
453
|
+
AND pa.deleted_at IS NULL
|
|
454
|
+
AND pa.status IN ('planned', 'active')
|
|
455
|
+
WHERE p.deleted_at IS NULL AND ${filter.clause}
|
|
456
|
+
GROUP BY p.id, c.id, m.id
|
|
457
|
+
ORDER BY p.name ASC`, [...assignmentParams, ...filter.params]);
|
|
458
|
+
}
|
|
459
|
+
async getProjectById(userId, projectId) {
|
|
460
|
+
const actor = await this.getActorContext(userId);
|
|
461
|
+
await this.assertProjectAccess(actor, projectId);
|
|
462
|
+
return this.getProjectDetails(projectId, actor.collaboratorId);
|
|
463
|
+
}
|
|
464
|
+
async createProject(userId, data) {
|
|
465
|
+
const actor = await this.getActorContext(userId);
|
|
466
|
+
this.ensureDirector(actor);
|
|
467
|
+
this.requireFields(data, ['code', 'name']);
|
|
468
|
+
const createdProjectId = await this.prisma.$transaction(async (tx) => {
|
|
469
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
|
|
470
|
+
const created = await tx.$queryRawUnsafe(`INSERT INTO operations_project (
|
|
471
|
+
contract_id,
|
|
472
|
+
manager_collaborator_id,
|
|
473
|
+
code,
|
|
474
|
+
name,
|
|
475
|
+
client_name,
|
|
476
|
+
summary,
|
|
477
|
+
status,
|
|
478
|
+
progress_percent,
|
|
479
|
+
delivery_model,
|
|
480
|
+
budget_amount,
|
|
481
|
+
start_date,
|
|
482
|
+
end_date,
|
|
483
|
+
created_at,
|
|
484
|
+
updated_at
|
|
485
|
+
) VALUES (
|
|
486
|
+
$1, $2, $3, $4, $5, $6, COALESCE($7, 'planning'), $8,
|
|
487
|
+
COALESCE($9, 'project_delivery'), $10, $11, $12, NOW(), NOW()
|
|
488
|
+
)
|
|
489
|
+
RETURNING id`, (_a = data.contractId) !== null && _a !== void 0 ? _a : null, (_b = data.managerCollaboratorId) !== null && _b !== void 0 ? _b : null, data.code, data.name, (_c = data.clientName) !== null && _c !== void 0 ? _c : null, (_d = data.summary) !== null && _d !== void 0 ? _d : null, (_e = data.status) !== null && _e !== void 0 ? _e : 'planning', (_f = data.progressPercent) !== null && _f !== void 0 ? _f : null, (_g = data.deliveryModel) !== null && _g !== void 0 ? _g : 'project_delivery', (_h = data.budgetAmount) !== null && _h !== void 0 ? _h : null, (_j = data.startDate) !== null && _j !== void 0 ? _j : null, (_k = data.endDate) !== null && _k !== void 0 ? _k : null);
|
|
490
|
+
const projectId = (_l = created[0]) === null || _l === void 0 ? void 0 : _l.id;
|
|
491
|
+
if ((_m = data.teamAssignments) === null || _m === void 0 ? void 0 : _m.length) {
|
|
492
|
+
await this.replaceProjectAssignments(tx, projectId, data.teamAssignments);
|
|
493
|
+
}
|
|
494
|
+
if (!data.contractId && data.autoGenerateContractDraft !== false) {
|
|
495
|
+
const contractId = await this.createProjectContractDraft(tx, actor.userId, {
|
|
496
|
+
projectId,
|
|
497
|
+
projectCode: data.code,
|
|
498
|
+
projectName: data.name,
|
|
499
|
+
clientName: (_o = data.clientName) !== null && _o !== void 0 ? _o : data.name,
|
|
500
|
+
managerCollaboratorId: (_p = data.managerCollaboratorId) !== null && _p !== void 0 ? _p : null,
|
|
501
|
+
startDate: (_q = data.startDate) !== null && _q !== void 0 ? _q : null,
|
|
502
|
+
endDate: (_r = data.endDate) !== null && _r !== void 0 ? _r : null,
|
|
503
|
+
budgetAmount: (_s = data.budgetAmount) !== null && _s !== void 0 ? _s : null,
|
|
504
|
+
monthlyHourCap: (_t = data.monthlyHourCap) !== null && _t !== void 0 ? _t : null,
|
|
505
|
+
billingModel: (_u = data.billingModel) !== null && _u !== void 0 ? _u : 'time_and_material',
|
|
506
|
+
contractCode: (_v = data.contractCode) !== null && _v !== void 0 ? _v : null,
|
|
507
|
+
contractName: (_w = data.contractName) !== null && _w !== void 0 ? _w : null,
|
|
508
|
+
description: (_y = (_x = data.contractDescription) !== null && _x !== void 0 ? _x : data.summary) !== null && _y !== void 0 ? _y : null,
|
|
509
|
+
});
|
|
510
|
+
await tx.$executeRawUnsafe(`UPDATE operations_project
|
|
511
|
+
SET contract_id = $1,
|
|
512
|
+
updated_at = NOW()
|
|
513
|
+
WHERE id = $2`, contractId, projectId);
|
|
514
|
+
}
|
|
515
|
+
return projectId;
|
|
516
|
+
});
|
|
517
|
+
return this.getProjectById(userId, createdProjectId);
|
|
518
|
+
}
|
|
519
|
+
async updateProject(userId, projectId, data) {
|
|
520
|
+
const actor = await this.getActorContext(userId);
|
|
521
|
+
this.ensureDirector(actor);
|
|
522
|
+
await this.getProjectById(userId, projectId);
|
|
523
|
+
const updates = [];
|
|
524
|
+
const params = [];
|
|
525
|
+
this.pushUpdate(updates, params, 'contract_id', data.contractId);
|
|
526
|
+
this.pushUpdate(updates, params, 'manager_collaborator_id', data.managerCollaboratorId);
|
|
527
|
+
this.pushUpdate(updates, params, 'code', data.code);
|
|
528
|
+
this.pushUpdate(updates, params, 'name', data.name);
|
|
529
|
+
this.pushUpdate(updates, params, 'client_name', data.clientName);
|
|
530
|
+
this.pushUpdate(updates, params, 'summary', data.summary);
|
|
531
|
+
this.pushUpdate(updates, params, 'status', data.status);
|
|
532
|
+
this.pushUpdate(updates, params, 'progress_percent', data.progressPercent);
|
|
533
|
+
this.pushUpdate(updates, params, 'delivery_model', data.deliveryModel);
|
|
534
|
+
this.pushUpdate(updates, params, 'budget_amount', data.budgetAmount);
|
|
535
|
+
this.pushUpdate(updates, params, 'start_date', data.startDate);
|
|
536
|
+
this.pushUpdate(updates, params, 'end_date', data.endDate);
|
|
537
|
+
await this.prisma.$transaction(async (tx) => {
|
|
538
|
+
if (updates.length) {
|
|
539
|
+
params.push(projectId);
|
|
540
|
+
await tx.$executeRawUnsafe(`UPDATE operations_project
|
|
541
|
+
SET ${updates.join(', ')},
|
|
542
|
+
updated_at = NOW()
|
|
543
|
+
WHERE id = $${params.length}`, ...params);
|
|
544
|
+
}
|
|
545
|
+
if (data.teamAssignments) {
|
|
546
|
+
await this.replaceProjectAssignments(tx, projectId, data.teamAssignments);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
return this.getProjectById(userId, projectId);
|
|
550
|
+
}
|
|
551
|
+
async listContracts(userId) {
|
|
552
|
+
const actor = await this.getActorContext(userId);
|
|
553
|
+
const params = [];
|
|
554
|
+
const accessClause = actor.isDirector
|
|
555
|
+
? 'c.deleted_at IS NULL'
|
|
556
|
+
: `c.deleted_at IS NULL AND (
|
|
557
|
+
c.related_collaborator_id = ANY(${this.param(params, actor.visibleCollaboratorIds)}::int[])
|
|
558
|
+
OR EXISTS (
|
|
559
|
+
SELECT 1
|
|
560
|
+
FROM operations_project p_access
|
|
561
|
+
WHERE p_access.contract_id = c.id
|
|
562
|
+
AND p_access.deleted_at IS NULL
|
|
563
|
+
AND p_access.id = ANY(${this.param(params, actor.visibleProjectIds)}::int[])
|
|
564
|
+
)
|
|
565
|
+
)`;
|
|
566
|
+
return this.queryRows(`SELECT c.id,
|
|
567
|
+
c.code,
|
|
568
|
+
c.name,
|
|
569
|
+
c.contract_category AS "contractCategory",
|
|
570
|
+
c.contract_type AS "contractType",
|
|
571
|
+
c.client_name AS "clientName",
|
|
572
|
+
c.signature_status AS "signatureStatus",
|
|
573
|
+
c.is_active AS "isActive",
|
|
574
|
+
c.billing_model AS "billingModel",
|
|
575
|
+
c.account_manager_collaborator_id AS "accountManagerCollaboratorId",
|
|
576
|
+
c.related_collaborator_id AS "relatedCollaboratorId",
|
|
577
|
+
c.origin_type AS "originType",
|
|
578
|
+
c.origin_id AS "originId",
|
|
579
|
+
c.start_date AS "startDate",
|
|
580
|
+
c.end_date AS "endDate",
|
|
581
|
+
c.signed_at AS "signedAt",
|
|
582
|
+
c.effective_date AS "effectiveDate",
|
|
583
|
+
c.budget_amount AS "budgetAmount",
|
|
584
|
+
c.monthly_hour_cap AS "monthlyHourCap",
|
|
585
|
+
c.status,
|
|
586
|
+
c.description,
|
|
587
|
+
m.display_name AS "accountManagerName",
|
|
588
|
+
linked.display_name AS "relatedCollaboratorName",
|
|
589
|
+
COALESCE(primary_party.display_name, linked.display_name, c.client_name) AS "mainRelatedPartyName",
|
|
590
|
+
COALESCE(financials.value_amount, 0) AS "valueAmount",
|
|
591
|
+
COALESCE(financials.payment_amount, 0) AS "paymentAmount",
|
|
592
|
+
COALESCE(financials.revenue_amount, 0) AS "revenueAmount",
|
|
593
|
+
COALESCE(financials.fine_amount, 0) AS "fineAmount",
|
|
594
|
+
COALESCE(pdf_document.file_name, '') AS "currentPdfFileName",
|
|
595
|
+
COUNT(DISTINCT p.id)::int AS "projectCount"
|
|
596
|
+
FROM operations_contract c
|
|
597
|
+
LEFT JOIN operations_collaborator m ON m.id = c.account_manager_collaborator_id
|
|
598
|
+
LEFT JOIN operations_collaborator linked ON linked.id = c.related_collaborator_id
|
|
599
|
+
LEFT JOIN LATERAL (
|
|
600
|
+
SELECT cp.display_name
|
|
601
|
+
FROM operations_contract_party cp
|
|
602
|
+
WHERE cp.contract_id = c.id
|
|
603
|
+
AND cp.deleted_at IS NULL
|
|
604
|
+
ORDER BY cp.is_primary DESC, cp.id ASC
|
|
605
|
+
LIMIT 1
|
|
606
|
+
) primary_party ON TRUE
|
|
607
|
+
LEFT JOIN LATERAL (
|
|
608
|
+
SELECT
|
|
609
|
+
SUM(CASE WHEN term_type = 'value' THEN amount ELSE 0 END) AS value_amount,
|
|
610
|
+
SUM(CASE WHEN term_type = 'payment' THEN amount ELSE 0 END) AS payment_amount,
|
|
611
|
+
SUM(CASE WHEN term_type = 'revenue' THEN amount ELSE 0 END) AS revenue_amount,
|
|
612
|
+
SUM(CASE WHEN term_type = 'fine' THEN amount ELSE 0 END) AS fine_amount
|
|
613
|
+
FROM operations_contract_financial_term ft
|
|
614
|
+
WHERE ft.contract_id = c.id
|
|
615
|
+
AND ft.deleted_at IS NULL
|
|
616
|
+
) financials ON TRUE
|
|
617
|
+
LEFT JOIN LATERAL (
|
|
618
|
+
SELECT cd.file_name
|
|
619
|
+
FROM operations_contract_document cd
|
|
620
|
+
WHERE cd.contract_id = c.id
|
|
621
|
+
AND cd.deleted_at IS NULL
|
|
622
|
+
AND cd.is_current = true
|
|
623
|
+
AND cd.document_type IN ('uploaded_pdf', 'generated_pdf')
|
|
624
|
+
ORDER BY cd.id DESC
|
|
625
|
+
LIMIT 1
|
|
626
|
+
) pdf_document ON TRUE
|
|
627
|
+
LEFT JOIN operations_project p
|
|
628
|
+
ON p.contract_id = c.id
|
|
629
|
+
AND p.deleted_at IS NULL
|
|
630
|
+
WHERE ${accessClause}
|
|
631
|
+
GROUP BY c.id, m.id, linked.id
|
|
632
|
+
ORDER BY c.name ASC`, params);
|
|
633
|
+
}
|
|
634
|
+
async getContractById(userId, contractId) {
|
|
635
|
+
var _a, _b, _c;
|
|
636
|
+
const actor = await this.getActorContext(userId);
|
|
637
|
+
const contract = await this.querySingle(`SELECT c.id,
|
|
638
|
+
c.code,
|
|
639
|
+
c.name,
|
|
640
|
+
c.contract_category AS "contractCategory",
|
|
641
|
+
c.contract_type AS "contractType",
|
|
642
|
+
c.client_name AS "clientName",
|
|
643
|
+
c.signature_status AS "signatureStatus",
|
|
644
|
+
c.is_active AS "isActive",
|
|
645
|
+
c.billing_model AS "billingModel",
|
|
646
|
+
c.account_manager_collaborator_id AS "accountManagerCollaboratorId",
|
|
647
|
+
c.related_collaborator_id AS "relatedCollaboratorId",
|
|
648
|
+
c.origin_type AS "originType",
|
|
649
|
+
c.origin_id AS "originId",
|
|
650
|
+
c.start_date AS "startDate",
|
|
651
|
+
c.end_date AS "endDate",
|
|
652
|
+
c.signed_at AS "signedAt",
|
|
653
|
+
c.effective_date AS "effectiveDate",
|
|
654
|
+
c.budget_amount AS "budgetAmount",
|
|
655
|
+
c.monthly_hour_cap AS "monthlyHourCap",
|
|
656
|
+
c.status,
|
|
657
|
+
c.description,
|
|
658
|
+
c.content_html AS "contentHtml",
|
|
659
|
+
m.display_name AS "accountManagerName",
|
|
660
|
+
linked.display_name AS "relatedCollaboratorName"
|
|
661
|
+
FROM operations_contract c
|
|
662
|
+
LEFT JOIN operations_collaborator m ON m.id = c.account_manager_collaborator_id
|
|
663
|
+
LEFT JOIN operations_collaborator linked ON linked.id = c.related_collaborator_id
|
|
664
|
+
WHERE c.id = $1
|
|
665
|
+
AND c.deleted_at IS NULL`, [contractId]);
|
|
666
|
+
if (!contract) {
|
|
667
|
+
throw new common_1.NotFoundException('Contract not found.');
|
|
668
|
+
}
|
|
669
|
+
if (!actor.isDirector) {
|
|
670
|
+
const access = await this.querySingle(`SELECT EXISTS (
|
|
671
|
+
SELECT 1
|
|
672
|
+
FROM operations_contract c
|
|
673
|
+
WHERE c.id = $1
|
|
674
|
+
AND c.deleted_at IS NULL
|
|
675
|
+
AND (
|
|
676
|
+
c.related_collaborator_id = ANY($2::int[])
|
|
677
|
+
OR EXISTS (
|
|
678
|
+
SELECT 1
|
|
679
|
+
FROM operations_project p
|
|
680
|
+
WHERE p.contract_id = c.id
|
|
681
|
+
AND p.deleted_at IS NULL
|
|
682
|
+
AND p.id = ANY($3::int[])
|
|
683
|
+
)
|
|
684
|
+
)
|
|
685
|
+
) AS exists`, [contractId, actor.visibleCollaboratorIds, actor.visibleProjectIds]);
|
|
686
|
+
if (!(access === null || access === void 0 ? void 0 : access.exists)) {
|
|
687
|
+
throw new common_1.ForbiddenException('You do not have access to this contract.');
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
const [projects, scheduleSummary, parties, signatures, financialTerms, documents, revisions, history] = await Promise.all([
|
|
691
|
+
this.queryRows(`SELECT id, code, name, status
|
|
692
|
+
FROM operations_project
|
|
693
|
+
WHERE contract_id = $1
|
|
694
|
+
AND deleted_at IS NULL
|
|
695
|
+
ORDER BY name ASC`, [contractId]),
|
|
696
|
+
contract.relatedCollaboratorId
|
|
697
|
+
? this.queryRows(`SELECT weekday,
|
|
698
|
+
is_working_day AS "isWorkingDay",
|
|
699
|
+
start_time AS "startTime",
|
|
700
|
+
end_time AS "endTime",
|
|
701
|
+
break_minutes AS "breakMinutes"
|
|
702
|
+
FROM operations_collaborator_schedule_day
|
|
703
|
+
WHERE collaborator_id = $1
|
|
704
|
+
AND deleted_at IS NULL
|
|
705
|
+
ORDER BY id ASC`, [contract.relatedCollaboratorId])
|
|
706
|
+
: Promise.resolve([]),
|
|
707
|
+
this.queryRows(`SELECT id,
|
|
708
|
+
party_role AS "partyRole",
|
|
709
|
+
party_type AS "partyType",
|
|
710
|
+
display_name AS "displayName",
|
|
711
|
+
document_number AS "documentNumber",
|
|
712
|
+
email,
|
|
713
|
+
phone,
|
|
714
|
+
is_primary AS "isPrimary"
|
|
715
|
+
FROM operations_contract_party
|
|
716
|
+
WHERE contract_id = $1
|
|
717
|
+
AND deleted_at IS NULL
|
|
718
|
+
ORDER BY is_primary DESC, id ASC`, [contractId]),
|
|
719
|
+
this.queryRows(`SELECT id,
|
|
720
|
+
signer_name AS "signerName",
|
|
721
|
+
signer_role AS "signerRole",
|
|
722
|
+
signer_email AS "signerEmail",
|
|
723
|
+
signer_status AS status,
|
|
724
|
+
signed_at AS "signedAt"
|
|
725
|
+
FROM operations_contract_signature
|
|
726
|
+
WHERE contract_id = $1
|
|
727
|
+
AND deleted_at IS NULL
|
|
728
|
+
ORDER BY id ASC`, [contractId]),
|
|
729
|
+
this.queryRows(`SELECT id,
|
|
730
|
+
term_type AS "termType",
|
|
731
|
+
label,
|
|
732
|
+
amount,
|
|
733
|
+
recurrence,
|
|
734
|
+
due_day AS "dueDay",
|
|
735
|
+
notes
|
|
736
|
+
FROM operations_contract_financial_term
|
|
737
|
+
WHERE contract_id = $1
|
|
738
|
+
AND deleted_at IS NULL
|
|
739
|
+
ORDER BY id ASC`, [contractId]),
|
|
740
|
+
this.queryRows(`SELECT id,
|
|
741
|
+
document_type AS "documentType",
|
|
742
|
+
file_name AS "fileName",
|
|
743
|
+
mime_type AS "mimeType",
|
|
744
|
+
file_content_base64 AS "fileContentBase64",
|
|
745
|
+
is_current AS "isCurrent",
|
|
746
|
+
notes,
|
|
747
|
+
created_at AS "createdAt"
|
|
748
|
+
FROM operations_contract_document
|
|
749
|
+
WHERE contract_id = $1
|
|
750
|
+
AND deleted_at IS NULL
|
|
751
|
+
ORDER BY is_current DESC, id DESC`, [contractId]),
|
|
752
|
+
this.queryRows(`SELECT id,
|
|
753
|
+
revision_type AS "revisionType",
|
|
754
|
+
title,
|
|
755
|
+
effective_date AS "effectiveDate",
|
|
756
|
+
status,
|
|
757
|
+
summary
|
|
758
|
+
FROM operations_contract_revision
|
|
759
|
+
WHERE contract_id = $1
|
|
760
|
+
AND deleted_at IS NULL
|
|
761
|
+
ORDER BY effective_date DESC NULLS LAST, id DESC`, [contractId]),
|
|
762
|
+
this.queryRows(`SELECT id,
|
|
763
|
+
actor_user_id AS "actorUserId",
|
|
764
|
+
action,
|
|
765
|
+
note,
|
|
766
|
+
metadata_json AS "metadataJson",
|
|
767
|
+
created_at AS "createdAt"
|
|
768
|
+
FROM operations_contract_history
|
|
769
|
+
WHERE contract_id = $1
|
|
770
|
+
ORDER BY id DESC`, [contractId]),
|
|
771
|
+
]);
|
|
772
|
+
return Object.assign(Object.assign({}, contract), { mainRelatedPartyName: (_c = (_b = (_a = parties.find((party) => party.isPrimary)) === null || _a === void 0 ? void 0 : _a.displayName) !== null && _b !== void 0 ? _b : contract.relatedCollaboratorName) !== null && _c !== void 0 ? _c : contract.clientName, projects,
|
|
773
|
+
scheduleSummary,
|
|
774
|
+
parties,
|
|
775
|
+
signatures,
|
|
776
|
+
financialTerms,
|
|
777
|
+
documents,
|
|
778
|
+
revisions,
|
|
779
|
+
history });
|
|
780
|
+
}
|
|
781
|
+
async createContract(userId, data) {
|
|
782
|
+
const actor = await this.getActorContext(userId);
|
|
783
|
+
this.ensureDirector(actor);
|
|
784
|
+
this.requireFields(data, ['code', 'name', 'clientName', 'startDate']);
|
|
785
|
+
const createdId = await this.prisma.$transaction(async (tx) => {
|
|
786
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
|
|
787
|
+
const created = await tx.$queryRawUnsafe(`INSERT INTO operations_contract (
|
|
788
|
+
code,
|
|
789
|
+
name,
|
|
790
|
+
contract_category,
|
|
791
|
+
contract_type,
|
|
792
|
+
client_name,
|
|
793
|
+
signature_status,
|
|
794
|
+
is_active,
|
|
795
|
+
billing_model,
|
|
796
|
+
account_manager_collaborator_id,
|
|
797
|
+
related_collaborator_id,
|
|
798
|
+
origin_type,
|
|
799
|
+
origin_id,
|
|
800
|
+
start_date,
|
|
801
|
+
end_date,
|
|
802
|
+
signed_at,
|
|
803
|
+
effective_date,
|
|
804
|
+
budget_amount,
|
|
805
|
+
monthly_hour_cap,
|
|
806
|
+
status,
|
|
807
|
+
description,
|
|
808
|
+
content_html,
|
|
809
|
+
created_at,
|
|
810
|
+
updated_at
|
|
811
|
+
) VALUES (
|
|
812
|
+
$1, $2, COALESCE($3, 'client'), COALESCE($4, 'service_agreement'), $5, COALESCE($6, 'not_started'),
|
|
813
|
+
COALESCE($7, true), COALESCE($8, 'time_and_material'), $9, $10, COALESCE($11, 'manual'), $12, $13,
|
|
814
|
+
$14, $15, $16, $17, $18, COALESCE($19, 'draft'), $20, $21, NOW(), NOW()
|
|
815
|
+
)
|
|
816
|
+
RETURNING id`, data.code, data.name, (_a = data.contractCategory) !== null && _a !== void 0 ? _a : 'client', (_b = data.contractType) !== null && _b !== void 0 ? _b : 'service_agreement', data.clientName, (_c = data.signatureStatus) !== null && _c !== void 0 ? _c : 'not_started', (_d = data.isActive) !== null && _d !== void 0 ? _d : true, (_e = data.billingModel) !== null && _e !== void 0 ? _e : 'time_and_material', (_f = data.accountManagerCollaboratorId) !== null && _f !== void 0 ? _f : null, (_g = data.relatedCollaboratorId) !== null && _g !== void 0 ? _g : null, (_h = data.originType) !== null && _h !== void 0 ? _h : 'manual', (_j = data.originId) !== null && _j !== void 0 ? _j : null, data.startDate, (_k = data.endDate) !== null && _k !== void 0 ? _k : null, (_l = data.signedAt) !== null && _l !== void 0 ? _l : null, (_m = data.effectiveDate) !== null && _m !== void 0 ? _m : data.startDate, (_o = data.budgetAmount) !== null && _o !== void 0 ? _o : null, (_p = data.monthlyHourCap) !== null && _p !== void 0 ? _p : null, (_q = data.status) !== null && _q !== void 0 ? _q : 'draft', (_r = data.description) !== null && _r !== void 0 ? _r : null, (_s = data.contentHtml) !== null && _s !== void 0 ? _s : null);
|
|
817
|
+
const contractId = (_t = created[0]) === null || _t === void 0 ? void 0 : _t.id;
|
|
818
|
+
await this.replaceContractParties(tx, contractId, data.parties);
|
|
819
|
+
await this.replaceContractSignatures(tx, contractId, data.signatures);
|
|
820
|
+
await this.replaceContractFinancialTerms(tx, contractId, data.financialTerms);
|
|
821
|
+
await this.replaceContractRevisions(tx, contractId, data.revisions);
|
|
822
|
+
if (data.replaceUploadedPdfDocument) {
|
|
823
|
+
await this.replaceContractPdfDocument(tx, contractId, data.replaceUploadedPdfDocument);
|
|
824
|
+
}
|
|
825
|
+
await this.insertContractHistory(tx, contractId, userId, 'created', data.originType === 'manual'
|
|
826
|
+
? 'Manual contract created from registry.'
|
|
827
|
+
: `Contract registered from origin ${data.originType}.`);
|
|
828
|
+
return contractId;
|
|
829
|
+
});
|
|
830
|
+
return this.getContractById(userId, createdId);
|
|
831
|
+
}
|
|
832
|
+
async updateContract(userId, contractId, data) {
|
|
833
|
+
const actor = await this.getActorContext(userId);
|
|
834
|
+
this.ensureDirector(actor);
|
|
835
|
+
await this.getContractById(userId, contractId);
|
|
836
|
+
const updates = [];
|
|
837
|
+
const params = [];
|
|
838
|
+
this.pushUpdate(updates, params, 'code', data.code);
|
|
839
|
+
this.pushUpdate(updates, params, 'name', data.name);
|
|
840
|
+
this.pushUpdate(updates, params, 'contract_category', data.contractCategory);
|
|
841
|
+
this.pushUpdate(updates, params, 'contract_type', data.contractType);
|
|
842
|
+
this.pushUpdate(updates, params, 'client_name', data.clientName);
|
|
843
|
+
this.pushUpdate(updates, params, 'signature_status', data.signatureStatus);
|
|
844
|
+
this.pushUpdate(updates, params, 'is_active', data.isActive);
|
|
845
|
+
this.pushUpdate(updates, params, 'billing_model', data.billingModel);
|
|
846
|
+
this.pushUpdate(updates, params, 'account_manager_collaborator_id', data.accountManagerCollaboratorId);
|
|
847
|
+
this.pushUpdate(updates, params, 'related_collaborator_id', data.relatedCollaboratorId);
|
|
848
|
+
this.pushUpdate(updates, params, 'origin_type', data.originType);
|
|
849
|
+
this.pushUpdate(updates, params, 'origin_id', data.originId);
|
|
850
|
+
this.pushUpdate(updates, params, 'start_date', data.startDate);
|
|
851
|
+
this.pushUpdate(updates, params, 'end_date', data.endDate);
|
|
852
|
+
this.pushUpdate(updates, params, 'signed_at', data.signedAt);
|
|
853
|
+
this.pushUpdate(updates, params, 'effective_date', data.effectiveDate);
|
|
854
|
+
this.pushUpdate(updates, params, 'budget_amount', data.budgetAmount);
|
|
855
|
+
this.pushUpdate(updates, params, 'monthly_hour_cap', data.monthlyHourCap);
|
|
856
|
+
this.pushUpdate(updates, params, 'status', data.status);
|
|
857
|
+
this.pushUpdate(updates, params, 'description', data.description);
|
|
858
|
+
this.pushUpdate(updates, params, 'content_html', data.contentHtml);
|
|
859
|
+
await this.prisma.$transaction(async (tx) => {
|
|
860
|
+
if (updates.length) {
|
|
861
|
+
params.push(contractId);
|
|
862
|
+
await tx.$executeRawUnsafe(`UPDATE operations_contract
|
|
863
|
+
SET ${updates.join(', ')},
|
|
864
|
+
updated_at = NOW()
|
|
865
|
+
WHERE id = $${params.length}`, ...params);
|
|
866
|
+
}
|
|
867
|
+
if (data.parties) {
|
|
868
|
+
await this.replaceContractParties(tx, contractId, data.parties);
|
|
869
|
+
}
|
|
870
|
+
if (data.signatures) {
|
|
871
|
+
await this.replaceContractSignatures(tx, contractId, data.signatures);
|
|
872
|
+
}
|
|
873
|
+
if (data.financialTerms) {
|
|
874
|
+
await this.replaceContractFinancialTerms(tx, contractId, data.financialTerms);
|
|
875
|
+
}
|
|
876
|
+
if (data.revisions) {
|
|
877
|
+
await this.replaceContractRevisions(tx, contractId, data.revisions);
|
|
878
|
+
}
|
|
879
|
+
if (data.replaceUploadedPdfDocument) {
|
|
880
|
+
await this.replaceContractPdfDocument(tx, contractId, data.replaceUploadedPdfDocument);
|
|
881
|
+
}
|
|
882
|
+
await this.insertContractHistory(tx, contractId, userId, 'updated', 'Contract registry data updated.');
|
|
883
|
+
});
|
|
884
|
+
return this.getContractById(userId, contractId);
|
|
885
|
+
}
|
|
886
|
+
async listTimesheets(userId) {
|
|
887
|
+
const actor = await this.getActorContext(userId);
|
|
888
|
+
const filter = this.buildIdFilter(actor.visibleCollaboratorIds, 't.collaborator_id', actor.isDirector);
|
|
889
|
+
const headers = await this.queryRows(`SELECT t.id,
|
|
890
|
+
t.collaborator_id AS "collaboratorId",
|
|
891
|
+
c.display_name AS "collaboratorName",
|
|
892
|
+
t.approver_collaborator_id AS "approverCollaboratorId",
|
|
893
|
+
a.display_name AS "approverName",
|
|
894
|
+
t.week_start_date AS "weekStartDate",
|
|
895
|
+
t.week_end_date AS "weekEndDate",
|
|
896
|
+
t.total_hours AS "totalHours",
|
|
897
|
+
t.status,
|
|
898
|
+
t.submitted_at AS "submittedAt",
|
|
899
|
+
t.reviewed_at AS "reviewedAt",
|
|
900
|
+
t.notes,
|
|
901
|
+
approval.decision_note AS "decisionNote"
|
|
902
|
+
FROM operations_timesheet t
|
|
903
|
+
JOIN operations_collaborator c ON c.id = t.collaborator_id
|
|
904
|
+
LEFT JOIN operations_collaborator a ON a.id = t.approver_collaborator_id
|
|
905
|
+
LEFT JOIN operations_approval approval
|
|
906
|
+
ON approval.target_type = 'timesheet'
|
|
907
|
+
AND approval.target_id = t.id
|
|
908
|
+
AND approval.deleted_at IS NULL
|
|
909
|
+
WHERE t.deleted_at IS NULL AND ${filter.clause}
|
|
910
|
+
ORDER BY t.week_start_date DESC, t.id DESC`, filter.params);
|
|
911
|
+
if (!headers.length) {
|
|
912
|
+
return headers;
|
|
913
|
+
}
|
|
914
|
+
const entries = await this.queryRows(`SELECT e.id,
|
|
915
|
+
e.timesheet_id AS "timesheetId",
|
|
916
|
+
e.project_assignment_id AS "projectAssignmentId",
|
|
917
|
+
pa.project_id AS "projectId",
|
|
918
|
+
p.name AS "projectName",
|
|
919
|
+
pa.role_label AS "roleLabel",
|
|
920
|
+
e.activity_label AS "activityLabel",
|
|
921
|
+
e.work_date AS "workDate",
|
|
922
|
+
e.hours,
|
|
923
|
+
e.description
|
|
924
|
+
FROM operations_timesheet_entry e
|
|
925
|
+
LEFT JOIN operations_project_assignment pa ON pa.id = e.project_assignment_id
|
|
926
|
+
LEFT JOIN operations_project p ON p.id = pa.project_id
|
|
927
|
+
WHERE e.deleted_at IS NULL
|
|
928
|
+
AND e.timesheet_id = ANY($1::int[])
|
|
929
|
+
ORDER BY e.work_date ASC, e.id ASC`, [headers.map((item) => item.id)]);
|
|
930
|
+
const grouped = this.groupBy(entries, 'timesheetId');
|
|
931
|
+
return headers.map((timesheet) => {
|
|
932
|
+
var _a;
|
|
933
|
+
return (Object.assign(Object.assign({}, timesheet), { entries: (_a = grouped[timesheet.id]) !== null && _a !== void 0 ? _a : [] }));
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
async createTimesheet(userId, data) {
|
|
937
|
+
const actor = await this.getActorContext(userId);
|
|
938
|
+
this.ensureCollaborator(actor);
|
|
939
|
+
this.requireFields(data, ['weekStartDate', 'weekEndDate']);
|
|
940
|
+
const collaboratorId = actor.isDirector && data.collaboratorId
|
|
941
|
+
? data.collaboratorId
|
|
942
|
+
: actor.collaboratorId;
|
|
943
|
+
if (!collaboratorId) {
|
|
944
|
+
throw new common_1.BadRequestException('Collaborator context is required.');
|
|
945
|
+
}
|
|
946
|
+
if (!actor.isDirector && collaboratorId !== actor.collaboratorId) {
|
|
947
|
+
throw new common_1.ForbiddenException('You can only create your own timesheets.');
|
|
948
|
+
}
|
|
949
|
+
const collaborator = await this.getCollaboratorById(collaboratorId);
|
|
950
|
+
const created = await this.prisma.$transaction(async (tx) => {
|
|
951
|
+
var _a, _b, _c, _d;
|
|
952
|
+
const row = (await tx.$queryRawUnsafe(`INSERT INTO operations_timesheet (
|
|
953
|
+
collaborator_id,
|
|
954
|
+
approver_collaborator_id,
|
|
955
|
+
week_start_date,
|
|
956
|
+
week_end_date,
|
|
957
|
+
notes,
|
|
958
|
+
status,
|
|
959
|
+
created_at,
|
|
960
|
+
updated_at
|
|
961
|
+
) VALUES ($1, $2, $3, $4, $5, 'draft', NOW(), NOW())
|
|
962
|
+
RETURNING id`, collaboratorId, (_a = collaborator.supervisorId) !== null && _a !== void 0 ? _a : null, data.weekStartDate, data.weekEndDate, (_b = data.notes) !== null && _b !== void 0 ? _b : null));
|
|
963
|
+
const timesheetId = (_c = row[0]) === null || _c === void 0 ? void 0 : _c.id;
|
|
964
|
+
await this.replaceTimesheetEntries(tx, timesheetId, (_d = data.entries) !== null && _d !== void 0 ? _d : [], collaboratorId);
|
|
965
|
+
await this.refreshTimesheetTotal(tx, timesheetId);
|
|
966
|
+
return timesheetId;
|
|
967
|
+
});
|
|
968
|
+
return this.listSingleTimesheet(actor, created);
|
|
969
|
+
}
|
|
970
|
+
async updateTimesheet(userId, timesheetId, data) {
|
|
971
|
+
const actor = await this.getActorContext(userId);
|
|
972
|
+
const current = await this.getTimesheetById(timesheetId);
|
|
973
|
+
if (!actor.isDirector && current.collaboratorId !== actor.collaboratorId) {
|
|
974
|
+
throw new common_1.ForbiddenException('You can only update your own timesheets.');
|
|
975
|
+
}
|
|
976
|
+
if (!actor.isDirector && !['draft', 'rejected'].includes(current.status)) {
|
|
977
|
+
throw new common_1.BadRequestException('Only draft or rejected timesheets can be edited.');
|
|
978
|
+
}
|
|
979
|
+
await this.prisma.$transaction(async (tx) => {
|
|
980
|
+
const updates = [];
|
|
981
|
+
const params = [];
|
|
982
|
+
this.pushUpdate(updates, params, 'week_start_date', data.weekStartDate);
|
|
983
|
+
this.pushUpdate(updates, params, 'week_end_date', data.weekEndDate);
|
|
984
|
+
this.pushUpdate(updates, params, 'notes', data.notes);
|
|
985
|
+
if (updates.length) {
|
|
986
|
+
params.push(timesheetId);
|
|
987
|
+
await tx.$executeRawUnsafe(`UPDATE operations_timesheet
|
|
988
|
+
SET ${updates.join(', ')},
|
|
989
|
+
updated_at = NOW()
|
|
990
|
+
WHERE id = $${params.length}`, ...params);
|
|
991
|
+
}
|
|
992
|
+
if (data.entries) {
|
|
993
|
+
await this.replaceTimesheetEntries(tx, timesheetId, data.entries, current.collaboratorId);
|
|
994
|
+
}
|
|
995
|
+
await this.refreshTimesheetTotal(tx, timesheetId);
|
|
996
|
+
});
|
|
997
|
+
return this.listSingleTimesheet(actor, timesheetId);
|
|
998
|
+
}
|
|
999
|
+
async submitTimesheet(userId, timesheetId) {
|
|
1000
|
+
var _a, _b;
|
|
1001
|
+
const actor = await this.getActorContext(userId);
|
|
1002
|
+
const current = await this.getTimesheetById(timesheetId);
|
|
1003
|
+
if (!actor.isDirector && current.collaboratorId !== actor.collaboratorId) {
|
|
1004
|
+
throw new common_1.ForbiddenException('You can only submit your own timesheets.');
|
|
1005
|
+
}
|
|
1006
|
+
if (!actor.isDirector && !['draft', 'rejected'].includes(current.status)) {
|
|
1007
|
+
throw new common_1.BadRequestException('Only draft or rejected timesheets can be submitted.');
|
|
1008
|
+
}
|
|
1009
|
+
const collaborator = await this.getCollaboratorById(current.collaboratorId);
|
|
1010
|
+
const approverId = (_b = (_a = current.approverCollaboratorId) !== null && _a !== void 0 ? _a : collaborator.supervisorId) !== null && _b !== void 0 ? _b : null;
|
|
1011
|
+
await this.prisma.$transaction(async (tx) => {
|
|
1012
|
+
await tx.$executeRawUnsafe(`UPDATE operations_timesheet
|
|
1013
|
+
SET status = 'submitted',
|
|
1014
|
+
approver_collaborator_id = $1,
|
|
1015
|
+
submitted_at = NOW(),
|
|
1016
|
+
updated_at = NOW()
|
|
1017
|
+
WHERE id = $2`, approverId, timesheetId);
|
|
1018
|
+
await this.upsertApproval(tx, {
|
|
1019
|
+
targetType: 'timesheet',
|
|
1020
|
+
targetId: timesheetId,
|
|
1021
|
+
requesterCollaboratorId: current.collaboratorId,
|
|
1022
|
+
approverCollaboratorId: approverId,
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
return this.listSingleTimesheet(actor, timesheetId);
|
|
1026
|
+
}
|
|
1027
|
+
async listTimeOffRequests(userId) {
|
|
1028
|
+
const actor = await this.getActorContext(userId);
|
|
1029
|
+
const filter = this.buildIdFilter(actor.visibleCollaboratorIds, 'tor.collaborator_id', actor.isDirector);
|
|
1030
|
+
return this.queryRows(`SELECT tor.id,
|
|
1031
|
+
tor.collaborator_id AS "collaboratorId",
|
|
1032
|
+
c.display_name AS "collaboratorName",
|
|
1033
|
+
tor.approver_collaborator_id AS "approverCollaboratorId",
|
|
1034
|
+
a.display_name AS "approverName",
|
|
1035
|
+
tor.request_type AS "requestType",
|
|
1036
|
+
tor.start_date AS "startDate",
|
|
1037
|
+
tor.end_date AS "endDate",
|
|
1038
|
+
tor.total_days AS "totalDays",
|
|
1039
|
+
tor.status,
|
|
1040
|
+
tor.reason,
|
|
1041
|
+
tor.submitted_at AS "submittedAt",
|
|
1042
|
+
tor.reviewed_at AS "reviewedAt",
|
|
1043
|
+
approval.decision_note AS "approverNote"
|
|
1044
|
+
FROM operations_time_off_request tor
|
|
1045
|
+
JOIN operations_collaborator c ON c.id = tor.collaborator_id
|
|
1046
|
+
LEFT JOIN operations_collaborator a ON a.id = tor.approver_collaborator_id
|
|
1047
|
+
LEFT JOIN operations_approval approval
|
|
1048
|
+
ON approval.target_type = 'time_off_request'
|
|
1049
|
+
AND approval.target_id = tor.id
|
|
1050
|
+
AND approval.deleted_at IS NULL
|
|
1051
|
+
WHERE tor.deleted_at IS NULL AND ${filter.clause}
|
|
1052
|
+
ORDER BY tor.start_date DESC, tor.id DESC`, filter.params);
|
|
1053
|
+
}
|
|
1054
|
+
async createTimeOffRequest(userId, data) {
|
|
1055
|
+
const actor = await this.getActorContext(userId);
|
|
1056
|
+
this.ensureCollaborator(actor);
|
|
1057
|
+
this.requireFields(data, ['startDate', 'endDate']);
|
|
1058
|
+
const collaboratorId = actor.isDirector && data.collaboratorId
|
|
1059
|
+
? data.collaboratorId
|
|
1060
|
+
: actor.collaboratorId;
|
|
1061
|
+
if (!collaboratorId) {
|
|
1062
|
+
throw new common_1.BadRequestException('Collaborator context is required.');
|
|
1063
|
+
}
|
|
1064
|
+
if (!actor.isDirector && collaboratorId !== actor.collaboratorId) {
|
|
1065
|
+
throw new common_1.ForbiddenException('You can only create your own time-off requests.');
|
|
1066
|
+
}
|
|
1067
|
+
const collaborator = await this.getCollaboratorById(collaboratorId);
|
|
1068
|
+
const created = await this.prisma.$transaction(async (tx) => {
|
|
1069
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1070
|
+
const row = (await tx.$queryRawUnsafe(`INSERT INTO operations_time_off_request (
|
|
1071
|
+
collaborator_id,
|
|
1072
|
+
approver_collaborator_id,
|
|
1073
|
+
request_type,
|
|
1074
|
+
start_date,
|
|
1075
|
+
end_date,
|
|
1076
|
+
total_days,
|
|
1077
|
+
status,
|
|
1078
|
+
reason,
|
|
1079
|
+
submitted_at,
|
|
1080
|
+
created_at,
|
|
1081
|
+
updated_at
|
|
1082
|
+
) VALUES ($1, $2, COALESCE($3, 'vacation'), $4, $5, $6, 'submitted', $7, NOW(), NOW(), NOW())
|
|
1083
|
+
RETURNING id`, collaboratorId, (_a = collaborator.supervisorId) !== null && _a !== void 0 ? _a : null, (_b = data.requestType) !== null && _b !== void 0 ? _b : 'vacation', data.startDate, data.endDate, (_c = data.totalDays) !== null && _c !== void 0 ? _c : null, (_d = data.reason) !== null && _d !== void 0 ? _d : null));
|
|
1084
|
+
const requestId = (_e = row[0]) === null || _e === void 0 ? void 0 : _e.id;
|
|
1085
|
+
await this.upsertApproval(tx, {
|
|
1086
|
+
targetType: 'time_off_request',
|
|
1087
|
+
targetId: requestId,
|
|
1088
|
+
requesterCollaboratorId: collaboratorId,
|
|
1089
|
+
approverCollaboratorId: (_f = collaborator.supervisorId) !== null && _f !== void 0 ? _f : null,
|
|
1090
|
+
});
|
|
1091
|
+
return requestId;
|
|
1092
|
+
});
|
|
1093
|
+
return this.querySingle(`SELECT id,
|
|
1094
|
+
collaborator_id AS "collaboratorId",
|
|
1095
|
+
approver_collaborator_id AS "approverCollaboratorId",
|
|
1096
|
+
request_type AS "requestType",
|
|
1097
|
+
start_date AS "startDate",
|
|
1098
|
+
end_date AS "endDate",
|
|
1099
|
+
total_days AS "totalDays",
|
|
1100
|
+
status,
|
|
1101
|
+
reason,
|
|
1102
|
+
submitted_at AS "submittedAt",
|
|
1103
|
+
reviewed_at AS "reviewedAt"
|
|
1104
|
+
FROM operations_time_off_request
|
|
1105
|
+
WHERE id = $1`, [created]);
|
|
1106
|
+
}
|
|
1107
|
+
async listScheduleAdjustments(userId) {
|
|
1108
|
+
const actor = await this.getActorContext(userId);
|
|
1109
|
+
const filter = this.buildIdFilter(actor.visibleCollaboratorIds, 'sar.collaborator_id', actor.isDirector);
|
|
1110
|
+
const requests = await this.queryRows(`SELECT sar.id,
|
|
1111
|
+
sar.collaborator_id AS "collaboratorId",
|
|
1112
|
+
c.display_name AS "collaboratorName",
|
|
1113
|
+
sar.approver_collaborator_id AS "approverCollaboratorId",
|
|
1114
|
+
a.display_name AS "approverName",
|
|
1115
|
+
sar.request_scope AS "requestScope",
|
|
1116
|
+
sar.effective_start_date AS "effectiveStartDate",
|
|
1117
|
+
sar.effective_end_date AS "effectiveEndDate",
|
|
1118
|
+
sar.status,
|
|
1119
|
+
sar.reason,
|
|
1120
|
+
sar.submitted_at AS "submittedAt",
|
|
1121
|
+
sar.reviewed_at AS "reviewedAt",
|
|
1122
|
+
approval.decision_note AS "approverNote"
|
|
1123
|
+
FROM operations_schedule_adjustment_request sar
|
|
1124
|
+
JOIN operations_collaborator c ON c.id = sar.collaborator_id
|
|
1125
|
+
LEFT JOIN operations_collaborator a ON a.id = sar.approver_collaborator_id
|
|
1126
|
+
LEFT JOIN operations_approval approval
|
|
1127
|
+
ON approval.target_type = 'schedule_adjustment_request'
|
|
1128
|
+
AND approval.target_id = sar.id
|
|
1129
|
+
AND approval.deleted_at IS NULL
|
|
1130
|
+
WHERE sar.deleted_at IS NULL AND ${filter.clause}
|
|
1131
|
+
ORDER BY sar.effective_start_date DESC, sar.id DESC`, filter.params);
|
|
1132
|
+
if (!requests.length) {
|
|
1133
|
+
return requests;
|
|
1134
|
+
}
|
|
1135
|
+
const days = await this.queryRows(`SELECT schedule_adjustment_request_id AS "requestId",
|
|
1136
|
+
weekday,
|
|
1137
|
+
is_working_day AS "isWorkingDay",
|
|
1138
|
+
start_time AS "startTime",
|
|
1139
|
+
end_time AS "endTime",
|
|
1140
|
+
break_minutes AS "breakMinutes"
|
|
1141
|
+
FROM operations_schedule_adjustment_day
|
|
1142
|
+
WHERE schedule_adjustment_request_id = ANY($1::int[])
|
|
1143
|
+
ORDER BY id ASC`, [requests.map((item) => item.id)]);
|
|
1144
|
+
const currentSchedule = await this.queryRows(`SELECT collaborator_id AS "collaboratorId",
|
|
1145
|
+
weekday,
|
|
1146
|
+
is_working_day AS "isWorkingDay",
|
|
1147
|
+
start_time AS "startTime",
|
|
1148
|
+
end_time AS "endTime",
|
|
1149
|
+
break_minutes AS "breakMinutes"
|
|
1150
|
+
FROM operations_collaborator_schedule_day
|
|
1151
|
+
WHERE collaborator_id = ANY($1::int[])
|
|
1152
|
+
ORDER BY id ASC`, [this.uniqueNumbers(requests.map((item) => item.collaboratorId))]);
|
|
1153
|
+
const grouped = this.groupBy(days, 'requestId');
|
|
1154
|
+
const currentScheduleByCollaborator = this.groupBy(currentSchedule, 'collaboratorId');
|
|
1155
|
+
return requests.map((request) => {
|
|
1156
|
+
var _a, _b;
|
|
1157
|
+
return (Object.assign(Object.assign({}, request), { days: (_a = grouped[request.id]) !== null && _a !== void 0 ? _a : [], currentSchedule: (_b = currentScheduleByCollaborator[request.collaboratorId]) !== null && _b !== void 0 ? _b : [] }));
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
async createScheduleAdjustmentRequest(userId, data) {
|
|
1161
|
+
const actor = await this.getActorContext(userId);
|
|
1162
|
+
this.ensureCollaborator(actor);
|
|
1163
|
+
this.requireFields(data, ['effectiveStartDate', 'days']);
|
|
1164
|
+
if (!Array.isArray(data.days) || data.days.length === 0) {
|
|
1165
|
+
throw new common_1.BadRequestException('At least one schedule day is required.');
|
|
1166
|
+
}
|
|
1167
|
+
const collaboratorId = actor.isDirector && data.collaboratorId
|
|
1168
|
+
? data.collaboratorId
|
|
1169
|
+
: actor.collaboratorId;
|
|
1170
|
+
if (!collaboratorId) {
|
|
1171
|
+
throw new common_1.BadRequestException('Collaborator context is required.');
|
|
1172
|
+
}
|
|
1173
|
+
if (!actor.isDirector && collaboratorId !== actor.collaboratorId) {
|
|
1174
|
+
throw new common_1.ForbiddenException('You can only create your own schedule adjustments.');
|
|
1175
|
+
}
|
|
1176
|
+
const collaborator = await this.getCollaboratorById(collaboratorId);
|
|
1177
|
+
const created = await this.prisma.$transaction(async (tx) => {
|
|
1178
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
1179
|
+
const row = (await tx.$queryRawUnsafe(`INSERT INTO operations_schedule_adjustment_request (
|
|
1180
|
+
collaborator_id,
|
|
1181
|
+
approver_collaborator_id,
|
|
1182
|
+
request_scope,
|
|
1183
|
+
effective_start_date,
|
|
1184
|
+
effective_end_date,
|
|
1185
|
+
status,
|
|
1186
|
+
reason,
|
|
1187
|
+
submitted_at,
|
|
1188
|
+
created_at,
|
|
1189
|
+
updated_at
|
|
1190
|
+
) VALUES (
|
|
1191
|
+
$1, $2, COALESCE($3, 'temporary'), $4, $5, 'submitted', $6, NOW(), NOW(), NOW()
|
|
1192
|
+
)
|
|
1193
|
+
RETURNING id`, collaboratorId, (_a = collaborator.supervisorId) !== null && _a !== void 0 ? _a : null, (_b = data.requestScope) !== null && _b !== void 0 ? _b : 'temporary', data.effectiveStartDate, (_c = data.effectiveEndDate) !== null && _c !== void 0 ? _c : null, (_d = data.reason) !== null && _d !== void 0 ? _d : null));
|
|
1194
|
+
const requestId = (_e = row[0]) === null || _e === void 0 ? void 0 : _e.id;
|
|
1195
|
+
for (const day of data.days) {
|
|
1196
|
+
await tx.$executeRawUnsafe(`INSERT INTO operations_schedule_adjustment_day (
|
|
1197
|
+
schedule_adjustment_request_id,
|
|
1198
|
+
weekday,
|
|
1199
|
+
is_working_day,
|
|
1200
|
+
start_time,
|
|
1201
|
+
end_time,
|
|
1202
|
+
break_minutes,
|
|
1203
|
+
created_at,
|
|
1204
|
+
updated_at
|
|
1205
|
+
) VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())`, requestId, day.weekday, (_f = day.isWorkingDay) !== null && _f !== void 0 ? _f : true, (_g = day.startTime) !== null && _g !== void 0 ? _g : null, (_h = day.endTime) !== null && _h !== void 0 ? _h : null, (_j = day.breakMinutes) !== null && _j !== void 0 ? _j : null);
|
|
1206
|
+
}
|
|
1207
|
+
await this.upsertApproval(tx, {
|
|
1208
|
+
targetType: 'schedule_adjustment_request',
|
|
1209
|
+
targetId: requestId,
|
|
1210
|
+
requesterCollaboratorId: collaboratorId,
|
|
1211
|
+
approverCollaboratorId: (_k = collaborator.supervisorId) !== null && _k !== void 0 ? _k : null,
|
|
1212
|
+
});
|
|
1213
|
+
return requestId;
|
|
1214
|
+
});
|
|
1215
|
+
return this.querySingle(`SELECT id,
|
|
1216
|
+
collaborator_id AS "collaboratorId",
|
|
1217
|
+
approver_collaborator_id AS "approverCollaboratorId",
|
|
1218
|
+
request_scope AS "requestScope",
|
|
1219
|
+
effective_start_date AS "effectiveStartDate",
|
|
1220
|
+
effective_end_date AS "effectiveEndDate",
|
|
1221
|
+
status,
|
|
1222
|
+
reason,
|
|
1223
|
+
submitted_at AS "submittedAt",
|
|
1224
|
+
reviewed_at AS "reviewedAt"
|
|
1225
|
+
FROM operations_schedule_adjustment_request
|
|
1226
|
+
WHERE id = $1`, [created]);
|
|
1227
|
+
}
|
|
1228
|
+
async listApprovals(userId) {
|
|
1229
|
+
const actor = await this.getActorContext(userId);
|
|
1230
|
+
this.ensureSupervisor(actor);
|
|
1231
|
+
const params = [];
|
|
1232
|
+
const clause = actor.isDirector
|
|
1233
|
+
? 'a.deleted_at IS NULL'
|
|
1234
|
+
: `a.deleted_at IS NULL AND a.approver_collaborator_id = ${this.param(params, actor.collaboratorId)}`;
|
|
1235
|
+
return this.queryRows(`SELECT a.id,
|
|
1236
|
+
a.target_type AS "targetType",
|
|
1237
|
+
a.target_id AS "targetId",
|
|
1238
|
+
a.requester_collaborator_id AS "requesterCollaboratorId",
|
|
1239
|
+
requester.display_name AS "requesterName",
|
|
1240
|
+
a.approver_collaborator_id AS "approverCollaboratorId",
|
|
1241
|
+
approver.display_name AS "approverName",
|
|
1242
|
+
a.status,
|
|
1243
|
+
a.submitted_at AS "submittedAt",
|
|
1244
|
+
a.decided_at AS "decidedAt",
|
|
1245
|
+
a.decision_note AS "decisionNote",
|
|
1246
|
+
t.week_start_date AS "timesheetWeekStartDate",
|
|
1247
|
+
t.week_end_date AS "timesheetWeekEndDate",
|
|
1248
|
+
t.total_hours AS "timesheetTotalHours",
|
|
1249
|
+
COALESCE(
|
|
1250
|
+
STRING_AGG(DISTINCT p.name, ', ') FILTER (WHERE p.name IS NOT NULL),
|
|
1251
|
+
''
|
|
1252
|
+
) AS "timesheetProjectNames",
|
|
1253
|
+
tor.request_type AS "timeOffType",
|
|
1254
|
+
tor.start_date AS "timeOffStartDate",
|
|
1255
|
+
tor.end_date AS "timeOffEndDate",
|
|
1256
|
+
tor.reason AS "timeOffReason",
|
|
1257
|
+
sar.request_scope AS "scheduleRequestScope",
|
|
1258
|
+
sar.effective_start_date AS "scheduleStartDate",
|
|
1259
|
+
sar.effective_end_date AS "scheduleEndDate",
|
|
1260
|
+
sar.reason AS "scheduleReason"
|
|
1261
|
+
FROM operations_approval a
|
|
1262
|
+
JOIN operations_collaborator requester
|
|
1263
|
+
ON requester.id = a.requester_collaborator_id
|
|
1264
|
+
LEFT JOIN operations_collaborator approver
|
|
1265
|
+
ON approver.id = a.approver_collaborator_id
|
|
1266
|
+
LEFT JOIN operations_timesheet t
|
|
1267
|
+
ON a.target_type = 'timesheet'
|
|
1268
|
+
AND t.id = a.target_id
|
|
1269
|
+
LEFT JOIN operations_timesheet_entry te
|
|
1270
|
+
ON te.timesheet_id = t.id
|
|
1271
|
+
AND te.deleted_at IS NULL
|
|
1272
|
+
LEFT JOIN operations_project_assignment pa
|
|
1273
|
+
ON pa.id = te.project_assignment_id
|
|
1274
|
+
LEFT JOIN operations_project p
|
|
1275
|
+
ON p.id = pa.project_id
|
|
1276
|
+
LEFT JOIN operations_time_off_request tor
|
|
1277
|
+
ON a.target_type = 'time_off_request'
|
|
1278
|
+
AND tor.id = a.target_id
|
|
1279
|
+
LEFT JOIN operations_schedule_adjustment_request sar
|
|
1280
|
+
ON a.target_type = 'schedule_adjustment_request'
|
|
1281
|
+
AND sar.id = a.target_id
|
|
1282
|
+
WHERE ${clause}
|
|
1283
|
+
GROUP BY a.id, requester.id, approver.id, t.id, tor.id, sar.id
|
|
1284
|
+
ORDER BY a.submitted_at DESC, a.id DESC`, params);
|
|
1285
|
+
}
|
|
1286
|
+
async approve(userId, approvalId, data) {
|
|
1287
|
+
return this.decideApproval(userId, approvalId, 'approve', data);
|
|
1288
|
+
}
|
|
1289
|
+
async reject(userId, approvalId, data) {
|
|
1290
|
+
return this.decideApproval(userId, approvalId, 'reject', data);
|
|
1291
|
+
}
|
|
1292
|
+
async publishAccountsPayableReference(userId, data) {
|
|
1293
|
+
var _a, _b, _c, _d;
|
|
1294
|
+
const actor = await this.getActorContext(userId);
|
|
1295
|
+
this.ensureSupervisor(actor);
|
|
1296
|
+
const sourceEntityId = String((data === null || data === void 0 ? void 0 : data.sourceEntityId) || '').trim();
|
|
1297
|
+
const sourceEntityType = String((data === null || data === void 0 ? void 0 : data.sourceEntityType) || '').trim() ||
|
|
1298
|
+
'operations_payable_request';
|
|
1299
|
+
const personId = Number(data === null || data === void 0 ? void 0 : data.personId);
|
|
1300
|
+
const totalAmount = Number(data === null || data === void 0 ? void 0 : data.totalAmount);
|
|
1301
|
+
const dueDate = String((data === null || data === void 0 ? void 0 : data.dueDate) || '').trim();
|
|
1302
|
+
const documentNumber = String((data === null || data === void 0 ? void 0 : data.documentNumber) || '').trim();
|
|
1303
|
+
const locale = String((data === null || data === void 0 ? void 0 : data.locale) || '').trim() || 'en';
|
|
1304
|
+
if (!sourceEntityId) {
|
|
1305
|
+
throw new common_1.BadRequestException('sourceEntityId is required.');
|
|
1306
|
+
}
|
|
1307
|
+
if (!Number.isInteger(personId) || personId <= 0) {
|
|
1308
|
+
throw new common_1.BadRequestException('personId must be a positive integer.');
|
|
1309
|
+
}
|
|
1310
|
+
if (!Number.isFinite(totalAmount) || totalAmount <= 0) {
|
|
1311
|
+
throw new common_1.BadRequestException('totalAmount must be greater than zero.');
|
|
1312
|
+
}
|
|
1313
|
+
if (!dueDate || Number.isNaN(new Date(dueDate).getTime())) {
|
|
1314
|
+
throw new common_1.BadRequestException('dueDate must be a valid ISO date string.');
|
|
1315
|
+
}
|
|
1316
|
+
if (!documentNumber) {
|
|
1317
|
+
throw new common_1.BadRequestException('documentNumber is required.');
|
|
1318
|
+
}
|
|
1319
|
+
const outboxEvent = await this.integrationApi.publishEvent({
|
|
1320
|
+
eventName: 'operations.accounts_payable.requested',
|
|
1321
|
+
sourceModule: 'operations',
|
|
1322
|
+
aggregateType: sourceEntityType,
|
|
1323
|
+
aggregateId: sourceEntityId,
|
|
1324
|
+
payload: {
|
|
1325
|
+
sourceEntityType,
|
|
1326
|
+
sourceEntityId,
|
|
1327
|
+
locale,
|
|
1328
|
+
requestedByUserId: userId,
|
|
1329
|
+
payable: {
|
|
1330
|
+
personId,
|
|
1331
|
+
dueDate,
|
|
1332
|
+
totalAmount,
|
|
1333
|
+
documentNumber,
|
|
1334
|
+
description: (_a = data === null || data === void 0 ? void 0 : data.description) !== null && _a !== void 0 ? _a : null,
|
|
1335
|
+
paymentChannel: (_b = data === null || data === void 0 ? void 0 : data.paymentChannel) !== null && _b !== void 0 ? _b : null,
|
|
1336
|
+
financeCategoryId: (_c = data === null || data === void 0 ? void 0 : data.financeCategoryId) !== null && _c !== void 0 ? _c : null,
|
|
1337
|
+
costCenterId: (_d = data === null || data === void 0 ? void 0 : data.costCenterId) !== null && _d !== void 0 ? _d : null,
|
|
1338
|
+
},
|
|
1339
|
+
},
|
|
1340
|
+
});
|
|
1341
|
+
return {
|
|
1342
|
+
queued: true,
|
|
1343
|
+
eventId: outboxEvent.id,
|
|
1344
|
+
eventName: outboxEvent.eventName,
|
|
1345
|
+
sourceEntityType,
|
|
1346
|
+
sourceEntityId,
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
async decideApproval(userId, approvalId, action, data) {
|
|
1350
|
+
const actor = await this.getActorContext(userId);
|
|
1351
|
+
this.ensureSupervisor(actor);
|
|
1352
|
+
const approval = await this.querySingle(`SELECT id,
|
|
1353
|
+
target_type AS "targetType",
|
|
1354
|
+
target_id AS "targetId",
|
|
1355
|
+
requester_collaborator_id AS "requesterCollaboratorId",
|
|
1356
|
+
approver_collaborator_id AS "approverCollaboratorId",
|
|
1357
|
+
status
|
|
1358
|
+
FROM operations_approval
|
|
1359
|
+
WHERE id = $1
|
|
1360
|
+
AND deleted_at IS NULL`, [approvalId]);
|
|
1361
|
+
if (!approval) {
|
|
1362
|
+
throw new common_1.NotFoundException('Approval not found.');
|
|
1363
|
+
}
|
|
1364
|
+
if (!actor.isDirector && approval.approverCollaboratorId !== actor.collaboratorId) {
|
|
1365
|
+
throw new common_1.ForbiddenException('You cannot decide this approval.');
|
|
1366
|
+
}
|
|
1367
|
+
if (approval.status !== 'pending') {
|
|
1368
|
+
throw new common_1.BadRequestException('Only pending approvals can be decided.');
|
|
1369
|
+
}
|
|
1370
|
+
const nextStatus = action === 'approve' ? 'approved' : 'rejected';
|
|
1371
|
+
await this.prisma.$transaction(async (tx) => {
|
|
1372
|
+
var _a, _b;
|
|
1373
|
+
await tx.$executeRawUnsafe(`UPDATE operations_approval
|
|
1374
|
+
SET status = $1,
|
|
1375
|
+
decided_at = NOW(),
|
|
1376
|
+
decision_note = $2,
|
|
1377
|
+
updated_at = NOW()
|
|
1378
|
+
WHERE id = $3`, nextStatus, (_a = data.note) !== null && _a !== void 0 ? _a : null, approvalId);
|
|
1379
|
+
if (approval.targetType === 'timesheet') {
|
|
1380
|
+
await tx.$executeRawUnsafe(`UPDATE operations_timesheet
|
|
1381
|
+
SET status = $1,
|
|
1382
|
+
reviewed_at = NOW(),
|
|
1383
|
+
approver_collaborator_id = $2,
|
|
1384
|
+
updated_at = NOW()
|
|
1385
|
+
WHERE id = $3`, nextStatus, actor.collaboratorId, approval.targetId);
|
|
1386
|
+
}
|
|
1387
|
+
if (approval.targetType === 'time_off_request') {
|
|
1388
|
+
await tx.$executeRawUnsafe(`UPDATE operations_time_off_request
|
|
1389
|
+
SET status = $1,
|
|
1390
|
+
reviewed_at = NOW(),
|
|
1391
|
+
approver_collaborator_id = $2,
|
|
1392
|
+
submitted_at = COALESCE(submitted_at, NOW()),
|
|
1393
|
+
updated_at = NOW()
|
|
1394
|
+
WHERE id = $3`, nextStatus, actor.collaboratorId, approval.targetId);
|
|
1395
|
+
}
|
|
1396
|
+
if (approval.targetType === 'schedule_adjustment_request') {
|
|
1397
|
+
await tx.$executeRawUnsafe(`UPDATE operations_schedule_adjustment_request
|
|
1398
|
+
SET status = $1,
|
|
1399
|
+
reviewed_at = NOW(),
|
|
1400
|
+
approver_collaborator_id = $2,
|
|
1401
|
+
submitted_at = COALESCE(submitted_at, NOW()),
|
|
1402
|
+
updated_at = NOW()
|
|
1403
|
+
WHERE id = $3`, nextStatus, actor.collaboratorId, approval.targetId);
|
|
1404
|
+
}
|
|
1405
|
+
await this.insertApprovalHistory(tx, approvalId, actor.collaboratorId, nextStatus === 'approved' ? 'approved' : 'rejected', (_b = data.note) !== null && _b !== void 0 ? _b : null);
|
|
1406
|
+
});
|
|
1407
|
+
return this.querySingle(`SELECT id,
|
|
1408
|
+
target_type AS "targetType",
|
|
1409
|
+
target_id AS "targetId",
|
|
1410
|
+
status,
|
|
1411
|
+
decided_at AS "decidedAt",
|
|
1412
|
+
decision_note AS "decisionNote"
|
|
1413
|
+
FROM operations_approval
|
|
1414
|
+
WHERE id = $1`, [approvalId]);
|
|
1415
|
+
}
|
|
1416
|
+
async getActorContext(userId) {
|
|
1417
|
+
var _a, _b;
|
|
1418
|
+
const roleSlugs = (await this.prisma.role.findMany({
|
|
1419
|
+
where: {
|
|
1420
|
+
role_user: {
|
|
1421
|
+
some: {
|
|
1422
|
+
user_id: userId,
|
|
1423
|
+
},
|
|
1424
|
+
},
|
|
1425
|
+
},
|
|
1426
|
+
select: {
|
|
1427
|
+
slug: true,
|
|
1428
|
+
},
|
|
1429
|
+
})).map((role) => role.slug);
|
|
1430
|
+
const isDirector = roleSlugs.includes('admin') || roleSlugs.includes(DIRECTOR_ROLE);
|
|
1431
|
+
const isSupervisor = isDirector || roleSlugs.includes(SUPERVISOR_ROLE);
|
|
1432
|
+
const isCollaborator = isSupervisor || roleSlugs.includes(COLLABORATOR_ROLE);
|
|
1433
|
+
const collaborator = await this.getCollaboratorByUserId(userId);
|
|
1434
|
+
if (!collaborator && isCollaborator && !isDirector) {
|
|
1435
|
+
throw new common_1.NotFoundException('The authenticated user does not have a linked operations collaborator profile.');
|
|
1436
|
+
}
|
|
1437
|
+
const collaboratorId = (_a = collaborator === null || collaborator === void 0 ? void 0 : collaborator.id) !== null && _a !== void 0 ? _a : null;
|
|
1438
|
+
const teamCollaboratorIds = isSupervisor && collaboratorId
|
|
1439
|
+
? await this.getDirectReportIds(collaboratorId)
|
|
1440
|
+
: [];
|
|
1441
|
+
return {
|
|
1442
|
+
userId,
|
|
1443
|
+
roleSlugs,
|
|
1444
|
+
collaboratorId,
|
|
1445
|
+
collaboratorName: (_b = collaborator === null || collaborator === void 0 ? void 0 : collaborator.displayName) !== null && _b !== void 0 ? _b : null,
|
|
1446
|
+
isDirector,
|
|
1447
|
+
isSupervisor,
|
|
1448
|
+
isCollaborator,
|
|
1449
|
+
teamCollaboratorIds,
|
|
1450
|
+
visibleCollaboratorIds: this.uniqueNumbers(isDirector
|
|
1451
|
+
? []
|
|
1452
|
+
: [collaboratorId, ...(isSupervisor ? teamCollaboratorIds : [])]),
|
|
1453
|
+
visibleProjectIds: isDirector
|
|
1454
|
+
? []
|
|
1455
|
+
: await this.getAssignedProjectIds(this.uniqueNumbers([
|
|
1456
|
+
collaboratorId,
|
|
1457
|
+
...(isSupervisor ? teamCollaboratorIds : []),
|
|
1458
|
+
])),
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
async getCollaboratorByUserId(userId) {
|
|
1462
|
+
return this.querySingle(`SELECT c.id,
|
|
1463
|
+
c.display_name AS "displayName",
|
|
1464
|
+
s.id AS "supervisorId",
|
|
1465
|
+
s.display_name AS "supervisorName"
|
|
1466
|
+
FROM operations_collaborator c
|
|
1467
|
+
LEFT JOIN operations_collaborator s
|
|
1468
|
+
ON s.id = c.supervisor_collaborator_id
|
|
1469
|
+
WHERE c.user_id = $1
|
|
1470
|
+
AND c.deleted_at IS NULL`, [userId]);
|
|
1471
|
+
}
|
|
1472
|
+
async getCollaboratorById(collaboratorId) {
|
|
1473
|
+
const collaborator = await this.querySingle(`SELECT c.id,
|
|
1474
|
+
c.display_name AS "displayName",
|
|
1475
|
+
s.id AS "supervisorId",
|
|
1476
|
+
s.display_name AS "supervisorName"
|
|
1477
|
+
FROM operations_collaborator c
|
|
1478
|
+
LEFT JOIN operations_collaborator s
|
|
1479
|
+
ON s.id = c.supervisor_collaborator_id
|
|
1480
|
+
WHERE c.id = $1
|
|
1481
|
+
AND c.deleted_at IS NULL`, [collaboratorId]);
|
|
1482
|
+
if (!collaborator) {
|
|
1483
|
+
throw new common_1.NotFoundException('Collaborator not found.');
|
|
1484
|
+
}
|
|
1485
|
+
return collaborator;
|
|
1486
|
+
}
|
|
1487
|
+
async getProjectDetails(projectId, actorCollaboratorId) {
|
|
1488
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1489
|
+
const project = await this.querySingle(`SELECT p.id,
|
|
1490
|
+
p.contract_id AS "contractId",
|
|
1491
|
+
p.manager_collaborator_id AS "managerCollaboratorId",
|
|
1492
|
+
p.code,
|
|
1493
|
+
p.name,
|
|
1494
|
+
p.client_name AS "clientName",
|
|
1495
|
+
p.summary,
|
|
1496
|
+
p.status,
|
|
1497
|
+
p.progress_percent AS "progressPercent",
|
|
1498
|
+
p.delivery_model AS "deliveryModel",
|
|
1499
|
+
p.budget_amount AS "budgetAmount",
|
|
1500
|
+
p.start_date AS "startDate",
|
|
1501
|
+
p.end_date AS "endDate",
|
|
1502
|
+
c.name AS "contractName",
|
|
1503
|
+
c.status AS "contractStatus",
|
|
1504
|
+
c.contract_category AS "contractCategory",
|
|
1505
|
+
m.display_name AS "managerName",
|
|
1506
|
+
MAX(CASE WHEN pa.collaborator_id = $2 THEN pa.id END)::int AS "myAssignmentId",
|
|
1507
|
+
MAX(CASE WHEN pa.collaborator_id = $2 THEN pa.role_label END) AS "myRoleLabel",
|
|
1508
|
+
COUNT(DISTINCT pa.id)::int AS "teamSize"
|
|
1509
|
+
FROM operations_project p
|
|
1510
|
+
LEFT JOIN operations_contract c ON c.id = p.contract_id
|
|
1511
|
+
LEFT JOIN operations_collaborator m ON m.id = p.manager_collaborator_id
|
|
1512
|
+
LEFT JOIN operations_project_assignment pa
|
|
1513
|
+
ON pa.project_id = p.id
|
|
1514
|
+
AND pa.deleted_at IS NULL
|
|
1515
|
+
WHERE p.id = $1
|
|
1516
|
+
AND p.deleted_at IS NULL
|
|
1517
|
+
GROUP BY p.id, c.id, m.id`, [projectId, actorCollaboratorId !== null && actorCollaboratorId !== void 0 ? actorCollaboratorId : null]);
|
|
1518
|
+
if (!project) {
|
|
1519
|
+
throw new common_1.NotFoundException('Project not found.');
|
|
1520
|
+
}
|
|
1521
|
+
const [assignments, relatedContract, timesheetSummary, operationalIndicators] = await Promise.all([
|
|
1522
|
+
this.queryRows(`SELECT pa.id,
|
|
1523
|
+
pa.collaborator_id AS "collaboratorId",
|
|
1524
|
+
c.display_name AS "collaboratorName",
|
|
1525
|
+
pa.role_label AS "roleLabel",
|
|
1526
|
+
pa.allocation_percent AS "allocationPercent",
|
|
1527
|
+
pa.weekly_hours AS "weeklyHours",
|
|
1528
|
+
pa.is_billable AS "isBillable",
|
|
1529
|
+
pa.start_date AS "startDate",
|
|
1530
|
+
pa.end_date AS "endDate",
|
|
1531
|
+
pa.status
|
|
1532
|
+
FROM operations_project_assignment pa
|
|
1533
|
+
JOIN operations_collaborator c ON c.id = pa.collaborator_id
|
|
1534
|
+
WHERE pa.project_id = $1
|
|
1535
|
+
AND pa.deleted_at IS NULL
|
|
1536
|
+
ORDER BY c.display_name ASC`, [projectId]),
|
|
1537
|
+
project.contractId
|
|
1538
|
+
? this.querySingle(`SELECT id,
|
|
1539
|
+
code,
|
|
1540
|
+
name,
|
|
1541
|
+
client_name AS "clientName",
|
|
1542
|
+
contract_category AS "contractCategory",
|
|
1543
|
+
billing_model AS "billingModel",
|
|
1544
|
+
status,
|
|
1545
|
+
start_date AS "startDate",
|
|
1546
|
+
end_date AS "endDate",
|
|
1547
|
+
budget_amount AS "budgetAmount",
|
|
1548
|
+
monthly_hour_cap AS "monthlyHourCap",
|
|
1549
|
+
description,
|
|
1550
|
+
origin_type AS "originType",
|
|
1551
|
+
origin_id AS "originId"
|
|
1552
|
+
FROM operations_contract
|
|
1553
|
+
WHERE id = $1
|
|
1554
|
+
AND deleted_at IS NULL`, [project.contractId])
|
|
1555
|
+
: Promise.resolve(null),
|
|
1556
|
+
this.querySingle(`SELECT COUNT(DISTINCT t.id)::text AS "totalTimesheets",
|
|
1557
|
+
COUNT(DISTINCT t.id) FILTER (WHERE t.status = 'submitted')::text AS "pendingTimesheets",
|
|
1558
|
+
COALESCE(SUM(e.hours), 0)::text AS "totalHours"
|
|
1559
|
+
FROM operations_project_assignment pa
|
|
1560
|
+
LEFT JOIN operations_timesheet_entry e
|
|
1561
|
+
ON e.project_assignment_id = pa.id
|
|
1562
|
+
AND e.deleted_at IS NULL
|
|
1563
|
+
LEFT JOIN operations_timesheet t
|
|
1564
|
+
ON t.id = e.timesheet_id
|
|
1565
|
+
AND t.deleted_at IS NULL
|
|
1566
|
+
WHERE pa.project_id = $1
|
|
1567
|
+
AND pa.deleted_at IS NULL`, [projectId]),
|
|
1568
|
+
this.querySingle(`SELECT COUNT(*) FILTER (WHERE status IN ('planned', 'active'))::text AS "activeAssignments",
|
|
1569
|
+
COUNT(*) FILTER (WHERE is_billable = true AND status IN ('planned', 'active'))::text AS "billableAssignments",
|
|
1570
|
+
COALESCE(AVG(allocation_percent), 0)::text AS "averageAllocation",
|
|
1571
|
+
COALESCE(SUM(weekly_hours), 0)::text AS "totalWeeklyHours"
|
|
1572
|
+
FROM operations_project_assignment
|
|
1573
|
+
WHERE project_id = $1
|
|
1574
|
+
AND deleted_at IS NULL`, [projectId]),
|
|
1575
|
+
]);
|
|
1576
|
+
return Object.assign(Object.assign({}, project), { assignments,
|
|
1577
|
+
relatedContract, timesheetSummary: {
|
|
1578
|
+
totalTimesheets: Number((_a = timesheetSummary === null || timesheetSummary === void 0 ? void 0 : timesheetSummary.totalTimesheets) !== null && _a !== void 0 ? _a : 0),
|
|
1579
|
+
pendingTimesheets: Number((_b = timesheetSummary === null || timesheetSummary === void 0 ? void 0 : timesheetSummary.pendingTimesheets) !== null && _b !== void 0 ? _b : 0),
|
|
1580
|
+
totalHours: Number((_c = timesheetSummary === null || timesheetSummary === void 0 ? void 0 : timesheetSummary.totalHours) !== null && _c !== void 0 ? _c : 0),
|
|
1581
|
+
}, operationalIndicators: {
|
|
1582
|
+
activeAssignments: Number((_d = operationalIndicators === null || operationalIndicators === void 0 ? void 0 : operationalIndicators.activeAssignments) !== null && _d !== void 0 ? _d : 0),
|
|
1583
|
+
billableAssignments: Number((_e = operationalIndicators === null || operationalIndicators === void 0 ? void 0 : operationalIndicators.billableAssignments) !== null && _e !== void 0 ? _e : 0),
|
|
1584
|
+
averageAllocation: Number((_f = operationalIndicators === null || operationalIndicators === void 0 ? void 0 : operationalIndicators.averageAllocation) !== null && _f !== void 0 ? _f : 0),
|
|
1585
|
+
totalWeeklyHours: Number((_g = operationalIndicators === null || operationalIndicators === void 0 ? void 0 : operationalIndicators.totalWeeklyHours) !== null && _g !== void 0 ? _g : 0),
|
|
1586
|
+
} });
|
|
1587
|
+
}
|
|
1588
|
+
async getCollaboratorDetails(collaboratorId) {
|
|
1589
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1590
|
+
const collaborator = await this.querySingle(`SELECT c.id,
|
|
1591
|
+
c.user_id AS "userId",
|
|
1592
|
+
c.code,
|
|
1593
|
+
c.collaborator_type AS "collaboratorType",
|
|
1594
|
+
c.display_name AS "displayName",
|
|
1595
|
+
c.department,
|
|
1596
|
+
c.title,
|
|
1597
|
+
c.level_label AS "levelLabel",
|
|
1598
|
+
c.weekly_capacity_hours AS "weeklyCapacityHours",
|
|
1599
|
+
c.status,
|
|
1600
|
+
c.joined_at AS "joinedAt",
|
|
1601
|
+
c.left_at AS "leftAt",
|
|
1602
|
+
c.notes,
|
|
1603
|
+
s.id AS "supervisorId",
|
|
1604
|
+
s.display_name AS "supervisorName",
|
|
1605
|
+
hiring_contract.id AS "contractId",
|
|
1606
|
+
hiring_contract.status AS "contractStatus",
|
|
1607
|
+
COUNT(DISTINCT pa.id)::int AS "activeAssignments"
|
|
1608
|
+
FROM operations_collaborator c
|
|
1609
|
+
LEFT JOIN operations_collaborator s
|
|
1610
|
+
ON s.id = c.supervisor_collaborator_id
|
|
1611
|
+
LEFT JOIN operations_project_assignment pa
|
|
1612
|
+
ON pa.collaborator_id = c.id
|
|
1613
|
+
AND pa.deleted_at IS NULL
|
|
1614
|
+
AND pa.status IN ('planned', 'active')
|
|
1615
|
+
LEFT JOIN LATERAL (
|
|
1616
|
+
SELECT oc.id, oc.status
|
|
1617
|
+
FROM operations_contract oc
|
|
1618
|
+
WHERE oc.related_collaborator_id = c.id
|
|
1619
|
+
AND oc.deleted_at IS NULL
|
|
1620
|
+
ORDER BY CASE WHEN oc.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
|
|
1621
|
+
oc.created_at DESC
|
|
1622
|
+
LIMIT 1
|
|
1623
|
+
) hiring_contract ON TRUE
|
|
1624
|
+
WHERE c.id = $1
|
|
1625
|
+
AND c.deleted_at IS NULL
|
|
1626
|
+
GROUP BY c.id, s.id, hiring_contract.id, hiring_contract.status`, [collaboratorId]);
|
|
1627
|
+
if (!collaborator) {
|
|
1628
|
+
throw new common_1.NotFoundException('Collaborator not found.');
|
|
1629
|
+
}
|
|
1630
|
+
const [assignedProjects, relatedContracts, weeklySchedule, timesheetSummary, timeOffSummary, scheduleAdjustmentRequests] = await Promise.all([
|
|
1631
|
+
this.queryRows(`SELECT p.id,
|
|
1632
|
+
p.code,
|
|
1633
|
+
p.name,
|
|
1634
|
+
p.status,
|
|
1635
|
+
pa.role_label AS "roleLabel",
|
|
1636
|
+
pa.allocation_percent AS "allocationPercent",
|
|
1637
|
+
pa.weekly_hours AS "weeklyHours",
|
|
1638
|
+
pa.start_date AS "startDate",
|
|
1639
|
+
pa.end_date AS "endDate"
|
|
1640
|
+
FROM operations_project_assignment pa
|
|
1641
|
+
JOIN operations_project p ON p.id = pa.project_id
|
|
1642
|
+
WHERE pa.collaborator_id = $1
|
|
1643
|
+
AND pa.deleted_at IS NULL
|
|
1644
|
+
AND p.deleted_at IS NULL
|
|
1645
|
+
ORDER BY p.name ASC`, [collaboratorId]),
|
|
1646
|
+
this.queryRows(`SELECT c.id,
|
|
1647
|
+
c.code,
|
|
1648
|
+
c.name,
|
|
1649
|
+
c.contract_category AS "contractCategory",
|
|
1650
|
+
c.client_name AS "clientName",
|
|
1651
|
+
c.billing_model AS "billingModel",
|
|
1652
|
+
c.start_date AS "startDate",
|
|
1653
|
+
c.end_date AS "endDate",
|
|
1654
|
+
c.budget_amount AS "budgetAmount",
|
|
1655
|
+
c.monthly_hour_cap AS "monthlyHourCap",
|
|
1656
|
+
c.status,
|
|
1657
|
+
c.origin_type AS "originType",
|
|
1658
|
+
c.origin_id AS "originId",
|
|
1659
|
+
c.description
|
|
1660
|
+
FROM operations_contract c
|
|
1661
|
+
WHERE c.related_collaborator_id = $1
|
|
1662
|
+
AND c.deleted_at IS NULL
|
|
1663
|
+
ORDER BY c.created_at DESC`, [collaboratorId]),
|
|
1664
|
+
this.queryRows(`SELECT weekday,
|
|
1665
|
+
is_working_day AS "isWorkingDay",
|
|
1666
|
+
start_time AS "startTime",
|
|
1667
|
+
end_time AS "endTime",
|
|
1668
|
+
break_minutes AS "breakMinutes"
|
|
1669
|
+
FROM operations_collaborator_schedule_day
|
|
1670
|
+
WHERE collaborator_id = $1
|
|
1671
|
+
AND deleted_at IS NULL
|
|
1672
|
+
ORDER BY id ASC`, [collaboratorId]),
|
|
1673
|
+
this.querySingle(`SELECT COUNT(*)::text AS "totalTimesheets",
|
|
1674
|
+
COUNT(*) FILTER (WHERE status = 'submitted')::text AS "pendingTimesheets",
|
|
1675
|
+
COALESCE(SUM(total_hours), 0)::text AS "totalHours"
|
|
1676
|
+
FROM operations_timesheet
|
|
1677
|
+
WHERE collaborator_id = $1
|
|
1678
|
+
AND deleted_at IS NULL`, [collaboratorId]),
|
|
1679
|
+
this.querySingle(`SELECT COUNT(*)::text AS "totalRequests",
|
|
1680
|
+
COUNT(*) FILTER (WHERE status = 'submitted')::text AS "pendingRequests",
|
|
1681
|
+
COUNT(*) FILTER (WHERE status = 'approved')::text AS "approvedRequests"
|
|
1682
|
+
FROM operations_time_off_request
|
|
1683
|
+
WHERE collaborator_id = $1
|
|
1684
|
+
AND deleted_at IS NULL`, [collaboratorId]),
|
|
1685
|
+
this.queryRows(`SELECT id,
|
|
1686
|
+
request_scope AS "requestScope",
|
|
1687
|
+
effective_start_date AS "effectiveStartDate",
|
|
1688
|
+
effective_end_date AS "effectiveEndDate",
|
|
1689
|
+
status,
|
|
1690
|
+
reason
|
|
1691
|
+
FROM operations_schedule_adjustment_request
|
|
1692
|
+
WHERE collaborator_id = $1
|
|
1693
|
+
AND deleted_at IS NULL
|
|
1694
|
+
ORDER BY effective_start_date DESC, id DESC`, [collaboratorId]),
|
|
1695
|
+
]);
|
|
1696
|
+
return Object.assign(Object.assign({}, collaborator), { assignedProjects,
|
|
1697
|
+
relatedContracts,
|
|
1698
|
+
weeklySchedule, timesheetSummary: {
|
|
1699
|
+
totalTimesheets: Number((_a = timesheetSummary === null || timesheetSummary === void 0 ? void 0 : timesheetSummary.totalTimesheets) !== null && _a !== void 0 ? _a : 0),
|
|
1700
|
+
pendingTimesheets: Number((_b = timesheetSummary === null || timesheetSummary === void 0 ? void 0 : timesheetSummary.pendingTimesheets) !== null && _b !== void 0 ? _b : 0),
|
|
1701
|
+
totalHours: Number((_c = timesheetSummary === null || timesheetSummary === void 0 ? void 0 : timesheetSummary.totalHours) !== null && _c !== void 0 ? _c : 0),
|
|
1702
|
+
}, timeOffSummary: {
|
|
1703
|
+
totalRequests: Number((_d = timeOffSummary === null || timeOffSummary === void 0 ? void 0 : timeOffSummary.totalRequests) !== null && _d !== void 0 ? _d : 0),
|
|
1704
|
+
pendingRequests: Number((_e = timeOffSummary === null || timeOffSummary === void 0 ? void 0 : timeOffSummary.pendingRequests) !== null && _e !== void 0 ? _e : 0),
|
|
1705
|
+
approvedRequests: Number((_f = timeOffSummary === null || timeOffSummary === void 0 ? void 0 : timeOffSummary.approvedRequests) !== null && _f !== void 0 ? _f : 0),
|
|
1706
|
+
}, scheduleAdjustmentRequests });
|
|
1707
|
+
}
|
|
1708
|
+
async getDirectReportIds(collaboratorId) {
|
|
1709
|
+
return (await this.queryRows(`SELECT id
|
|
1710
|
+
FROM operations_collaborator
|
|
1711
|
+
WHERE supervisor_collaborator_id = $1
|
|
1712
|
+
AND deleted_at IS NULL
|
|
1713
|
+
ORDER BY id ASC`, [collaboratorId])).map((row) => row.id);
|
|
1714
|
+
}
|
|
1715
|
+
async getAssignedProjectIds(collaboratorIds) {
|
|
1716
|
+
if (!collaboratorIds.length)
|
|
1717
|
+
return [];
|
|
1718
|
+
return (await this.queryRows(`SELECT DISTINCT project_id AS "projectId"
|
|
1719
|
+
FROM operations_project_assignment
|
|
1720
|
+
WHERE deleted_at IS NULL
|
|
1721
|
+
AND status IN ('planned', 'active')
|
|
1722
|
+
AND collaborator_id = ANY($1::int[])`, [collaboratorIds])).map((row) => row.projectId);
|
|
1723
|
+
}
|
|
1724
|
+
async getTimesheetById(timesheetId) {
|
|
1725
|
+
const timesheet = await this.querySingle(`SELECT id,
|
|
1726
|
+
collaborator_id AS "collaboratorId",
|
|
1727
|
+
approver_collaborator_id AS "approverCollaboratorId",
|
|
1728
|
+
status
|
|
1729
|
+
FROM operations_timesheet
|
|
1730
|
+
WHERE id = $1
|
|
1731
|
+
AND deleted_at IS NULL`, [timesheetId]);
|
|
1732
|
+
if (!timesheet) {
|
|
1733
|
+
throw new common_1.NotFoundException('Timesheet not found.');
|
|
1734
|
+
}
|
|
1735
|
+
return timesheet;
|
|
1736
|
+
}
|
|
1737
|
+
async replaceTimesheetEntries(client, timesheetId, entries, collaboratorId) {
|
|
1738
|
+
var _a, _b, _c;
|
|
1739
|
+
await client.$executeRawUnsafe(`UPDATE operations_timesheet_entry
|
|
1740
|
+
SET deleted_at = NOW()
|
|
1741
|
+
WHERE timesheet_id = $1
|
|
1742
|
+
AND deleted_at IS NULL`, timesheetId);
|
|
1743
|
+
if (!entries.length)
|
|
1744
|
+
return;
|
|
1745
|
+
const assignmentIds = entries
|
|
1746
|
+
.map((entry) => entry.projectAssignmentId)
|
|
1747
|
+
.filter((value) => typeof value === 'number');
|
|
1748
|
+
if (assignmentIds.length) {
|
|
1749
|
+
const assignments = (await client.$queryRawUnsafe(`SELECT id, collaborator_id AS "collaboratorId"
|
|
1750
|
+
FROM operations_project_assignment
|
|
1751
|
+
WHERE id = ANY($1::int[])
|
|
1752
|
+
AND deleted_at IS NULL`, assignmentIds));
|
|
1753
|
+
const assignmentMap = new Map(assignments.map((assignment) => [
|
|
1754
|
+
assignment.id,
|
|
1755
|
+
assignment.collaboratorId,
|
|
1756
|
+
]));
|
|
1757
|
+
for (const assignmentId of assignmentIds) {
|
|
1758
|
+
if (assignmentMap.get(assignmentId) !== collaboratorId) {
|
|
1759
|
+
throw new common_1.ForbiddenException('Timesheet entries must use assignments owned by the target collaborator.');
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
for (const entry of entries) {
|
|
1764
|
+
await client.$executeRawUnsafe(`INSERT INTO operations_timesheet_entry (
|
|
1765
|
+
timesheet_id,
|
|
1766
|
+
project_assignment_id,
|
|
1767
|
+
activity_label,
|
|
1768
|
+
work_date,
|
|
1769
|
+
hours,
|
|
1770
|
+
description,
|
|
1771
|
+
created_at,
|
|
1772
|
+
updated_at
|
|
1773
|
+
) VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())`, timesheetId, (_a = entry.projectAssignmentId) !== null && _a !== void 0 ? _a : null, (_b = entry.activityLabel) !== null && _b !== void 0 ? _b : null, entry.workDate, entry.hours, (_c = entry.description) !== null && _c !== void 0 ? _c : null);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
async refreshTimesheetTotal(client, timesheetId) {
|
|
1777
|
+
await client.$executeRawUnsafe(`UPDATE operations_timesheet
|
|
1778
|
+
SET total_hours = (
|
|
1779
|
+
SELECT COALESCE(SUM(hours), 0)
|
|
1780
|
+
FROM operations_timesheet_entry
|
|
1781
|
+
WHERE timesheet_id = $1
|
|
1782
|
+
AND deleted_at IS NULL
|
|
1783
|
+
),
|
|
1784
|
+
updated_at = NOW()
|
|
1785
|
+
WHERE id = $1`, timesheetId);
|
|
1786
|
+
}
|
|
1787
|
+
async upsertApproval(client, input) {
|
|
1788
|
+
var _a, _b;
|
|
1789
|
+
const existing = (await client.$queryRawUnsafe(`SELECT id
|
|
1790
|
+
FROM operations_approval
|
|
1791
|
+
WHERE target_type = $1
|
|
1792
|
+
AND target_id = $2
|
|
1793
|
+
AND deleted_at IS NULL`, input.targetType, input.targetId));
|
|
1794
|
+
let approvalId = (_a = existing[0]) === null || _a === void 0 ? void 0 : _a.id;
|
|
1795
|
+
if (!approvalId) {
|
|
1796
|
+
const created = (await client.$queryRawUnsafe(`INSERT INTO operations_approval (
|
|
1797
|
+
target_type,
|
|
1798
|
+
target_id,
|
|
1799
|
+
requester_collaborator_id,
|
|
1800
|
+
approver_collaborator_id,
|
|
1801
|
+
status,
|
|
1802
|
+
submitted_at,
|
|
1803
|
+
created_at,
|
|
1804
|
+
updated_at
|
|
1805
|
+
) VALUES ($1, $2, $3, $4, 'pending', NOW(), NOW(), NOW())
|
|
1806
|
+
RETURNING id`, input.targetType, input.targetId, input.requesterCollaboratorId, input.approverCollaboratorId));
|
|
1807
|
+
approvalId = (_b = created[0]) === null || _b === void 0 ? void 0 : _b.id;
|
|
1808
|
+
await this.insertApprovalHistory(client, approvalId, input.requesterCollaboratorId, 'created', null);
|
|
1809
|
+
}
|
|
1810
|
+
else {
|
|
1811
|
+
await client.$executeRawUnsafe(`UPDATE operations_approval
|
|
1812
|
+
SET requester_collaborator_id = $1,
|
|
1813
|
+
approver_collaborator_id = $2,
|
|
1814
|
+
status = 'pending',
|
|
1815
|
+
submitted_at = NOW(),
|
|
1816
|
+
decided_at = NULL,
|
|
1817
|
+
decision_note = NULL,
|
|
1818
|
+
updated_at = NOW()
|
|
1819
|
+
WHERE id = $3`, input.requesterCollaboratorId, input.approverCollaboratorId, approvalId);
|
|
1820
|
+
await this.insertApprovalHistory(client, approvalId, input.requesterCollaboratorId, 'reopened', null);
|
|
1821
|
+
}
|
|
1822
|
+
await this.insertApprovalHistory(client, approvalId, input.requesterCollaboratorId, 'submitted', null);
|
|
1823
|
+
}
|
|
1824
|
+
async insertApprovalHistory(client, approvalId, actorCollaboratorId, action, note) {
|
|
1825
|
+
await client.$executeRawUnsafe(`INSERT INTO operations_approval_history (
|
|
1826
|
+
approval_id,
|
|
1827
|
+
actor_collaborator_id,
|
|
1828
|
+
action,
|
|
1829
|
+
note,
|
|
1830
|
+
created_at
|
|
1831
|
+
) VALUES ($1, $2, $3, $4, NOW())`, approvalId, actorCollaboratorId, action, note);
|
|
1832
|
+
}
|
|
1833
|
+
async assertProjectAccess(actor, projectId) {
|
|
1834
|
+
if (actor.isDirector)
|
|
1835
|
+
return;
|
|
1836
|
+
if (!actor.visibleProjectIds.includes(projectId)) {
|
|
1837
|
+
throw new common_1.ForbiddenException('You do not have access to this project.');
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
ensureCollaboratorAccess(actor, collaboratorId) {
|
|
1841
|
+
if (actor.isDirector) {
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
if (!actor.visibleCollaboratorIds.includes(collaboratorId)) {
|
|
1845
|
+
throw new common_1.ForbiddenException('You do not have access to this collaborator.');
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
async listSingleTimesheet(actor, timesheetId) {
|
|
1849
|
+
const timesheets = await this.listTimesheets(actor.userId);
|
|
1850
|
+
const timesheet = timesheets.find((item) => item.id === timesheetId);
|
|
1851
|
+
if (!timesheet) {
|
|
1852
|
+
throw new common_1.NotFoundException('Timesheet not found.');
|
|
1853
|
+
}
|
|
1854
|
+
return timesheet;
|
|
1855
|
+
}
|
|
1856
|
+
ensureDirector(actor) {
|
|
1857
|
+
if (!actor.isDirector) {
|
|
1858
|
+
throw new common_1.ForbiddenException('Director access is required.');
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
ensureSupervisor(actor) {
|
|
1862
|
+
if (!actor.isSupervisor) {
|
|
1863
|
+
throw new common_1.ForbiddenException('Supervisor access is required.');
|
|
1864
|
+
}
|
|
95
1865
|
}
|
|
96
|
-
|
|
97
|
-
|
|
1866
|
+
ensureCollaborator(actor) {
|
|
1867
|
+
if (!actor.isCollaborator) {
|
|
1868
|
+
throw new common_1.ForbiddenException('Operations collaborator access is required.');
|
|
1869
|
+
}
|
|
98
1870
|
}
|
|
99
|
-
|
|
100
|
-
return
|
|
1871
|
+
defaultWeeklySchedule() {
|
|
1872
|
+
return [
|
|
1873
|
+
{
|
|
1874
|
+
weekday: 'monday',
|
|
1875
|
+
isWorkingDay: true,
|
|
1876
|
+
startTime: '09:00',
|
|
1877
|
+
endTime: '18:00',
|
|
1878
|
+
breakMinutes: 60,
|
|
1879
|
+
},
|
|
1880
|
+
{
|
|
1881
|
+
weekday: 'tuesday',
|
|
1882
|
+
isWorkingDay: true,
|
|
1883
|
+
startTime: '09:00',
|
|
1884
|
+
endTime: '18:00',
|
|
1885
|
+
breakMinutes: 60,
|
|
1886
|
+
},
|
|
1887
|
+
{
|
|
1888
|
+
weekday: 'wednesday',
|
|
1889
|
+
isWorkingDay: true,
|
|
1890
|
+
startTime: '09:00',
|
|
1891
|
+
endTime: '18:00',
|
|
1892
|
+
breakMinutes: 60,
|
|
1893
|
+
},
|
|
1894
|
+
{
|
|
1895
|
+
weekday: 'thursday',
|
|
1896
|
+
isWorkingDay: true,
|
|
1897
|
+
startTime: '09:00',
|
|
1898
|
+
endTime: '18:00',
|
|
1899
|
+
breakMinutes: 60,
|
|
1900
|
+
},
|
|
1901
|
+
{
|
|
1902
|
+
weekday: 'friday',
|
|
1903
|
+
isWorkingDay: true,
|
|
1904
|
+
startTime: '09:00',
|
|
1905
|
+
endTime: '18:00',
|
|
1906
|
+
breakMinutes: 60,
|
|
1907
|
+
},
|
|
1908
|
+
{
|
|
1909
|
+
weekday: 'saturday',
|
|
1910
|
+
isWorkingDay: false,
|
|
1911
|
+
startTime: null,
|
|
1912
|
+
endTime: null,
|
|
1913
|
+
breakMinutes: 0,
|
|
1914
|
+
},
|
|
1915
|
+
{
|
|
1916
|
+
weekday: 'sunday',
|
|
1917
|
+
isWorkingDay: false,
|
|
1918
|
+
startTime: null,
|
|
1919
|
+
endTime: null,
|
|
1920
|
+
breakMinutes: 0,
|
|
1921
|
+
},
|
|
1922
|
+
];
|
|
101
1923
|
}
|
|
102
|
-
|
|
103
|
-
|
|
1924
|
+
async replaceCollaboratorScheduleDays(client, collaboratorId, weeklySchedule) {
|
|
1925
|
+
var _a, _b, _c, _d;
|
|
1926
|
+
const schedule = (weeklySchedule === null || weeklySchedule === void 0 ? void 0 : weeklySchedule.length)
|
|
1927
|
+
? weeklySchedule
|
|
1928
|
+
: this.defaultWeeklySchedule();
|
|
1929
|
+
await client.$executeRawUnsafe(`UPDATE operations_collaborator_schedule_day
|
|
1930
|
+
SET deleted_at = NOW(),
|
|
1931
|
+
updated_at = NOW()
|
|
1932
|
+
WHERE collaborator_id = $1
|
|
1933
|
+
AND deleted_at IS NULL`, collaboratorId);
|
|
1934
|
+
for (const day of schedule) {
|
|
1935
|
+
await client.$executeRawUnsafe(`INSERT INTO operations_collaborator_schedule_day (
|
|
1936
|
+
collaborator_id,
|
|
1937
|
+
weekday,
|
|
1938
|
+
is_working_day,
|
|
1939
|
+
start_time,
|
|
1940
|
+
end_time,
|
|
1941
|
+
break_minutes,
|
|
1942
|
+
created_at,
|
|
1943
|
+
updated_at
|
|
1944
|
+
) VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())`, collaboratorId, day.weekday, (_a = day.isWorkingDay) !== null && _a !== void 0 ? _a : true, day.isWorkingDay === false ? null : (_b = day.startTime) !== null && _b !== void 0 ? _b : null, day.isWorkingDay === false ? null : (_c = day.endTime) !== null && _c !== void 0 ? _c : null, (_d = day.breakMinutes) !== null && _d !== void 0 ? _d : null);
|
|
1945
|
+
}
|
|
104
1946
|
}
|
|
105
|
-
|
|
106
|
-
|
|
1947
|
+
async replaceProjectAssignments(client, projectId, teamAssignments) {
|
|
1948
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1949
|
+
await client.$executeRawUnsafe(`UPDATE operations_project_assignment
|
|
1950
|
+
SET deleted_at = NOW(),
|
|
1951
|
+
updated_at = NOW()
|
|
1952
|
+
WHERE project_id = $1
|
|
1953
|
+
AND deleted_at IS NULL`, projectId);
|
|
1954
|
+
for (const assignment of teamAssignments !== null && teamAssignments !== void 0 ? teamAssignments : []) {
|
|
1955
|
+
await client.$executeRawUnsafe(`INSERT INTO operations_project_assignment (
|
|
1956
|
+
project_id,
|
|
1957
|
+
collaborator_id,
|
|
1958
|
+
role_label,
|
|
1959
|
+
allocation_percent,
|
|
1960
|
+
weekly_hours,
|
|
1961
|
+
is_billable,
|
|
1962
|
+
start_date,
|
|
1963
|
+
end_date,
|
|
1964
|
+
status,
|
|
1965
|
+
created_at,
|
|
1966
|
+
updated_at
|
|
1967
|
+
) VALUES (
|
|
1968
|
+
$1, $2, $3, $4, $5, $6, $7, $8, COALESCE($9, 'active'), NOW(), NOW()
|
|
1969
|
+
)`, projectId, assignment.collaboratorId, (_a = assignment.roleLabel) !== null && _a !== void 0 ? _a : 'Team Member', (_b = assignment.allocationPercent) !== null && _b !== void 0 ? _b : null, (_c = assignment.weeklyHours) !== null && _c !== void 0 ? _c : null, (_d = assignment.isBillable) !== null && _d !== void 0 ? _d : true, (_e = assignment.startDate) !== null && _e !== void 0 ? _e : null, (_f = assignment.endDate) !== null && _f !== void 0 ? _f : null, (_g = assignment.status) !== null && _g !== void 0 ? _g : 'active');
|
|
1970
|
+
}
|
|
107
1971
|
}
|
|
108
|
-
|
|
109
|
-
|
|
1972
|
+
mapContractCategoryForCollaboratorType(collaboratorType) {
|
|
1973
|
+
switch (collaboratorType) {
|
|
1974
|
+
case 'clt':
|
|
1975
|
+
return 'employee';
|
|
1976
|
+
case 'pj':
|
|
1977
|
+
case 'freelancer':
|
|
1978
|
+
return 'contractor';
|
|
1979
|
+
default:
|
|
1980
|
+
return 'other';
|
|
1981
|
+
}
|
|
110
1982
|
}
|
|
111
|
-
|
|
112
|
-
return
|
|
1983
|
+
mapBillingModelForCollaboratorType(collaboratorType) {
|
|
1984
|
+
return collaboratorType === 'freelancer'
|
|
1985
|
+
? 'time_and_material'
|
|
1986
|
+
: 'monthly_retainer';
|
|
113
1987
|
}
|
|
114
|
-
|
|
115
|
-
|
|
1988
|
+
mapContractTypeForCollaboratorType(collaboratorType) {
|
|
1989
|
+
switch (collaboratorType) {
|
|
1990
|
+
case 'clt':
|
|
1991
|
+
return 'clt';
|
|
1992
|
+
case 'pj':
|
|
1993
|
+
return 'pj';
|
|
1994
|
+
case 'freelancer':
|
|
1995
|
+
return 'freelancer_agreement';
|
|
1996
|
+
case 'intern':
|
|
1997
|
+
return 'fixed_term';
|
|
1998
|
+
default:
|
|
1999
|
+
return 'other';
|
|
2000
|
+
}
|
|
116
2001
|
}
|
|
117
|
-
|
|
118
|
-
|
|
2002
|
+
async createHiringContractDraft(client, createdByUserId, input) {
|
|
2003
|
+
var _a, _b, _c;
|
|
2004
|
+
const contractCode = `HIR-${input.collaboratorCode}`;
|
|
2005
|
+
const contractName = input.collaboratorType === 'clt'
|
|
2006
|
+
? `${input.displayName} Employment Contract`
|
|
2007
|
+
: `${input.displayName} Service Contract`;
|
|
2008
|
+
await client.$executeRawUnsafe(`INSERT INTO operations_contract (
|
|
2009
|
+
code,
|
|
2010
|
+
name,
|
|
2011
|
+
contract_category,
|
|
2012
|
+
contract_type,
|
|
2013
|
+
client_name,
|
|
2014
|
+
signature_status,
|
|
2015
|
+
is_active,
|
|
2016
|
+
billing_model,
|
|
2017
|
+
account_manager_collaborator_id,
|
|
2018
|
+
related_collaborator_id,
|
|
2019
|
+
origin_type,
|
|
2020
|
+
origin_id,
|
|
2021
|
+
start_date,
|
|
2022
|
+
end_date,
|
|
2023
|
+
signed_at,
|
|
2024
|
+
effective_date,
|
|
2025
|
+
budget_amount,
|
|
2026
|
+
monthly_hour_cap,
|
|
2027
|
+
status,
|
|
2028
|
+
description,
|
|
2029
|
+
content_html,
|
|
2030
|
+
created_by_user_id,
|
|
2031
|
+
updated_by_user_id,
|
|
2032
|
+
created_at,
|
|
2033
|
+
updated_at
|
|
2034
|
+
) VALUES (
|
|
2035
|
+
$1, $2, $3, $4, $5, 'not_started', true, $6, $7, $8, 'employee_hiring', $9, $10, NULL,
|
|
2036
|
+
NULL, $10, $11, $12, 'draft', $13, NULL, $14, $14, NOW(), NOW()
|
|
2037
|
+
)`, contractCode, contractName, this.mapContractCategoryForCollaboratorType(input.collaboratorType), this.mapContractTypeForCollaboratorType(input.collaboratorType), input.displayName, this.mapBillingModelForCollaboratorType(input.collaboratorType), input.supervisorCollaboratorId, input.collaboratorId, input.collaboratorId, (_a = input.startDate) !== null && _a !== void 0 ? _a : new Date().toISOString().slice(0, 10), (_b = input.compensationAmount) !== null && _b !== void 0 ? _b : null, input.weeklyCapacityHours
|
|
2038
|
+
? Math.round(Number(input.weeklyCapacityHours) * 4)
|
|
2039
|
+
: null, (_c = input.description) !== null && _c !== void 0 ? _c : null, createdByUserId);
|
|
119
2040
|
}
|
|
120
|
-
|
|
121
|
-
|
|
2041
|
+
async createProjectContractDraft(client, createdByUserId, input) {
|
|
2042
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
2043
|
+
const created = await client.$queryRawUnsafe(`INSERT INTO operations_contract (
|
|
2044
|
+
code,
|
|
2045
|
+
name,
|
|
2046
|
+
contract_category,
|
|
2047
|
+
contract_type,
|
|
2048
|
+
client_name,
|
|
2049
|
+
signature_status,
|
|
2050
|
+
is_active,
|
|
2051
|
+
billing_model,
|
|
2052
|
+
account_manager_collaborator_id,
|
|
2053
|
+
related_collaborator_id,
|
|
2054
|
+
origin_type,
|
|
2055
|
+
origin_id,
|
|
2056
|
+
start_date,
|
|
2057
|
+
end_date,
|
|
2058
|
+
signed_at,
|
|
2059
|
+
effective_date,
|
|
2060
|
+
budget_amount,
|
|
2061
|
+
monthly_hour_cap,
|
|
2062
|
+
status,
|
|
2063
|
+
description,
|
|
2064
|
+
content_html,
|
|
2065
|
+
created_by_user_id,
|
|
2066
|
+
updated_by_user_id,
|
|
2067
|
+
created_at,
|
|
2068
|
+
updated_at
|
|
2069
|
+
) VALUES (
|
|
2070
|
+
$1, $2, 'client', 'service_agreement', $3, 'not_started', true, $4, $5, NULL, 'client_project', $6,
|
|
2071
|
+
$7, $8, NULL, $7, $9, $10, 'draft', $11, NULL, $12, $12, NOW(), NOW()
|
|
2072
|
+
)
|
|
2073
|
+
RETURNING id`, (_a = input.contractCode) !== null && _a !== void 0 ? _a : `PRJ-${input.projectCode}`, (_b = input.contractName) !== null && _b !== void 0 ? _b : `${input.projectName} Service Agreement`, input.clientName, input.billingModel, input.managerCollaboratorId, input.projectId, (_c = input.startDate) !== null && _c !== void 0 ? _c : new Date().toISOString().slice(0, 10), (_d = input.endDate) !== null && _d !== void 0 ? _d : null, (_e = input.budgetAmount) !== null && _e !== void 0 ? _e : null, (_f = input.monthlyHourCap) !== null && _f !== void 0 ? _f : null, (_g = input.description) !== null && _g !== void 0 ? _g : null, createdByUserId);
|
|
2074
|
+
return (_h = created[0]) === null || _h === void 0 ? void 0 : _h.id;
|
|
122
2075
|
}
|
|
123
|
-
|
|
124
|
-
|
|
2076
|
+
async replaceContractParties(client, contractId, parties) {
|
|
2077
|
+
var _a, _b, _c, _d, _e, _f;
|
|
2078
|
+
await client.$executeRawUnsafe(`UPDATE operations_contract_party
|
|
2079
|
+
SET deleted_at = NOW(),
|
|
2080
|
+
updated_at = NOW()
|
|
2081
|
+
WHERE contract_id = $1
|
|
2082
|
+
AND deleted_at IS NULL`, contractId);
|
|
2083
|
+
for (const party of parties !== null && parties !== void 0 ? parties : []) {
|
|
2084
|
+
await client.$executeRawUnsafe(`INSERT INTO operations_contract_party (
|
|
2085
|
+
contract_id,
|
|
2086
|
+
party_role,
|
|
2087
|
+
party_type,
|
|
2088
|
+
display_name,
|
|
2089
|
+
document_number,
|
|
2090
|
+
email,
|
|
2091
|
+
phone,
|
|
2092
|
+
is_primary,
|
|
2093
|
+
created_at,
|
|
2094
|
+
updated_at
|
|
2095
|
+
) VALUES (
|
|
2096
|
+
$1, COALESCE($2, 'other'), COALESCE($3, 'company'), $4, $5, $6, $7, COALESCE($8, false), NOW(), NOW()
|
|
2097
|
+
)`, contractId, (_a = party.partyRole) !== null && _a !== void 0 ? _a : 'other', (_b = party.partyType) !== null && _b !== void 0 ? _b : 'company', party.displayName, (_c = party.documentNumber) !== null && _c !== void 0 ? _c : null, (_d = party.email) !== null && _d !== void 0 ? _d : null, (_e = party.phone) !== null && _e !== void 0 ? _e : null, (_f = party.isPrimary) !== null && _f !== void 0 ? _f : false);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
async replaceContractSignatures(client, contractId, signatures) {
|
|
2101
|
+
var _a, _b, _c, _d;
|
|
2102
|
+
await client.$executeRawUnsafe(`UPDATE operations_contract_signature
|
|
2103
|
+
SET deleted_at = NOW(),
|
|
2104
|
+
updated_at = NOW()
|
|
2105
|
+
WHERE contract_id = $1
|
|
2106
|
+
AND deleted_at IS NULL`, contractId);
|
|
2107
|
+
for (const signature of signatures !== null && signatures !== void 0 ? signatures : []) {
|
|
2108
|
+
await client.$executeRawUnsafe(`INSERT INTO operations_contract_signature (
|
|
2109
|
+
contract_id,
|
|
2110
|
+
signer_name,
|
|
2111
|
+
signer_role,
|
|
2112
|
+
signer_email,
|
|
2113
|
+
signer_status,
|
|
2114
|
+
signed_at,
|
|
2115
|
+
created_at,
|
|
2116
|
+
updated_at
|
|
2117
|
+
) VALUES (
|
|
2118
|
+
$1, $2, $3, $4, COALESCE($5, 'pending'), $6, NOW(), NOW()
|
|
2119
|
+
)`, contractId, signature.signerName, (_a = signature.signerRole) !== null && _a !== void 0 ? _a : null, (_b = signature.signerEmail) !== null && _b !== void 0 ? _b : null, (_c = signature.status) !== null && _c !== void 0 ? _c : 'pending', (_d = signature.signedAt) !== null && _d !== void 0 ? _d : null);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
async replaceContractFinancialTerms(client, contractId, financialTerms) {
|
|
2123
|
+
var _a, _b, _c, _d;
|
|
2124
|
+
await client.$executeRawUnsafe(`UPDATE operations_contract_financial_term
|
|
2125
|
+
SET deleted_at = NOW(),
|
|
2126
|
+
updated_at = NOW()
|
|
2127
|
+
WHERE contract_id = $1
|
|
2128
|
+
AND deleted_at IS NULL`, contractId);
|
|
2129
|
+
for (const term of financialTerms !== null && financialTerms !== void 0 ? financialTerms : []) {
|
|
2130
|
+
await client.$executeRawUnsafe(`INSERT INTO operations_contract_financial_term (
|
|
2131
|
+
contract_id,
|
|
2132
|
+
term_type,
|
|
2133
|
+
label,
|
|
2134
|
+
amount,
|
|
2135
|
+
recurrence,
|
|
2136
|
+
due_day,
|
|
2137
|
+
notes,
|
|
2138
|
+
created_at,
|
|
2139
|
+
updated_at
|
|
2140
|
+
) VALUES (
|
|
2141
|
+
$1, COALESCE($2, 'value'), $3, $4, COALESCE($5, 'one_time'), $6, $7, NOW(), NOW()
|
|
2142
|
+
)`, contractId, (_a = term.termType) !== null && _a !== void 0 ? _a : 'value', term.label, term.amount, (_b = term.recurrence) !== null && _b !== void 0 ? _b : 'one_time', (_c = term.dueDay) !== null && _c !== void 0 ? _c : null, (_d = term.notes) !== null && _d !== void 0 ? _d : null);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
async replaceContractRevisions(client, contractId, revisions) {
|
|
2146
|
+
var _a, _b, _c, _d;
|
|
2147
|
+
await client.$executeRawUnsafe(`UPDATE operations_contract_revision
|
|
2148
|
+
SET deleted_at = NOW(),
|
|
2149
|
+
updated_at = NOW()
|
|
2150
|
+
WHERE contract_id = $1
|
|
2151
|
+
AND deleted_at IS NULL`, contractId);
|
|
2152
|
+
for (const revision of revisions !== null && revisions !== void 0 ? revisions : []) {
|
|
2153
|
+
await client.$executeRawUnsafe(`INSERT INTO operations_contract_revision (
|
|
2154
|
+
contract_id,
|
|
2155
|
+
revision_type,
|
|
2156
|
+
title,
|
|
2157
|
+
effective_date,
|
|
2158
|
+
status,
|
|
2159
|
+
summary,
|
|
2160
|
+
created_at,
|
|
2161
|
+
updated_at
|
|
2162
|
+
) VALUES (
|
|
2163
|
+
$1, COALESCE($2, 'revision'), $3, $4, COALESCE($5, 'draft'), $6, NOW(), NOW()
|
|
2164
|
+
)`, contractId, (_a = revision.revisionType) !== null && _a !== void 0 ? _a : 'revision', revision.title, (_b = revision.effectiveDate) !== null && _b !== void 0 ? _b : null, (_c = revision.status) !== null && _c !== void 0 ? _c : 'draft', (_d = revision.summary) !== null && _d !== void 0 ? _d : null);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
async replaceContractPdfDocument(client, contractId, document) {
|
|
2168
|
+
var _a;
|
|
2169
|
+
await client.$executeRawUnsafe(`UPDATE operations_contract_document
|
|
2170
|
+
SET is_current = false,
|
|
2171
|
+
updated_at = NOW()
|
|
2172
|
+
WHERE contract_id = $1
|
|
2173
|
+
AND deleted_at IS NULL
|
|
2174
|
+
AND document_type IN ('uploaded_pdf', 'generated_pdf')`, contractId);
|
|
2175
|
+
await client.$executeRawUnsafe(`INSERT INTO operations_contract_document (
|
|
2176
|
+
contract_id,
|
|
2177
|
+
document_type,
|
|
2178
|
+
file_name,
|
|
2179
|
+
mime_type,
|
|
2180
|
+
file_content_base64,
|
|
2181
|
+
is_current,
|
|
2182
|
+
notes,
|
|
2183
|
+
created_at,
|
|
2184
|
+
updated_at
|
|
2185
|
+
) VALUES (
|
|
2186
|
+
$1, 'uploaded_pdf', $2, $3, $4, true, $5, NOW(), NOW()
|
|
2187
|
+
)`, contractId, document.fileName, document.mimeType, document.fileContentBase64, (_a = document.notes) !== null && _a !== void 0 ? _a : null);
|
|
2188
|
+
}
|
|
2189
|
+
async insertContractHistory(client, contractId, actorUserId, action, note, metadataJson) {
|
|
2190
|
+
await client.$executeRawUnsafe(`INSERT INTO operations_contract_history (
|
|
2191
|
+
contract_id,
|
|
2192
|
+
actor_user_id,
|
|
2193
|
+
action,
|
|
2194
|
+
note,
|
|
2195
|
+
metadata_json,
|
|
2196
|
+
created_at
|
|
2197
|
+
) VALUES (
|
|
2198
|
+
$1, $2, $3, $4, $5, NOW()
|
|
2199
|
+
)`, contractId, actorUserId, action, note, metadataJson !== null && metadataJson !== void 0 ? metadataJson : null);
|
|
2200
|
+
}
|
|
2201
|
+
requireFields(input, required) {
|
|
2202
|
+
for (const field of required) {
|
|
2203
|
+
const value = input[field];
|
|
2204
|
+
if (value === undefined || value === null || value === '') {
|
|
2205
|
+
throw new common_1.BadRequestException(`Field "${field}" is required.`);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
buildIdFilter(ids, column, allowAll) {
|
|
2210
|
+
if (allowAll) {
|
|
2211
|
+
return { clause: '1 = 1', params: [] };
|
|
2212
|
+
}
|
|
2213
|
+
if (!ids.length) {
|
|
2214
|
+
return { clause: '1 = 0', params: [] };
|
|
2215
|
+
}
|
|
2216
|
+
return {
|
|
2217
|
+
clause: `${column} = ANY($1::int[])`,
|
|
2218
|
+
params: [ids],
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
uniqueNumbers(values) {
|
|
2222
|
+
return [
|
|
2223
|
+
...new Set(values.filter((value) => typeof value === 'number')),
|
|
2224
|
+
];
|
|
2225
|
+
}
|
|
2226
|
+
pushUpdate(updates, params, column, value) {
|
|
2227
|
+
if (value === undefined)
|
|
2228
|
+
return;
|
|
2229
|
+
params.push(value);
|
|
2230
|
+
updates.push(`${column} = $${params.length}`);
|
|
2231
|
+
}
|
|
2232
|
+
param(params, value) {
|
|
2233
|
+
params.push(value);
|
|
2234
|
+
return `$${params.length}`;
|
|
2235
|
+
}
|
|
2236
|
+
groupBy(items, key) {
|
|
2237
|
+
return items.reduce((accumulator, item) => {
|
|
2238
|
+
var _a;
|
|
2239
|
+
const groupKey = String(item[key]);
|
|
2240
|
+
(_a = accumulator[groupKey]) !== null && _a !== void 0 ? _a : (accumulator[groupKey] = []);
|
|
2241
|
+
accumulator[groupKey].push(item);
|
|
2242
|
+
return accumulator;
|
|
2243
|
+
}, {});
|
|
2244
|
+
}
|
|
2245
|
+
async queryRows(sql, params = []) {
|
|
2246
|
+
return this.prisma.$queryRawUnsafe(sql, ...params);
|
|
2247
|
+
}
|
|
2248
|
+
async querySingle(sql, params = []) {
|
|
2249
|
+
var _a;
|
|
2250
|
+
const rows = await this.queryRows(sql, params);
|
|
2251
|
+
return (_a = rows[0]) !== null && _a !== void 0 ? _a : null;
|
|
125
2252
|
}
|
|
126
|
-
|
|
127
|
-
return
|
|
2253
|
+
async execute(sql, params = []) {
|
|
2254
|
+
return this.prisma.$executeRawUnsafe(sql, ...params);
|
|
128
2255
|
}
|
|
129
2256
|
};
|
|
130
2257
|
exports.OperationsService = OperationsService;
|
|
131
2258
|
exports.OperationsService = OperationsService = __decorate([
|
|
132
|
-
(0, common_1.Injectable)()
|
|
2259
|
+
(0, common_1.Injectable)(),
|
|
2260
|
+
__metadata("design:paramtypes", [api_prisma_1.PrismaService,
|
|
2261
|
+
core_1.IntegrationDeveloperApiService])
|
|
133
2262
|
], OperationsService);
|
|
134
2263
|
//# sourceMappingURL=operations.service.js.map
|