@hed-hog/operations 0.0.295 → 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.
Files changed (126) hide show
  1. package/dist/operations.controller.d.ts +415 -0
  2. package/dist/operations.controller.d.ts.map +1 -0
  3. package/dist/operations.controller.js +333 -0
  4. package/dist/operations.controller.js.map +1 -0
  5. package/dist/operations.module.d.ts.map +1 -1
  6. package/dist/operations.module.js +4 -3
  7. package/dist/operations.module.js.map +1 -1
  8. package/dist/operations.service.d.ts +589 -153
  9. package/dist/operations.service.d.ts.map +1 -1
  10. package/dist/operations.service.js +2229 -100
  11. package/dist/operations.service.js.map +1 -1
  12. package/hedhog/data/menu.yaml +198 -251
  13. package/hedhog/data/role.yaml +23 -14
  14. package/hedhog/data/route.yaml +317 -143
  15. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +310 -0
  16. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +631 -0
  17. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +132 -0
  18. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +558 -0
  19. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +291 -0
  20. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +689 -0
  21. package/hedhog/frontend/app/_lib/api.ts.ejs +32 -0
  22. package/hedhog/frontend/app/_lib/hooks/use-operations-access.ts.ejs +44 -0
  23. package/hedhog/frontend/app/_lib/types.ts.ejs +360 -0
  24. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +129 -25
  25. package/hedhog/frontend/app/_lib/utils/forms.ts.ejs +14 -0
  26. package/hedhog/frontend/app/approvals/page.tsx.ejs +386 -147
  27. package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +11 -0
  28. package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +11 -0
  29. package/hedhog/frontend/app/collaborators/new/page.tsx.ejs +5 -0
  30. package/hedhog/frontend/app/collaborators/page.tsx.ejs +261 -0
  31. package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +11 -0
  32. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +11 -108
  33. package/hedhog/frontend/app/contracts/new/page.tsx.ejs +17 -0
  34. package/hedhog/frontend/app/contracts/page.tsx.ejs +262 -181
  35. package/hedhog/frontend/app/page.tsx.ejs +319 -177
  36. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +11 -0
  37. package/hedhog/frontend/app/projects/[id]/page.tsx.ejs +11 -936
  38. package/hedhog/frontend/app/projects/new/page.tsx.ejs +5 -0
  39. package/hedhog/frontend/app/projects/page.tsx.ejs +236 -1074
  40. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +418 -0
  41. package/hedhog/frontend/app/team/page.tsx.ejs +339 -0
  42. package/hedhog/frontend/app/time-off/page.tsx.ejs +328 -0
  43. package/hedhog/frontend/app/timesheets/page.tsx.ejs +636 -126
  44. package/hedhog/frontend/messages/en.json +648 -454
  45. package/hedhog/frontend/messages/pt.json +647 -454
  46. package/hedhog/table/operations_approval.yaml +49 -0
  47. package/hedhog/table/operations_approval_history.yaml +29 -0
  48. package/hedhog/table/{operations_employee.yaml → operations_collaborator.yaml} +67 -64
  49. package/hedhog/table/operations_collaborator_schedule_day.yaml +34 -0
  50. package/hedhog/table/operations_contract.yaml +100 -48
  51. package/hedhog/table/operations_contract_document.yaml +39 -0
  52. package/hedhog/table/operations_contract_financial_term.yaml +40 -0
  53. package/hedhog/table/operations_contract_history.yaml +27 -0
  54. package/hedhog/table/operations_contract_party.yaml +46 -0
  55. package/hedhog/table/operations_contract_revision.yaml +38 -0
  56. package/hedhog/table/operations_contract_signature.yaml +38 -0
  57. package/hedhog/table/operations_project.yaml +54 -50
  58. package/hedhog/table/{operations_allocation.yaml → operations_project_assignment.yaml} +55 -52
  59. package/hedhog/table/operations_schedule_adjustment_day.yaml +34 -0
  60. package/hedhog/table/operations_schedule_adjustment_request.yaml +53 -0
  61. package/hedhog/table/operations_time_off_request.yaml +57 -0
  62. package/hedhog/table/operations_timesheet.yaml +41 -36
  63. package/hedhog/table/operations_timesheet_entry.yaml +40 -50
  64. package/package.json +8 -7
  65. package/src/operations.controller.ts +182 -0
  66. package/src/operations.module.ts +22 -21
  67. package/src/operations.service.ts +3595 -137
  68. package/hedhog/data/operations_career_level.yaml +0 -102
  69. package/hedhog/data/operations_career_track.yaml +0 -8
  70. package/hedhog/data/operations_certification.yaml +0 -38
  71. package/hedhog/data/operations_evaluation_cycle.yaml +0 -18
  72. package/hedhog/data/operations_performance_criterion.yaml +0 -48
  73. package/hedhog/frontend/app/_components/allocation-calendar.tsx.ejs +0 -56
  74. package/hedhog/frontend/app/_components/kanban-board.tsx.ejs +0 -626
  75. package/hedhog/frontend/app/_components/timesheet-entry-dialog.tsx.ejs +0 -142
  76. package/hedhog/frontend/app/_lib/hooks/use-operations-data.ts.ejs +0 -41
  77. package/hedhog/frontend/app/_lib/hooks/use-operations-growth-data.ts.ejs +0 -63
  78. package/hedhog/frontend/app/_lib/mocks/allocations.mock.ts.ejs +0 -74
  79. package/hedhog/frontend/app/_lib/mocks/contracts.mock.ts.ejs +0 -74
  80. package/hedhog/frontend/app/_lib/mocks/operations-growth.mock.ts.ejs +0 -824
  81. package/hedhog/frontend/app/_lib/mocks/projects.mock.ts.ejs +0 -455
  82. package/hedhog/frontend/app/_lib/mocks/tasks.mock.ts.ejs +0 -117
  83. package/hedhog/frontend/app/_lib/mocks/timesheets.mock.ts.ejs +0 -84
  84. package/hedhog/frontend/app/_lib/mocks/users.mock.ts.ejs +0 -67
  85. package/hedhog/frontend/app/_lib/services/contracts.service.ts.ejs +0 -10
  86. package/hedhog/frontend/app/_lib/services/operations-growth.service.ts.ejs +0 -31
  87. package/hedhog/frontend/app/_lib/services/projects.service.ts.ejs +0 -10
  88. package/hedhog/frontend/app/_lib/services/tasks.service.ts.ejs +0 -10
  89. package/hedhog/frontend/app/_lib/services/timesheets.service.ts.ejs +0 -10
  90. package/hedhog/frontend/app/_lib/types/operations-growth.ts.ejs +0 -209
  91. package/hedhog/frontend/app/_lib/types/operations.ts.ejs +0 -156
  92. package/hedhog/frontend/app/_lib/utils/growth.ts.ejs +0 -62
  93. package/hedhog/frontend/app/_lib/utils/metrics.ts.ejs +0 -103
  94. package/hedhog/frontend/app/_lib/utils/status.ts.ejs +0 -80
  95. package/hedhog/frontend/app/allocations/page.tsx.ejs +0 -155
  96. package/hedhog/frontend/app/career/page.tsx.ejs +0 -143
  97. package/hedhog/frontend/app/certifications/page.tsx.ejs +0 -202
  98. package/hedhog/frontend/app/evaluations/page.tsx.ejs +0 -278
  99. package/hedhog/frontend/app/goals/page.tsx.ejs +0 -171
  100. package/hedhog/frontend/app/growth/page.tsx.ejs +0 -288
  101. package/hedhog/frontend/app/manager/page.tsx.ejs +0 -175
  102. package/hedhog/frontend/app/rewards/page.tsx.ejs +0 -196
  103. package/hedhog/frontend/app/tasks/page.tsx.ejs +0 -999
  104. package/hedhog/table/operations_calibration_item.yaml +0 -61
  105. package/hedhog/table/operations_calibration_session.yaml +0 -25
  106. package/hedhog/table/operations_career_level.yaml +0 -75
  107. package/hedhog/table/operations_career_track.yaml +0 -21
  108. package/hedhog/table/operations_certification.yaml +0 -48
  109. package/hedhog/table/operations_employee_certification.yaml +0 -43
  110. package/hedhog/table/operations_employee_connect.yaml +0 -61
  111. package/hedhog/table/operations_employee_evaluation.yaml +0 -113
  112. package/hedhog/table/operations_employee_evaluation_item.yaml +0 -39
  113. package/hedhog/table/operations_employee_profile.yaml +0 -80
  114. package/hedhog/table/operations_employee_skill_matrix.yaml +0 -30
  115. package/hedhog/table/operations_evaluation_cycle.yaml +0 -31
  116. package/hedhog/table/operations_goal.yaml +0 -67
  117. package/hedhog/table/operations_goal_progress.yaml +0 -31
  118. package/hedhog/table/operations_performance_criterion.yaml +0 -29
  119. package/hedhog/table/operations_promotion_readiness.yaml +0 -49
  120. package/hedhog/table/operations_promotion_recommendation.yaml +0 -63
  121. package/hedhog/table/operations_public_recognition.yaml +0 -46
  122. package/hedhog/table/operations_reward.yaml +0 -100
  123. package/hedhog/table/operations_score_event.yaml +0 -81
  124. package/hedhog/table/operations_task.yaml +0 -60
  125. package/src/operations-data.controller.ts +0 -54
  126. 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 operationalCollections = {
12
- contracts: [
13
- { id: 1, code: 'CTR-001', name: 'Northwind Retainer', status: 'active' },
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
- // TODO: Replace static collections with Prisma queries after the first schema apply.
72
- // TODO: Promotion must not be derived from score alone; score remains only a support signal.
73
- // TODO: Certifications are supporting evidence and do not guarantee promotion on their own.
74
- // TODO: Future readiness logic must consider individual, team, and org impact, plus time operating at the next level and calibration outcomes.
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
- module: 'operations',
78
- operationalCollections,
79
- growthCollections,
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
- listContracts() {
83
- return operationalCollections.contracts;
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
- getContractById(id) {
86
- var _a;
87
- return (_a = operationalCollections.contracts.find((item) => item.id === id)) !== null && _a !== void 0 ? _a : null;
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
- listProjects() {
90
- return operationalCollections.projects;
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
- getProjectById(id) {
93
- var _a;
94
- return (_a = operationalCollections.projects.find((item) => item.id === id)) !== null && _a !== void 0 ? _a : null;
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
- listAllocations() {
97
- return operationalCollections.allocations;
1866
+ ensureCollaborator(actor) {
1867
+ if (!actor.isCollaborator) {
1868
+ throw new common_1.ForbiddenException('Operations collaborator access is required.');
1869
+ }
98
1870
  }
99
- listTasks() {
100
- return operationalCollections.tasks;
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
- listTimesheets() {
103
- return operationalCollections.timesheets;
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
- listApprovals() {
106
- return operationalCollections.approvals;
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
- getGrowthDashboard() {
109
- return growthCollections.growth;
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
- listEvaluations() {
112
- return growthCollections.evaluations;
1983
+ mapBillingModelForCollaboratorType(collaboratorType) {
1984
+ return collaboratorType === 'freelancer'
1985
+ ? 'time_and_material'
1986
+ : 'monthly_retainer';
113
1987
  }
114
- listGoals() {
115
- return growthCollections.goals;
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
- listCertifications() {
118
- return growthCollections.certifications;
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
- listRewards() {
121
- return growthCollections.rewards;
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
- listCareerPaths() {
124
- return growthCollections.career;
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
- getManagerOverview() {
127
- return growthCollections.manager;
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