@hed-hog/operations 0.0.303 → 0.0.304

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +200 -43
  2. package/dist/controllers/operations-approvals.controller.d.ts +9 -0
  3. package/dist/controllers/operations-approvals.controller.d.ts.map +1 -0
  4. package/dist/controllers/operations-approvals.controller.js +64 -0
  5. package/dist/controllers/operations-approvals.controller.js.map +1 -0
  6. package/dist/controllers/operations-collaborators.controller.d.ts +223 -0
  7. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -0
  8. package/dist/controllers/operations-collaborators.controller.js +96 -0
  9. package/dist/controllers/operations-collaborators.controller.js.map +1 -0
  10. package/dist/controllers/operations-contracts.controller.d.ts +683 -0
  11. package/dist/controllers/operations-contracts.controller.d.ts.map +1 -0
  12. package/dist/controllers/operations-contracts.controller.js +198 -0
  13. package/dist/controllers/operations-contracts.controller.js.map +1 -0
  14. package/dist/controllers/operations-org-structure.controller.d.ts +108 -0
  15. package/dist/controllers/operations-org-structure.controller.d.ts.map +1 -0
  16. package/dist/controllers/operations-org-structure.controller.js +143 -0
  17. package/dist/controllers/operations-org-structure.controller.js.map +1 -0
  18. package/dist/controllers/operations-projects.controller.d.ts +169 -0
  19. package/dist/controllers/operations-projects.controller.d.ts.map +1 -0
  20. package/dist/controllers/operations-projects.controller.js +87 -0
  21. package/dist/controllers/operations-projects.controller.js.map +1 -0
  22. package/dist/controllers/operations-tasks.controller.d.ts +54 -0
  23. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -0
  24. package/dist/controllers/operations-tasks.controller.js +79 -0
  25. package/dist/controllers/operations-tasks.controller.js.map +1 -0
  26. package/dist/controllers/operations-timesheets.controller.d.ts +99 -0
  27. package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -0
  28. package/dist/controllers/operations-timesheets.controller.js +154 -0
  29. package/dist/controllers/operations-timesheets.controller.js.map +1 -0
  30. package/dist/dto/create-collaborator-type.dto.d.ts +10 -0
  31. package/dist/dto/create-collaborator-type.dto.d.ts.map +1 -0
  32. package/dist/dto/create-collaborator-type.dto.js +56 -0
  33. package/dist/dto/create-collaborator-type.dto.js.map +1 -0
  34. package/dist/dto/create-collaborator.dto.d.ts +42 -0
  35. package/dist/dto/create-collaborator.dto.d.ts.map +1 -0
  36. package/dist/dto/create-collaborator.dto.js +228 -0
  37. package/dist/dto/create-collaborator.dto.js.map +1 -0
  38. package/dist/dto/create-schedule-adjustment-request.dto.d.ts +17 -0
  39. package/dist/dto/create-schedule-adjustment-request.dto.d.ts.map +1 -0
  40. package/dist/dto/create-schedule-adjustment-request.dto.js +89 -0
  41. package/dist/dto/create-schedule-adjustment-request.dto.js.map +1 -0
  42. package/dist/dto/create-task.dto.d.ts +8 -0
  43. package/dist/dto/create-task.dto.d.ts.map +1 -0
  44. package/dist/dto/create-task.dto.js +50 -0
  45. package/dist/dto/create-task.dto.js.map +1 -0
  46. package/dist/dto/create-time-off-request.dto.d.ts +9 -0
  47. package/dist/dto/create-time-off-request.dto.d.ts.map +1 -0
  48. package/dist/dto/create-time-off-request.dto.js +54 -0
  49. package/dist/dto/create-time-off-request.dto.js.map +1 -0
  50. package/dist/dto/create-timesheet-entry.dto.d.ts +12 -0
  51. package/dist/dto/create-timesheet-entry.dto.d.ts.map +1 -0
  52. package/dist/dto/create-timesheet-entry.dto.js +75 -0
  53. package/dist/dto/create-timesheet-entry.dto.js.map +1 -0
  54. package/dist/dto/list-collaborator-types.dto.d.ts +4 -0
  55. package/dist/dto/list-collaborator-types.dto.d.ts.map +1 -0
  56. package/dist/dto/list-collaborator-types.dto.js +29 -0
  57. package/dist/dto/list-collaborator-types.dto.js.map +1 -0
  58. package/dist/dto/list-collaborators.dto.d.ts +8 -0
  59. package/dist/dto/list-collaborators.dto.d.ts.map +1 -0
  60. package/dist/dto/list-collaborators.dto.js +42 -0
  61. package/dist/dto/list-collaborators.dto.js.map +1 -0
  62. package/dist/dto/list-project-options.dto.d.ts +4 -0
  63. package/dist/dto/list-project-options.dto.d.ts.map +1 -0
  64. package/dist/dto/list-project-options.dto.js +8 -0
  65. package/dist/dto/list-project-options.dto.js.map +1 -0
  66. package/dist/dto/list-tasks.dto.d.ts +7 -0
  67. package/dist/dto/list-tasks.dto.d.ts.map +1 -0
  68. package/dist/dto/list-tasks.dto.js +38 -0
  69. package/dist/dto/list-tasks.dto.js.map +1 -0
  70. package/dist/dto/list-timesheet-entries.dto.d.ts +10 -0
  71. package/dist/dto/list-timesheet-entries.dto.d.ts.map +1 -0
  72. package/dist/dto/list-timesheet-entries.dto.js +54 -0
  73. package/dist/dto/list-timesheet-entries.dto.js.map +1 -0
  74. package/dist/dto/update-collaborator-type.dto.d.ts +4 -0
  75. package/dist/dto/update-collaborator-type.dto.d.ts.map +1 -0
  76. package/dist/dto/update-collaborator-type.dto.js +8 -0
  77. package/dist/dto/update-collaborator-type.dto.js.map +1 -0
  78. package/dist/dto/update-collaborator.dto.d.ts +4 -0
  79. package/dist/dto/update-collaborator.dto.d.ts.map +1 -0
  80. package/dist/dto/update-collaborator.dto.js +8 -0
  81. package/dist/dto/update-collaborator.dto.js.map +1 -0
  82. package/dist/dto/update-task.dto.d.ts +8 -0
  83. package/dist/dto/update-task.dto.d.ts.map +1 -0
  84. package/dist/dto/update-task.dto.js +51 -0
  85. package/dist/dto/update-task.dto.js.map +1 -0
  86. package/dist/operations.controller.d.ts +0 -1045
  87. package/dist/operations.controller.d.ts.map +1 -1
  88. package/dist/operations.controller.js +0 -429
  89. package/dist/operations.controller.js.map +1 -1
  90. package/dist/operations.module.d.ts.map +1 -1
  91. package/dist/operations.module.js +23 -2
  92. package/dist/operations.module.js.map +1 -1
  93. package/dist/operations.service.d.ts +373 -8
  94. package/dist/operations.service.d.ts.map +1 -1
  95. package/dist/operations.service.js +1598 -111
  96. package/dist/operations.service.js.map +1 -1
  97. package/dist/operations.service.spec.js +315 -1
  98. package/dist/operations.service.spec.js.map +1 -1
  99. package/dist/services/shared/operations-access.service.d.ts +16 -0
  100. package/dist/services/shared/operations-access.service.d.ts.map +1 -0
  101. package/dist/services/shared/operations-access.service.js +48 -0
  102. package/dist/services/shared/operations-access.service.js.map +1 -0
  103. package/hedhog/data/dashboard.yaml +20 -0
  104. package/hedhog/data/dashboard_component.yaml +274 -0
  105. package/hedhog/data/dashboard_component_role.yaml +174 -0
  106. package/hedhog/data/dashboard_item.yaml +299 -0
  107. package/hedhog/data/dashboard_role.yaml +20 -0
  108. package/hedhog/data/menu.yaml +30 -13
  109. package/hedhog/data/operations_collaborator_type.yaml +76 -0
  110. package/hedhog/data/route.yaml +183 -0
  111. package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +231 -0
  112. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +134 -49
  113. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +772 -93
  114. package/hedhog/frontend/app/_components/department-select-with-create.tsx.ejs +38 -16
  115. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +875 -632
  116. package/hedhog/frontend/app/_components/timesheet-task-create-sheet.tsx.ejs +213 -0
  117. package/hedhog/frontend/app/_lib/api.ts.ejs +30 -1
  118. package/hedhog/frontend/app/_lib/types.ts.ejs +142 -39
  119. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +33 -2
  120. package/hedhog/frontend/app/approvals/page.tsx.ejs +116 -98
  121. package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +502 -0
  122. package/hedhog/frontend/app/collaborators/page.tsx.ejs +109 -68
  123. package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +11 -9
  124. package/hedhog/frontend/app/departments/page.tsx.ejs +1 -1
  125. package/hedhog/frontend/app/projects/page.tsx.ejs +5 -1
  126. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +244 -120
  127. package/hedhog/frontend/app/team/page.tsx.ejs +15 -2
  128. package/hedhog/frontend/app/time-off/page.tsx.ejs +158 -82
  129. package/hedhog/frontend/app/timesheets/page.tsx.ejs +814 -357
  130. package/hedhog/frontend/messages/en.json +243 -51
  131. package/hedhog/frontend/messages/pt.json +458 -268
  132. package/hedhog/table/operations_collaborator.yaml +26 -13
  133. package/hedhog/table/operations_collaborator_equity_participation.yaml +43 -0
  134. package/hedhog/table/operations_collaborator_type.yaml +33 -0
  135. package/hedhog/table/operations_job_title.yaml +24 -0
  136. package/hedhog/table/operations_project_assignment.yaml +9 -0
  137. package/hedhog/table/operations_project_role.yaml +39 -0
  138. package/hedhog/table/operations_task.yaml +30 -0
  139. package/hedhog/table/operations_timesheet_entry.yaml +12 -0
  140. package/package.json +6 -6
  141. package/src/controllers/operations-approvals.controller.ts +24 -0
  142. package/src/controllers/operations-collaborators.controller.ts +60 -0
  143. package/src/controllers/operations-contracts.controller.ts +138 -0
  144. package/src/controllers/operations-org-structure.controller.ts +92 -0
  145. package/src/controllers/operations-projects.controller.ts +50 -0
  146. package/src/controllers/operations-tasks.controller.ts +52 -0
  147. package/src/controllers/operations-timesheets.controller.ts +100 -0
  148. package/src/dto/create-collaborator-type.dto.ts +43 -0
  149. package/src/dto/create-collaborator.dto.ts +223 -0
  150. package/src/dto/create-schedule-adjustment-request.dto.ts +91 -0
  151. package/src/dto/create-task.dto.ts +35 -0
  152. package/src/dto/create-time-off-request.dto.ts +53 -0
  153. package/src/dto/create-timesheet-entry.dto.ts +67 -0
  154. package/src/dto/list-collaborator-types.dto.ts +15 -0
  155. package/src/dto/list-collaborators.dto.ts +30 -0
  156. package/src/dto/list-project-options.dto.ts +3 -0
  157. package/src/dto/list-tasks.dto.ts +25 -0
  158. package/src/dto/list-timesheet-entries.dto.ts +40 -0
  159. package/src/dto/update-collaborator-type.dto.ts +3 -0
  160. package/src/dto/update-collaborator.dto.ts +3 -0
  161. package/src/dto/update-task.dto.ts +36 -0
  162. package/src/operations.controller.ts +1 -278
  163. package/src/operations.module.ts +23 -2
  164. package/src/operations.service.spec.ts +450 -0
  165. package/src/operations.service.ts +4641 -2163
  166. package/src/services/shared/operations-access.service.ts +52 -0
@@ -14,9 +14,11 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
14
14
  var OperationsService_1;
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.OperationsService = void 0;
17
+ const api_locale_1 = require("@hed-hog/api-locale");
17
18
  const api_prisma_1 = require("@hed-hog/api-prisma");
18
19
  const core_1 = require("@hed-hog/core");
19
20
  const common_1 = require("@nestjs/common");
21
+ const operations_access_service_1 = require("./services/shared/operations-access.service");
20
22
  const COLLABORATOR_ROLE = 'admin-operations-collaborator';
21
23
  const SUPERVISOR_ROLE = 'admin-operations-supervisor';
22
24
  const DIRECTOR_ROLE = 'admin-operations-director';
@@ -105,15 +107,238 @@ const FINANCIAL_TERM_TYPE_VALUES = ['value', 'payment', 'revenue', 'fine', 'othe
105
107
  const RECURRENCE_VALUES = ['one_time', 'monthly', 'quarterly', 'yearly', 'other'];
106
108
  const REVISION_TYPE_VALUES = ['amendment', 'renewal', 'revision', 'addendum', 'other'];
107
109
  const REVISION_STATUS_VALUES = ['draft', 'active', 'completed', 'cancelled'];
110
+ const TASK_STATUS_VALUES = ['active', 'completed', 'archived'];
108
111
  let OperationsService = OperationsService_1 = class OperationsService {
109
- constructor(prisma, aiService, integrationApi, fileService, settingService) {
112
+ constructor(prisma, aiService, integrationApi, fileService, settingService, accessService, localeService) {
110
113
  this.prisma = prisma;
111
114
  this.aiService = aiService;
112
115
  this.integrationApi = integrationApi;
113
116
  this.fileService = fileService;
114
117
  this.settingService = settingService;
118
+ this.accessService = accessService;
119
+ this.localeService = localeService;
115
120
  this.logger = new common_1.Logger(OperationsService_1.name);
116
121
  }
122
+ normalizeLocaleCode(locale) {
123
+ var _a, _b;
124
+ const rawLocale = String(locale !== null && locale !== void 0 ? locale : '').trim();
125
+ if (!rawLocale) {
126
+ return null;
127
+ }
128
+ return (((_b = (_a = rawLocale.split(',')[0]) === null || _a === void 0 ? void 0 : _a.split('-')[0]) === null || _b === void 0 ? void 0 : _b.trim().toLowerCase()) || null);
129
+ }
130
+ async resolvePreferredLocaleId(client = this.prisma) {
131
+ var _a, _b;
132
+ const candidateCodes = [
133
+ this.normalizeLocaleCode((0, core_1.getLocaleFromContext)()),
134
+ 'en',
135
+ 'pt',
136
+ ].filter((code, index, values) => Boolean(code) && values.indexOf(code) === index);
137
+ for (const code of candidateCodes) {
138
+ const localeRecord = this.localeService
139
+ ? await this.localeService.getByCode(code)
140
+ : null;
141
+ if (localeRecord === null || localeRecord === void 0 ? void 0 : localeRecord.id) {
142
+ return Number(localeRecord.id);
143
+ }
144
+ }
145
+ const fallbackLocales = (await client.$queryRawUnsafe(`SELECT id
146
+ FROM locale
147
+ ORDER BY CASE WHEN enabled = true THEN 0 ELSE 1 END ASC,
148
+ CASE
149
+ WHEN code = 'en' THEN 0
150
+ WHEN code = 'pt' THEN 1
151
+ ELSE 2
152
+ END ASC,
153
+ id ASC
154
+ LIMIT 1`));
155
+ return (_b = (_a = fallbackLocales[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null;
156
+ }
157
+ async listCollaboratorTypes(userId, filters) {
158
+ const actor = await this.getActorContext(userId);
159
+ this.ensureCollaborator(actor);
160
+ return this.queryRows(`SELECT ct.id,
161
+ ct.slug,
162
+ ct.name,
163
+ ct.description,
164
+ ct.category,
165
+ ct.is_active AS "isActive",
166
+ ct.sort_order AS "sortOrder",
167
+ CASE
168
+ WHEN ct.deleted_at IS NULL AND ct.is_active THEN 'active'
169
+ ELSE 'inactive'
170
+ END AS status,
171
+ COUNT(DISTINCT c.id)::int AS "collaboratorCount",
172
+ ct.created_at AS "createdAt",
173
+ ct.updated_at AS "updatedAt"
174
+ FROM operations_collaborator_type ct
175
+ LEFT JOIN operations_collaborator c
176
+ ON c.deleted_at IS NULL
177
+ AND c.collaborator_type_id = ct.id
178
+ WHERE ($1::boolean = false OR (ct.deleted_at IS NULL AND ct.is_active = true))
179
+ GROUP BY ct.id
180
+ ORDER BY CASE
181
+ WHEN ct.deleted_at IS NULL AND ct.is_active THEN 0
182
+ ELSE 1
183
+ END ASC,
184
+ ct.sort_order ASC,
185
+ ct.name ASC`, [Boolean(filters === null || filters === void 0 ? void 0 : filters.active)]);
186
+ }
187
+ async createCollaboratorType(userId, data) {
188
+ const actor = await this.getActorContext(userId);
189
+ this.ensureDirector(actor);
190
+ const name = this.normalizeOptionalText(data.name);
191
+ if (!name) {
192
+ throw new common_1.BadRequestException('Collaborator type name is required.');
193
+ }
194
+ return this.prisma.$transaction(async (tx) => {
195
+ var _a, _b;
196
+ await this.assertCollaboratorTypeNameAvailable(tx, name);
197
+ const nextSlug = await this.buildCollaboratorTypeSlug(tx, name, data.slug);
198
+ const nextIsActive = (_a = data.isActive) !== null && _a !== void 0 ? _a : (data.status ? data.status === 'active' : true);
199
+ const created = (await tx.$queryRawUnsafe(`INSERT INTO operations_collaborator_type (
200
+ slug,
201
+ name,
202
+ description,
203
+ category,
204
+ is_active,
205
+ sort_order,
206
+ deleted_at,
207
+ created_at,
208
+ updated_at
209
+ ) VALUES (
210
+ $1, $2, $3, $4, $5, $6,
211
+ CASE WHEN $5::boolean THEN NULL ELSE NOW() END,
212
+ NOW(), NOW()
213
+ )
214
+ RETURNING id`, nextSlug, name, this.normalizeOptionalText(data.description), this.normalizeOptionalText(data.category), nextIsActive, Number.isFinite(Number(data.sortOrder)) ? Number(data.sortOrder) : 0));
215
+ const createdCollaboratorTypeId = (_b = created[0]) === null || _b === void 0 ? void 0 : _b.id;
216
+ if (!createdCollaboratorTypeId) {
217
+ throw new common_1.BadRequestException('Unable to create the collaborator type.');
218
+ }
219
+ return this.getCollaboratorTypeById(tx, createdCollaboratorTypeId, true);
220
+ });
221
+ }
222
+ async updateCollaboratorType(userId, collaboratorTypeId, data) {
223
+ const actor = await this.getActorContext(userId);
224
+ this.ensureDirector(actor);
225
+ return this.prisma.$transaction(async (tx) => {
226
+ var _a, _b, _c, _d, _e;
227
+ const current = await this.getCollaboratorTypeById(tx, collaboratorTypeId, true);
228
+ const nextName = data.name !== undefined
229
+ ? this.normalizeOptionalText(data.name)
230
+ : current.name;
231
+ if (!nextName) {
232
+ throw new common_1.BadRequestException('Collaborator type name is required.');
233
+ }
234
+ if (String(nextName).toLowerCase() !== String(current.name).toLowerCase()) {
235
+ await this.assertCollaboratorTypeNameAvailable(tx, nextName, collaboratorTypeId);
236
+ }
237
+ const nextSlug = await this.buildCollaboratorTypeSlug(tx, nextName, data.slug !== undefined ? data.slug : current.slug, collaboratorTypeId);
238
+ const nextDescription = data.description !== undefined
239
+ ? this.normalizeOptionalText(data.description)
240
+ : ((_a = current.description) !== null && _a !== void 0 ? _a : null);
241
+ const nextCategory = data.category !== undefined
242
+ ? this.normalizeOptionalText(data.category)
243
+ : ((_b = current.category) !== null && _b !== void 0 ? _b : null);
244
+ const nextSortOrder = data.sortOrder !== undefined && Number.isFinite(Number(data.sortOrder))
245
+ ? Number(data.sortOrder)
246
+ : ((_c = current.sortOrder) !== null && _c !== void 0 ? _c : 0);
247
+ const nextIsActive = (_d = data.isActive) !== null && _d !== void 0 ? _d : (data.status ? data.status === 'active' : ((_e = current.isActive) !== null && _e !== void 0 ? _e : true));
248
+ await tx.$executeRawUnsafe(`UPDATE operations_collaborator_type
249
+ SET slug = $1,
250
+ name = $2,
251
+ description = $3,
252
+ category = $4,
253
+ is_active = $5,
254
+ sort_order = $6,
255
+ deleted_at = CASE
256
+ WHEN $5::boolean THEN NULL
257
+ ELSE COALESCE(deleted_at, NOW())
258
+ END,
259
+ updated_at = NOW()
260
+ WHERE id = $7`, nextSlug, nextName, nextDescription, nextCategory, nextIsActive, nextSortOrder, collaboratorTypeId);
261
+ return this.getCollaboratorTypeById(tx, collaboratorTypeId, true);
262
+ });
263
+ }
264
+ async listProjectRoles(userId) {
265
+ const actor = await this.getActorContext(userId);
266
+ this.ensureCollaborator(actor);
267
+ const localeId = await this.resolvePreferredLocaleId();
268
+ return this.queryRows(`SELECT pr.id,
269
+ pr.slug,
270
+ pr.code,
271
+ COALESCE(prl.name, pr.code, pr.slug) AS name,
272
+ prl.description,
273
+ pr.is_active AS "isActive",
274
+ pr.sort_order AS "sortOrder",
275
+ pr.created_at AS "createdAt",
276
+ pr.updated_at AS "updatedAt"
277
+ FROM operations_project_role pr
278
+ LEFT JOIN LATERAL (
279
+ SELECT pr_locale.name,
280
+ pr_locale.description
281
+ FROM operations_project_role_locale pr_locale
282
+ WHERE pr_locale.operations_project_role_id = pr.id
283
+ ORDER BY CASE
284
+ WHEN $1::int IS NOT NULL AND pr_locale.locale_id = $1 THEN 0
285
+ ELSE 1
286
+ END ASC,
287
+ pr_locale.id ASC
288
+ LIMIT 1
289
+ ) prl ON TRUE
290
+ WHERE pr.deleted_at IS NULL
291
+ ORDER BY pr.sort_order ASC,
292
+ COALESCE(prl.name, pr.code, pr.slug) ASC`, [localeId]);
293
+ }
294
+ async createProjectRole(userId, data) {
295
+ const actor = await this.getActorContext(userId);
296
+ this.ensureDirector(actor);
297
+ const name = this.normalizeOptionalText(data.name);
298
+ if (!name) {
299
+ throw new common_1.BadRequestException('Project role name is required.');
300
+ }
301
+ const description = this.normalizeOptionalText(data.description);
302
+ return this.prisma.$transaction(async (tx) => {
303
+ var _a, _b, _c, _d;
304
+ await this.assertProjectRoleNameAvailable(tx, name);
305
+ const normalizedCode = (_b = (_a = this.normalizeOptionalText(data.code)) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : null;
306
+ if (normalizedCode) {
307
+ await this.assertProjectRoleCodeAvailable(tx, normalizedCode);
308
+ }
309
+ const localeId = await this.resolvePreferredLocaleId(tx);
310
+ if (!localeId) {
311
+ throw new common_1.BadRequestException('No locale is configured to create project roles.');
312
+ }
313
+ const created = (await tx.$queryRawUnsafe(`INSERT INTO operations_project_role (
314
+ slug,
315
+ code,
316
+ is_active,
317
+ sort_order,
318
+ deleted_at,
319
+ created_at,
320
+ updated_at
321
+ ) VALUES (
322
+ $1, $2, $3, $4,
323
+ CASE WHEN $3::boolean THEN NULL ELSE NOW() END,
324
+ NOW(), NOW()
325
+ )
326
+ RETURNING id`, await this.generateUniqueProjectRoleSlug(tx, name), normalizedCode, (_c = data.isActive) !== null && _c !== void 0 ? _c : true, Number.isFinite(Number(data.sortOrder)) ? Number(data.sortOrder) : 0));
327
+ const createdProjectRoleId = (_d = created[0]) === null || _d === void 0 ? void 0 : _d.id;
328
+ if (!createdProjectRoleId) {
329
+ throw new common_1.BadRequestException('Unable to create the project role.');
330
+ }
331
+ await tx.$executeRawUnsafe(`INSERT INTO operations_project_role_locale (
332
+ operations_project_role_id,
333
+ locale_id,
334
+ name,
335
+ description,
336
+ created_at,
337
+ updated_at
338
+ ) VALUES ($1, $2, $3, $4, NOW(), NOW())`, createdProjectRoleId, localeId, name, description);
339
+ return this.getProjectRoleById(tx, createdProjectRoleId, true);
340
+ });
341
+ }
117
342
  async getDashboard(userId) {
118
343
  var _a, _b, _c, _d, _e, _f, _g;
119
344
  const actor = await this.getActorContext(userId);
@@ -192,13 +417,16 @@ let OperationsService = OperationsService_1 = class OperationsService {
192
417
  c.user_id AS "userId",
193
418
  c.person_id AS "personId",
194
419
  c.code,
195
- c.collaborator_type AS "collaboratorType",
420
+ c.collaborator_type_id AS "collaboratorTypeId",
421
+ collaborator_type.slug AS "collaboratorTypeSlug",
422
+ collaborator_type.name AS "collaboratorType",
196
423
  COALESCE(NULLIF(c.display_name, ''), person_record.name) AS "displayName",
197
424
  person_record.name AS "personName",
198
425
  person_record.avatar_id AS "personAvatarId",
199
426
  c.department_id AS "departmentId",
200
427
  COALESCE(NULLIF(department_record.name, ''), NULLIF(c.department, '')) AS "department",
201
- c.title,
428
+ c.job_title_id AS "jobTitleId",
429
+ COALESCE(NULLIF(job_title_record.name, ''), NULLIF(c.title, '')) AS "title",
202
430
  c.level_label AS "levelLabel",
203
431
  c.weekly_capacity_hours AS "weeklyCapacityHours",
204
432
  c.status,
@@ -209,13 +437,20 @@ let OperationsService = OperationsService_1 = class OperationsService {
209
437
  s.display_name AS "supervisorName",
210
438
  hiring_contract.id AS "contractId",
211
439
  hiring_contract.status AS "contractStatus",
440
+ hiring_contract.budget_amount AS "compensationAmount",
212
441
  COUNT(DISTINCT pa.id)::int AS "activeAssignments"
213
442
  FROM operations_collaborator c
214
443
  LEFT JOIN person person_record
215
444
  ON person_record.id = c.person_id
445
+ LEFT JOIN operations_collaborator_type collaborator_type
446
+ ON collaborator_type.id = c.collaborator_type_id
447
+ AND collaborator_type.deleted_at IS NULL
216
448
  LEFT JOIN operations_department department_record
217
449
  ON department_record.id = c.department_id
218
450
  AND department_record.deleted_at IS NULL
451
+ LEFT JOIN operations_job_title job_title_record
452
+ ON job_title_record.id = c.job_title_id
453
+ AND job_title_record.deleted_at IS NULL
219
454
  LEFT JOIN operations_collaborator s
220
455
  ON s.id = c.supervisor_collaborator_id
221
456
  LEFT JOIN operations_project_assignment pa
@@ -223,7 +458,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
223
458
  AND pa.deleted_at IS NULL
224
459
  AND pa.status IN ('planned', 'active')
225
460
  LEFT JOIN LATERAL (
226
- SELECT oc.id, oc.status
461
+ SELECT oc.id, oc.status, oc.budget_amount
227
462
  FROM operations_contract oc
228
463
  WHERE oc.related_collaborator_id = c.id
229
464
  AND oc.deleted_at IS NULL
@@ -233,7 +468,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
233
468
  ) hiring_contract ON TRUE
234
469
  WHERE c.deleted_at IS NULL
235
470
  AND ${filter.clause}
236
- GROUP BY c.id, person_record.id, department_record.id, s.id, hiring_contract.id, hiring_contract.status
471
+ GROUP BY c.id, person_record.id, collaborator_type.id, department_record.id, job_title_record.id, s.id, hiring_contract.id, hiring_contract.status, hiring_contract.budget_amount
237
472
  ORDER BY COALESCE(NULLIF(c.display_name, ''), person_record.name) ASC`, filter.params);
238
473
  }
239
474
  async getMyCollaborator(userId) {
@@ -273,13 +508,16 @@ let OperationsService = OperationsService_1 = class OperationsService {
273
508
  c.user_id AS "userId",
274
509
  c.person_id AS "personId",
275
510
  c.code,
511
+ c.collaborator_type_id AS "collaboratorTypeId",
512
+ collaborator_type.slug AS "collaboratorTypeSlug",
513
+ collaborator_type.name AS "collaboratorType",
276
514
  COALESCE(NULLIF(c.display_name, ''), person_record.name) AS "displayName",
277
515
  person_record.name AS "personName",
278
516
  person_record.avatar_id AS "personAvatarId",
279
- c.collaborator_type AS "collaboratorType",
280
517
  c.department_id AS "departmentId",
281
518
  COALESCE(NULLIF(department_record.name, ''), NULLIF(c.department, '')) AS "department",
282
- c.title,
519
+ c.job_title_id AS "jobTitleId",
520
+ COALESCE(NULLIF(job_title_record.name, ''), NULLIF(c.title, '')) AS "title",
283
521
  c.status,
284
522
  COUNT(DISTINCT pa.id)::int AS "activeAssignments",
285
523
  COUNT(DISTINCT a.id) FILTER (WHERE a.status = 'pending')::int AS "pendingApprovals",
@@ -288,9 +526,15 @@ let OperationsService = OperationsService_1 = class OperationsService {
288
526
  FROM operations_collaborator c
289
527
  LEFT JOIN person person_record
290
528
  ON person_record.id = c.person_id
529
+ LEFT JOIN operations_collaborator_type collaborator_type
530
+ ON collaborator_type.id = c.collaborator_type_id
531
+ AND collaborator_type.deleted_at IS NULL
291
532
  LEFT JOIN operations_department department_record
292
533
  ON department_record.id = c.department_id
293
534
  AND department_record.deleted_at IS NULL
535
+ LEFT JOIN operations_job_title job_title_record
536
+ ON job_title_record.id = c.job_title_id
537
+ AND job_title_record.deleted_at IS NULL
294
538
  LEFT JOIN operations_project_assignment pa
295
539
  ON pa.collaborator_id = c.id
296
540
  AND pa.deleted_at IS NULL
@@ -308,7 +552,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
308
552
  AND sar.deleted_at IS NULL
309
553
  AND sar.status = 'submitted'
310
554
  WHERE c.deleted_at IS NULL AND ${teamFilter.clause}
311
- GROUP BY c.id, person_record.id, department_record.id
555
+ GROUP BY c.id, person_record.id, collaborator_type.id, department_record.id, job_title_record.id
312
556
  ORDER BY COALESCE(NULLIF(c.display_name, ''), person_record.name) ASC`, teamFilter.params);
313
557
  const [teamProjects, pendingApprovalQueue, pendingTimeOffRequests, pendingScheduleAdjustmentRequests] = await Promise.all([
314
558
  this.queryRows(`SELECT p.id,
@@ -459,26 +703,36 @@ let OperationsService = OperationsService_1 = class OperationsService {
459
703
  ? await this.getPersonById(Number(data.personId))
460
704
  : null;
461
705
  const resolvedDisplayName = (_d = (_b = (_a = resolvedPerson === null || resolvedPerson === void 0 ? void 0 : resolvedPerson.name) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : (_c = data.displayName) === null || _c === void 0 ? void 0 : _c.trim()) !== null && _d !== void 0 ? _d : '';
706
+ const normalizedStatus = data.status === 'draft' ? 'active' : data.status;
462
707
  if (!resolvedDisplayName) {
463
708
  throw new common_1.BadRequestException('Field "personId" is required.');
464
709
  }
465
710
  const collaboratorId = await this.prisma.$transaction(async (tx) => {
466
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
711
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3;
467
712
  const normalizedCode = String((_a = data.code) !== null && _a !== void 0 ? _a : '').trim() ||
468
713
  (await this.generateCollaboratorCode(tx));
469
714
  const resolvedDepartment = await this.resolveDepartmentReference(tx, {
470
715
  departmentId: (_b = data.departmentId) !== null && _b !== void 0 ? _b : null,
471
716
  departmentName: data.department,
472
717
  });
718
+ const resolvedJobTitle = await this.resolveJobTitleReference(tx, {
719
+ jobTitleId: (_c = data.jobTitleId) !== null && _c !== void 0 ? _c : null,
720
+ jobTitleName: data.title,
721
+ });
722
+ const resolvedCollaboratorType = await this.resolveCollaboratorTypeReference(tx, {
723
+ collaboratorTypeId: (_d = data.collaboratorTypeId) !== null && _d !== void 0 ? _d : null,
724
+ collaboratorTypeSlug: (_f = (_e = data.collaboratorTypeSlug) !== null && _e !== void 0 ? _e : data.collaboratorType) !== null && _f !== void 0 ? _f : null,
725
+ });
473
726
  const created = (await tx.$queryRawUnsafe(`INSERT INTO operations_collaborator (
474
727
  user_id,
475
728
  person_id,
476
729
  supervisor_collaborator_id,
477
730
  code,
478
- collaborator_type,
731
+ collaborator_type_id,
479
732
  display_name,
480
733
  department,
481
734
  department_id,
735
+ job_title_id,
482
736
  title,
483
737
  level_label,
484
738
  weekly_capacity_hours,
@@ -489,26 +743,26 @@ let OperationsService = OperationsService_1 = class OperationsService {
489
743
  created_at,
490
744
  updated_at
491
745
  ) VALUES (
492
- $1, $2, $3, $4,
493
- $5::operations_collaborator_collaborator_type_7dd7b0ada2_enum,
494
- $6, $7, $8, $9, $10, $11,
495
- $12::operations_collaborator_status_ef779877d4_enum,
496
- $13::date, $14::date, $15, NOW(), NOW()
746
+ $1, $2, $3, $4, $5,
747
+ $6, $7, $8, $9, $10, $11, $12,
748
+ $13::operations_collaborator_status_ef779877d4_enum,
749
+ $14::date, $15::date, $16, NOW(), NOW()
497
750
  )
498
- RETURNING id`, (_c = data.userId) !== null && _c !== void 0 ? _c : null, (_d = resolvedPerson === null || resolvedPerson === void 0 ? void 0 : resolvedPerson.id) !== null && _d !== void 0 ? _d : null, (_e = data.supervisorCollaboratorId) !== null && _e !== void 0 ? _e : null, normalizedCode, (_f = data.collaboratorType) !== null && _f !== void 0 ? _f : 'other', resolvedDisplayName, (_g = resolvedDepartment === null || resolvedDepartment === void 0 ? void 0 : resolvedDepartment.name) !== null && _g !== void 0 ? _g : null, (_h = resolvedDepartment === null || resolvedDepartment === void 0 ? void 0 : resolvedDepartment.id) !== null && _h !== void 0 ? _h : null, (_j = data.title) !== null && _j !== void 0 ? _j : null, (_k = data.levelLabel) !== null && _k !== void 0 ? _k : null, (_l = data.weeklyCapacityHours) !== null && _l !== void 0 ? _l : null, (_m = data.status) !== null && _m !== void 0 ? _m : 'active', (_o = data.joinedAt) !== null && _o !== void 0 ? _o : null, (_p = data.leftAt) !== null && _p !== void 0 ? _p : null, (_q = data.notes) !== null && _q !== void 0 ? _q : null));
499
- const createdCollaboratorId = (_r = created[0]) === null || _r === void 0 ? void 0 : _r.id;
751
+ RETURNING id`, (_g = data.userId) !== null && _g !== void 0 ? _g : null, (_h = resolvedPerson === null || resolvedPerson === void 0 ? void 0 : resolvedPerson.id) !== null && _h !== void 0 ? _h : null, (_j = data.supervisorCollaboratorId) !== null && _j !== void 0 ? _j : null, normalizedCode, (_k = resolvedCollaboratorType === null || resolvedCollaboratorType === void 0 ? void 0 : resolvedCollaboratorType.id) !== null && _k !== void 0 ? _k : null, resolvedDisplayName, (_l = resolvedDepartment === null || resolvedDepartment === void 0 ? void 0 : resolvedDepartment.name) !== null && _l !== void 0 ? _l : null, (_m = resolvedDepartment === null || resolvedDepartment === void 0 ? void 0 : resolvedDepartment.id) !== null && _m !== void 0 ? _m : null, (_o = resolvedJobTitle === null || resolvedJobTitle === void 0 ? void 0 : resolvedJobTitle.id) !== null && _o !== void 0 ? _o : null, (_p = resolvedJobTitle === null || resolvedJobTitle === void 0 ? void 0 : resolvedJobTitle.name) !== null && _p !== void 0 ? _p : this.normalizeOptionalText(data.title), (_q = data.levelLabel) !== null && _q !== void 0 ? _q : null, (_r = data.weeklyCapacityHours) !== null && _r !== void 0 ? _r : null, normalizedStatus !== null && normalizedStatus !== void 0 ? normalizedStatus : 'active', (_s = data.joinedAt) !== null && _s !== void 0 ? _s : null, (_t = data.leftAt) !== null && _t !== void 0 ? _t : null, (_u = data.notes) !== null && _u !== void 0 ? _u : null));
752
+ const createdCollaboratorId = (_v = created[0]) === null || _v === void 0 ? void 0 : _v.id;
500
753
  await this.replaceCollaboratorScheduleDays(tx, createdCollaboratorId, data.weeklySchedule);
754
+ await this.replaceCollaboratorEquityParticipation(tx, createdCollaboratorId, data.equityParticipation);
501
755
  if (data.autoGenerateContractDraft !== false) {
502
756
  await this.createHiringContractDraft(tx, actor.userId, {
503
757
  collaboratorId: createdCollaboratorId,
504
758
  collaboratorCode: normalizedCode,
505
759
  displayName: resolvedDisplayName,
506
- collaboratorType: (_s = data.collaboratorType) !== null && _s !== void 0 ? _s : 'other',
507
- supervisorCollaboratorId: (_t = data.supervisorCollaboratorId) !== null && _t !== void 0 ? _t : null,
508
- startDate: (_u = data.joinedAt) !== null && _u !== void 0 ? _u : null,
509
- weeklyCapacityHours: (_v = data.weeklyCapacityHours) !== null && _v !== void 0 ? _v : null,
510
- compensationAmount: (_w = data.compensationAmount) !== null && _w !== void 0 ? _w : null,
511
- description: (_y = (_x = data.contractDescription) !== null && _x !== void 0 ? _x : data.notes) !== null && _y !== void 0 ? _y : null,
760
+ collaboratorType: ((_x = (_w = resolvedCollaboratorType === null || resolvedCollaboratorType === void 0 ? void 0 : resolvedCollaboratorType.slug) !== null && _w !== void 0 ? _w : data.collaboratorType) !== null && _x !== void 0 ? _x : 'other'),
761
+ supervisorCollaboratorId: (_y = data.supervisorCollaboratorId) !== null && _y !== void 0 ? _y : null,
762
+ startDate: (_z = data.joinedAt) !== null && _z !== void 0 ? _z : null,
763
+ weeklyCapacityHours: (_0 = data.weeklyCapacityHours) !== null && _0 !== void 0 ? _0 : null,
764
+ compensationAmount: (_1 = data.compensationAmount) !== null && _1 !== void 0 ? _1 : null,
765
+ description: (_3 = (_2 = data.contractDescription) !== null && _2 !== void 0 ? _2 : data.notes) !== null && _3 !== void 0 ? _3 : null,
512
766
  });
513
767
  }
514
768
  return createdCollaboratorId;
@@ -526,30 +780,46 @@ let OperationsService = OperationsService_1 = class OperationsService {
526
780
  ? await this.getPersonById(Number(data.personId))
527
781
  : null;
528
782
  const resolvedDisplayName = (_d = (_b = (_a = resolvedPerson === null || resolvedPerson === void 0 ? void 0 : resolvedPerson.name) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : (_c = data.displayName) === null || _c === void 0 ? void 0 : _c.trim()) !== null && _d !== void 0 ? _d : undefined;
783
+ const normalizedStatus = data.status === 'draft' ? 'active' : data.status;
529
784
  this.pushUpdate(updates, params, 'user_id', data.userId);
530
785
  this.pushUpdate(updates, params, 'person_id', data.personId);
531
786
  this.pushUpdate(updates, params, 'supervisor_collaborator_id', data.supervisorCollaboratorId);
532
787
  this.pushUpdate(updates, params, 'code', (_e = data.code) === null || _e === void 0 ? void 0 : _e.trim());
533
- this.pushUpdate(updates, params, 'collaborator_type', data.collaboratorType, 'operations_collaborator_collaborator_type_7dd7b0ada2_enum');
534
788
  if (data.personId !== undefined || data.displayName !== undefined) {
535
789
  this.pushUpdate(updates, params, 'display_name', resolvedDisplayName !== null && resolvedDisplayName !== void 0 ? resolvedDisplayName : null);
536
790
  }
537
- this.pushUpdate(updates, params, 'title', data.title);
538
791
  this.pushUpdate(updates, params, 'level_label', data.levelLabel);
539
792
  this.pushUpdate(updates, params, 'weekly_capacity_hours', data.weeklyCapacityHours);
540
- this.pushUpdate(updates, params, 'status', data.status, 'operations_collaborator_status_ef779877d4_enum');
793
+ this.pushUpdate(updates, params, 'status', normalizedStatus, 'operations_collaborator_status_ef779877d4_enum');
541
794
  this.pushUpdate(updates, params, 'joined_at', data.joinedAt, 'date');
542
795
  this.pushUpdate(updates, params, 'left_at', data.leftAt, 'date');
543
796
  this.pushUpdate(updates, params, 'notes', data.notes);
544
797
  await this.prisma.$transaction(async (tx) => {
545
- var _a, _b, _c;
798
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
799
+ if (data.collaboratorType !== undefined ||
800
+ data.collaboratorTypeId !== undefined ||
801
+ data.collaboratorTypeSlug !== undefined) {
802
+ const resolvedCollaboratorType = await this.resolveCollaboratorTypeReference(tx, {
803
+ collaboratorTypeId: (_a = data.collaboratorTypeId) !== null && _a !== void 0 ? _a : null,
804
+ collaboratorTypeSlug: (_c = (_b = data.collaboratorTypeSlug) !== null && _b !== void 0 ? _b : data.collaboratorType) !== null && _c !== void 0 ? _c : null,
805
+ });
806
+ this.pushUpdate(updates, params, 'collaborator_type_id', (_d = resolvedCollaboratorType === null || resolvedCollaboratorType === void 0 ? void 0 : resolvedCollaboratorType.id) !== null && _d !== void 0 ? _d : null);
807
+ }
546
808
  if (data.department !== undefined || data.departmentId !== undefined) {
547
809
  const resolvedDepartment = await this.resolveDepartmentReference(tx, {
548
- departmentId: (_a = data.departmentId) !== null && _a !== void 0 ? _a : null,
810
+ departmentId: (_e = data.departmentId) !== null && _e !== void 0 ? _e : null,
549
811
  departmentName: data.department,
550
812
  });
551
- this.pushUpdate(updates, params, 'department', (_b = resolvedDepartment === null || resolvedDepartment === void 0 ? void 0 : resolvedDepartment.name) !== null && _b !== void 0 ? _b : null);
552
- this.pushUpdate(updates, params, 'department_id', (_c = resolvedDepartment === null || resolvedDepartment === void 0 ? void 0 : resolvedDepartment.id) !== null && _c !== void 0 ? _c : null);
813
+ this.pushUpdate(updates, params, 'department', (_f = resolvedDepartment === null || resolvedDepartment === void 0 ? void 0 : resolvedDepartment.name) !== null && _f !== void 0 ? _f : null);
814
+ this.pushUpdate(updates, params, 'department_id', (_g = resolvedDepartment === null || resolvedDepartment === void 0 ? void 0 : resolvedDepartment.id) !== null && _g !== void 0 ? _g : null);
815
+ }
816
+ if (data.title !== undefined || data.jobTitleId !== undefined) {
817
+ const resolvedJobTitle = await this.resolveJobTitleReference(tx, {
818
+ jobTitleId: (_h = data.jobTitleId) !== null && _h !== void 0 ? _h : null,
819
+ jobTitleName: data.title,
820
+ });
821
+ this.pushUpdate(updates, params, 'job_title_id', (_j = resolvedJobTitle === null || resolvedJobTitle === void 0 ? void 0 : resolvedJobTitle.id) !== null && _j !== void 0 ? _j : null);
822
+ this.pushUpdate(updates, params, 'title', (_k = resolvedJobTitle === null || resolvedJobTitle === void 0 ? void 0 : resolvedJobTitle.name) !== null && _k !== void 0 ? _k : this.normalizeOptionalText(data.title));
553
823
  }
554
824
  if (updates.length) {
555
825
  params.push(collaboratorId);
@@ -561,6 +831,9 @@ let OperationsService = OperationsService_1 = class OperationsService {
561
831
  if (data.weeklySchedule) {
562
832
  await this.replaceCollaboratorScheduleDays(tx, collaboratorId, data.weeklySchedule);
563
833
  }
834
+ if (data.equityParticipation !== undefined) {
835
+ await this.replaceCollaboratorEquityParticipation(tx, collaboratorId, data.equityParticipation);
836
+ }
564
837
  });
565
838
  return this.getCollaboratorByIdForUser(userId, collaboratorId);
566
839
  }
@@ -590,6 +863,67 @@ let OperationsService = OperationsService_1 = class OperationsService {
590
863
  ORDER BY CASE WHEN d.deleted_at IS NULL THEN 0 ELSE 1 END ASC,
591
864
  d.name ASC`);
592
865
  }
866
+ async listJobTitles(userId) {
867
+ const actor = await this.getActorContext(userId);
868
+ this.ensureCollaborator(actor);
869
+ return this.queryRows(`SELECT jt.id,
870
+ jt.slug,
871
+ jt.code,
872
+ jt.name,
873
+ jt.description,
874
+ CASE WHEN jt.deleted_at IS NULL THEN 'active' ELSE 'inactive' END AS status,
875
+ COUNT(DISTINCT c.id)::int AS "collaboratorCount",
876
+ jt.created_at AS "createdAt",
877
+ jt.updated_at AS "updatedAt"
878
+ FROM operations_job_title jt
879
+ LEFT JOIN operations_collaborator c
880
+ ON c.deleted_at IS NULL
881
+ AND (
882
+ c.job_title_id = jt.id
883
+ OR (
884
+ c.job_title_id IS NULL
885
+ AND LOWER(COALESCE(c.title, '')) = LOWER(jt.name)
886
+ )
887
+ )
888
+ GROUP BY jt.id
889
+ ORDER BY CASE WHEN jt.deleted_at IS NULL THEN 0 ELSE 1 END ASC,
890
+ jt.name ASC`);
891
+ }
892
+ async createJobTitle(userId, data) {
893
+ const actor = await this.getActorContext(userId);
894
+ this.ensureDirector(actor);
895
+ const name = this.normalizeOptionalText(data.name);
896
+ if (!name) {
897
+ throw new common_1.BadRequestException('Job title name is required.');
898
+ }
899
+ return this.prisma.$transaction(async (tx) => {
900
+ var _a, _b, _c, _d;
901
+ await this.assertJobTitleNameAvailable(tx, name);
902
+ const normalizedCode = (_b = (_a = this.normalizeOptionalText(data.code)) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : null;
903
+ if (normalizedCode) {
904
+ await this.assertJobTitleCodeAvailable(tx, normalizedCode);
905
+ }
906
+ const created = (await tx.$queryRawUnsafe(`INSERT INTO operations_job_title (
907
+ slug,
908
+ code,
909
+ name,
910
+ description,
911
+ deleted_at,
912
+ created_at,
913
+ updated_at
914
+ ) VALUES (
915
+ $1, $2, $3, $4,
916
+ CASE WHEN $5 = 'inactive' THEN NOW() ELSE NULL END,
917
+ NOW(), NOW()
918
+ )
919
+ RETURNING id`, await this.generateUniqueJobTitleSlug(tx, name), normalizedCode, name, this.normalizeOptionalText(data.description), (_c = data.status) !== null && _c !== void 0 ? _c : 'active'));
920
+ const createdJobTitleId = (_d = created[0]) === null || _d === void 0 ? void 0 : _d.id;
921
+ if (!createdJobTitleId) {
922
+ throw new common_1.BadRequestException('Unable to create the job title.');
923
+ }
924
+ return this.getJobTitleById(tx, createdJobTitleId, true);
925
+ });
926
+ }
593
927
  async createDepartment(userId, data) {
594
928
  const actor = await this.getActorContext(userId);
595
929
  this.ensureDirector(actor);
@@ -705,6 +1039,436 @@ let OperationsService = OperationsService_1 = class OperationsService {
705
1039
  GROUP BY p.id, c.id, m.id
706
1040
  ORDER BY p.name ASC`, [...assignmentParams, ...filter.params]);
707
1041
  }
1042
+ async listProjectOptions(userId, paginationParams) {
1043
+ var _a, _b;
1044
+ const actor = await this.getActorContext(userId);
1045
+ this.ensureCollaborator(actor);
1046
+ if (!actor.collaboratorId) {
1047
+ throw new common_1.BadRequestException('Collaborator context is required.');
1048
+ }
1049
+ const pagination = this.normalizePaginationParams(paginationParams, {
1050
+ defaultSortField: 'name',
1051
+ defaultSortOrder: 'asc',
1052
+ allowedSortFields: ['name', 'code', 'clientName', 'startDate', 'endDate'],
1053
+ });
1054
+ const params = [actor.collaboratorId];
1055
+ const filters = [
1056
+ 'p.deleted_at IS NULL',
1057
+ 'pa.deleted_at IS NULL',
1058
+ `pa.collaborator_id = $1`,
1059
+ `pa.status IN ('planned', 'active')`,
1060
+ ];
1061
+ if (pagination.search) {
1062
+ const searchPlaceholder = this.param(params, `%${pagination.search}%`);
1063
+ filters.push(`(
1064
+ COALESCE(p.name, '') ILIKE ${searchPlaceholder}
1065
+ OR COALESCE(p.code, '') ILIKE ${searchPlaceholder}
1066
+ OR COALESCE(p.client_name, '') ILIKE ${searchPlaceholder}
1067
+ OR COALESCE(pa.role_label, '') ILIKE ${searchPlaceholder}
1068
+ )`);
1069
+ }
1070
+ const whereClause = filters.join(' AND ');
1071
+ const totalRow = await this.querySingle(`SELECT COUNT(DISTINCT p.id)::text AS total
1072
+ FROM operations_project_assignment pa
1073
+ JOIN operations_project p
1074
+ ON p.id = pa.project_id
1075
+ WHERE ${whereClause}`, params);
1076
+ const sortColumn = (_a = {
1077
+ name: 'p.name',
1078
+ code: 'p.code',
1079
+ clientName: 'p.client_name',
1080
+ startDate: 'p.start_date',
1081
+ endDate: 'p.end_date',
1082
+ }[pagination.sortField]) !== null && _a !== void 0 ? _a : 'p.name';
1083
+ const queryParams = [...params];
1084
+ const limitPlaceholder = this.param(queryParams, pagination.pageSize);
1085
+ const offsetPlaceholder = this.param(queryParams, pagination.offset);
1086
+ const rows = await this.queryRows(`SELECT p.id,
1087
+ p.code,
1088
+ p.name,
1089
+ p.client_name AS "clientName",
1090
+ MAX(pa.id)::int AS "projectAssignmentId",
1091
+ MAX(pa.role_label) AS "roleLabel",
1092
+ p.status,
1093
+ p.start_date AS "startDate",
1094
+ p.end_date AS "endDate"
1095
+ FROM operations_project_assignment pa
1096
+ JOIN operations_project p
1097
+ ON p.id = pa.project_id
1098
+ WHERE ${whereClause}
1099
+ GROUP BY p.id
1100
+ ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, p.id ASC
1101
+ LIMIT ${limitPlaceholder}
1102
+ OFFSET ${offsetPlaceholder}`, queryParams);
1103
+ return this.buildPaginationResult(rows.map((row) => (Object.assign(Object.assign({}, row), { label: [row.code, row.name, row.roleLabel].filter(Boolean).join(' • ') }))), Number((_b = totalRow === null || totalRow === void 0 ? void 0 : totalRow.total) !== null && _b !== void 0 ? _b : 0), pagination.page, pagination.pageSize);
1104
+ }
1105
+ async listTasks(userId, paginationParams) {
1106
+ var _a, _b;
1107
+ const actor = await this.getActorContext(userId);
1108
+ this.ensureCollaborator(actor);
1109
+ if (!actor.collaboratorId) {
1110
+ throw new common_1.BadRequestException('Collaborator context is required.');
1111
+ }
1112
+ const pagination = this.normalizePaginationParams(paginationParams, {
1113
+ defaultSortField: 'name',
1114
+ defaultSortOrder: 'asc',
1115
+ allowedSortFields: ['name', 'projectName', 'status', 'createdAt'],
1116
+ });
1117
+ const params = [actor.collaboratorId];
1118
+ const filters = [
1119
+ 't.deleted_at IS NULL',
1120
+ 'pa.deleted_at IS NULL',
1121
+ 'p.deleted_at IS NULL',
1122
+ `pa.collaborator_id = $1`,
1123
+ `pa.status IN ('planned', 'active')`,
1124
+ ];
1125
+ if (pagination.search) {
1126
+ const searchPlaceholder = this.param(params, `%${pagination.search}%`);
1127
+ filters.push(`(
1128
+ COALESCE(t.name, '') ILIKE ${searchPlaceholder}
1129
+ OR COALESCE(t.description, '') ILIKE ${searchPlaceholder}
1130
+ OR COALESCE(p.name, '') ILIKE ${searchPlaceholder}
1131
+ OR COALESCE(p.code, '') ILIKE ${searchPlaceholder}
1132
+ )`);
1133
+ }
1134
+ if (paginationParams.projectAssignmentId) {
1135
+ filters.push(`pa.id = ${this.param(params, paginationParams.projectAssignmentId)}`);
1136
+ }
1137
+ if (paginationParams.projectId) {
1138
+ filters.push(`pa.project_id = ${this.param(params, paginationParams.projectId)}`);
1139
+ }
1140
+ if (paginationParams.status) {
1141
+ filters.push(`t.status = ${this.param(params, paginationParams.status)}`);
1142
+ }
1143
+ const whereClause = filters.join(' AND ');
1144
+ const totalRow = await this.querySingle(`SELECT COUNT(*)::text AS total
1145
+ FROM operations_task t
1146
+ JOIN operations_project_assignment pa
1147
+ ON pa.id = t.project_assignment_id
1148
+ JOIN operations_project p
1149
+ ON p.id = pa.project_id
1150
+ WHERE ${whereClause}`, params);
1151
+ const sortColumn = (_a = {
1152
+ name: 't.name',
1153
+ projectName: 'p.name',
1154
+ status: 't.status',
1155
+ createdAt: 't.created_at',
1156
+ }[pagination.sortField]) !== null && _a !== void 0 ? _a : 't.name';
1157
+ const queryParams = [...params];
1158
+ const limitPlaceholder = this.param(queryParams, pagination.pageSize);
1159
+ const offsetPlaceholder = this.param(queryParams, pagination.offset);
1160
+ const rows = await this.queryRows(`SELECT t.id,
1161
+ t.name,
1162
+ t.description,
1163
+ t.status,
1164
+ pa.project_id AS "projectId",
1165
+ pa.id AS "projectAssignmentId",
1166
+ p.name AS "projectName",
1167
+ p.code AS "projectCode",
1168
+ t.created_at AS "createdAt"
1169
+ FROM operations_task t
1170
+ JOIN operations_project_assignment pa
1171
+ ON pa.id = t.project_assignment_id
1172
+ JOIN operations_project p
1173
+ ON p.id = pa.project_id
1174
+ WHERE ${whereClause}
1175
+ ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, t.id ASC
1176
+ LIMIT ${limitPlaceholder}
1177
+ OFFSET ${offsetPlaceholder}`, queryParams);
1178
+ return this.buildPaginationResult(rows.map((row) => (Object.assign(Object.assign({}, row), { label: [row.name, row.projectName].filter(Boolean).join(' • ') }))), Number((_b = totalRow === null || totalRow === void 0 ? void 0 : totalRow.total) !== null && _b !== void 0 ? _b : 0), pagination.page, pagination.pageSize);
1179
+ }
1180
+ async createTask(userId, data) {
1181
+ var _a, _b, _c, _d;
1182
+ const actor = await this.getActorContext(userId);
1183
+ this.ensureCollaborator(actor);
1184
+ this.requireFields(data, ['name']);
1185
+ if (!actor.collaboratorId) {
1186
+ throw new common_1.BadRequestException('Collaborator context is required.');
1187
+ }
1188
+ const assignment = await this.resolveOwnedProjectAssignment(this.prisma, actor.collaboratorId, {
1189
+ projectId: (_a = data.projectId) !== null && _a !== void 0 ? _a : null,
1190
+ projectAssignmentId: (_b = data.projectAssignmentId) !== null && _b !== void 0 ? _b : null,
1191
+ });
1192
+ await this.assertProjectAccess(actor, assignment.projectId);
1193
+ const name = this.normalizeOptionalText(data.name);
1194
+ if (!name) {
1195
+ throw new common_1.BadRequestException('Field "name" is required.');
1196
+ }
1197
+ const created = await this.querySingle(`INSERT INTO operations_task (
1198
+ project_assignment_id,
1199
+ name,
1200
+ description,
1201
+ status,
1202
+ created_at,
1203
+ updated_at
1204
+ ) VALUES (
1205
+ $1,
1206
+ $2,
1207
+ $3,
1208
+ $4,
1209
+ NOW(),
1210
+ NOW()
1211
+ )
1212
+ RETURNING id`, [
1213
+ assignment.id,
1214
+ name,
1215
+ this.normalizeOptionalText(data.description),
1216
+ (_c = data.status) !== null && _c !== void 0 ? _c : 'active',
1217
+ ]);
1218
+ return this.getTaskOptionById(actor.collaboratorId, (_d = created === null || created === void 0 ? void 0 : created.id) !== null && _d !== void 0 ? _d : 0);
1219
+ }
1220
+ async updateTask(userId, taskId, data) {
1221
+ const actor = await this.getActorContext(userId);
1222
+ this.ensureCollaborator(actor);
1223
+ if (!actor.collaboratorId) {
1224
+ throw new common_1.BadRequestException('Collaborator context is required.');
1225
+ }
1226
+ const current = await this.getOwnedTaskRecord(this.prisma, actor.collaboratorId, taskId);
1227
+ await this.assertProjectAccess(actor, current.projectId);
1228
+ const nextName = data.name !== undefined
1229
+ ? this.normalizeOptionalText(data.name)
1230
+ : current.name;
1231
+ if (!nextName) {
1232
+ throw new common_1.BadRequestException('Field "name" is required.');
1233
+ }
1234
+ await this.prisma.$transaction(async (tx) => {
1235
+ var _a, _b, _c, _d;
1236
+ const nextAssignment = data.projectId !== undefined || data.projectAssignmentId !== undefined
1237
+ ? await this.resolveOwnedProjectAssignment(tx, actor.collaboratorId, {
1238
+ projectId: (_a = data.projectId) !== null && _a !== void 0 ? _a : null,
1239
+ projectAssignmentId: (_b = data.projectAssignmentId) !== null && _b !== void 0 ? _b : null,
1240
+ })
1241
+ : {
1242
+ id: current.projectAssignmentId,
1243
+ projectId: current.projectId,
1244
+ };
1245
+ await this.assertProjectAccess(actor, nextAssignment.projectId);
1246
+ await tx.$executeRawUnsafe(`UPDATE operations_task
1247
+ SET project_assignment_id = $1,
1248
+ name = $2,
1249
+ description = $3,
1250
+ status = $4,
1251
+ updated_at = NOW()
1252
+ WHERE id = $5
1253
+ AND deleted_at IS NULL`, nextAssignment.id, nextName, data.description !== undefined
1254
+ ? this.normalizeOptionalText(data.description)
1255
+ : ((_c = current.description) !== null && _c !== void 0 ? _c : null), (_d = data.status) !== null && _d !== void 0 ? _d : current.status, taskId);
1256
+ });
1257
+ return this.getTaskOptionById(actor.collaboratorId, taskId);
1258
+ }
1259
+ async removeTask(userId, taskId) {
1260
+ const actor = await this.getActorContext(userId);
1261
+ this.ensureCollaborator(actor);
1262
+ if (!actor.collaboratorId) {
1263
+ throw new common_1.BadRequestException('Collaborator context is required.');
1264
+ }
1265
+ const current = await this.getOwnedTaskRecord(this.prisma, actor.collaboratorId, taskId);
1266
+ await this.assertProjectAccess(actor, current.projectId);
1267
+ await this.prisma.$transaction(async (tx) => {
1268
+ await tx.$executeRawUnsafe(`UPDATE operations_task
1269
+ SET deleted_at = COALESCE(deleted_at, NOW()),
1270
+ status = 'archived',
1271
+ updated_at = NOW()
1272
+ WHERE id = $1
1273
+ AND deleted_at IS NULL`, taskId);
1274
+ });
1275
+ return { success: true };
1276
+ }
1277
+ async listTimesheetEntries(userId, paginationParams) {
1278
+ var _a, _b;
1279
+ const actor = await this.getActorContext(userId);
1280
+ this.ensureCollaborator(actor);
1281
+ if (!actor.collaboratorId) {
1282
+ throw new common_1.BadRequestException('Collaborator context is required.');
1283
+ }
1284
+ const pagination = this.normalizePaginationParams(paginationParams, {
1285
+ defaultSortField: 'workDate',
1286
+ defaultSortOrder: 'desc',
1287
+ allowedSortFields: [
1288
+ 'workDate',
1289
+ 'createdAt',
1290
+ 'durationMinutes',
1291
+ 'projectName',
1292
+ 'taskName',
1293
+ 'status',
1294
+ ],
1295
+ });
1296
+ const params = [actor.collaboratorId];
1297
+ const filters = [
1298
+ 'e.deleted_at IS NULL',
1299
+ 't.deleted_at IS NULL',
1300
+ `t.collaborator_id = $1`,
1301
+ ];
1302
+ if (pagination.search) {
1303
+ const searchPlaceholder = this.param(params, `%${pagination.search}%`);
1304
+ filters.push(`(
1305
+ COALESCE(p.name, '') ILIKE ${searchPlaceholder}
1306
+ OR COALESCE(p.code, '') ILIKE ${searchPlaceholder}
1307
+ OR COALESCE(task_record.name, '') ILIKE ${searchPlaceholder}
1308
+ OR COALESCE(e.activity_label, '') ILIKE ${searchPlaceholder}
1309
+ OR COALESCE(e.description, '') ILIKE ${searchPlaceholder}
1310
+ )`);
1311
+ }
1312
+ if (paginationParams.projectAssignmentId) {
1313
+ filters.push(`e.project_assignment_id = ${this.param(params, paginationParams.projectAssignmentId)}`);
1314
+ }
1315
+ if (paginationParams.projectId) {
1316
+ filters.push(`pa.project_id = ${this.param(params, paginationParams.projectId)}`);
1317
+ }
1318
+ if (paginationParams.taskId) {
1319
+ filters.push(`e.task_id = ${this.param(params, paginationParams.taskId)}`);
1320
+ }
1321
+ if (paginationParams.status) {
1322
+ filters.push(`t.status = ${this.param(params, paginationParams.status, 'operations_timesheet_status_128a8ecd30_enum')}`);
1323
+ }
1324
+ if (paginationParams.fromDate) {
1325
+ filters.push(`e.work_date >= ${this.param(params, paginationParams.fromDate, 'date')}`);
1326
+ }
1327
+ if (paginationParams.toDate) {
1328
+ filters.push(`e.work_date <= ${this.param(params, paginationParams.toDate, 'date')}`);
1329
+ }
1330
+ const whereClause = filters.join(' AND ');
1331
+ const totalRow = await this.querySingle(`SELECT COUNT(*)::text AS total
1332
+ FROM operations_timesheet_entry e
1333
+ JOIN operations_timesheet t
1334
+ ON t.id = e.timesheet_id
1335
+ LEFT JOIN operations_project_assignment pa
1336
+ ON pa.id = e.project_assignment_id
1337
+ LEFT JOIN operations_project p
1338
+ ON p.id = pa.project_id
1339
+ LEFT JOIN operations_task task_record
1340
+ ON task_record.id = e.task_id
1341
+ AND task_record.deleted_at IS NULL
1342
+ WHERE ${whereClause}`, params);
1343
+ const sortColumn = (_a = {
1344
+ workDate: 'e.work_date',
1345
+ createdAt: 'e.created_at',
1346
+ durationMinutes: 'COALESCE(NULLIF(e.duration_minutes, 0), ROUND(COALESCE(e.hours, 0)::numeric * 60))',
1347
+ projectName: 'p.name',
1348
+ taskName: 'COALESCE(task_record.name, e.activity_label)',
1349
+ status: 't.status',
1350
+ }[pagination.sortField]) !== null && _a !== void 0 ? _a : 'e.work_date';
1351
+ const queryParams = [...params];
1352
+ const limitPlaceholder = this.param(queryParams, pagination.pageSize);
1353
+ const offsetPlaceholder = this.param(queryParams, pagination.offset);
1354
+ const rows = await this.queryRows(`SELECT e.id,
1355
+ e.timesheet_id AS "timesheetId",
1356
+ t.collaborator_id AS "collaboratorId",
1357
+ pa.project_id AS "projectId",
1358
+ e.project_assignment_id AS "projectAssignmentId",
1359
+ p.code AS "projectCode",
1360
+ p.name AS "projectName",
1361
+ e.task_id AS "taskId",
1362
+ COALESCE(task_record.name, e.activity_label) AS "taskName",
1363
+ e.activity_label AS "activityLabel",
1364
+ e.work_date AS "workDate",
1365
+ COALESCE(NULLIF(e.duration_minutes, 0), ROUND(COALESCE(e.hours, 0)::numeric * 60))::int AS "durationMinutes",
1366
+ COALESCE(
1367
+ e.hours,
1368
+ ROUND((COALESCE(NULLIF(e.duration_minutes, 0), 0)::numeric / 60), 2)
1369
+ ) AS hours,
1370
+ e.description,
1371
+ t.status,
1372
+ t.week_start_date AS "weekStartDate",
1373
+ t.week_end_date AS "weekEndDate",
1374
+ e.created_at AS "createdAt"
1375
+ FROM operations_timesheet_entry e
1376
+ JOIN operations_timesheet t
1377
+ ON t.id = e.timesheet_id
1378
+ LEFT JOIN operations_project_assignment pa
1379
+ ON pa.id = e.project_assignment_id
1380
+ LEFT JOIN operations_project p
1381
+ ON p.id = pa.project_id
1382
+ LEFT JOIN operations_task task_record
1383
+ ON task_record.id = e.task_id
1384
+ AND task_record.deleted_at IS NULL
1385
+ WHERE ${whereClause}
1386
+ ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, e.id DESC
1387
+ LIMIT ${limitPlaceholder}
1388
+ OFFSET ${offsetPlaceholder}`, queryParams);
1389
+ return this.buildPaginationResult(rows.map((row) => (Object.assign(Object.assign({}, row), { label: [row.projectCode, row.projectName, row.taskName]
1390
+ .filter(Boolean)
1391
+ .join(' • ') }))), Number((_b = totalRow === null || totalRow === void 0 ? void 0 : totalRow.total) !== null && _b !== void 0 ? _b : 0), pagination.page, pagination.pageSize);
1392
+ }
1393
+ async createTimesheetEntry(userId, data) {
1394
+ var _a;
1395
+ const actor = await this.getActorContext(userId);
1396
+ this.ensureCollaborator(actor);
1397
+ this.requireFields(data, ['workDate', 'duration']);
1398
+ if (!actor.collaboratorId) {
1399
+ throw new common_1.BadRequestException('Collaborator context is required.');
1400
+ }
1401
+ const durationMinutes = this.normalizeDurationMinutes(data.duration, data.unit);
1402
+ const taskLabel = (_a = this.normalizeOptionalText(data.taskName)) !== null && _a !== void 0 ? _a : this.normalizeOptionalText(data.activityLabel);
1403
+ const createdEntryId = await this.prisma.$transaction(async (tx) => {
1404
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
1405
+ const assignment = await this.resolveOwnedProjectAssignment(tx, actor.collaboratorId, {
1406
+ projectId: (_a = data.projectId) !== null && _a !== void 0 ? _a : null,
1407
+ projectAssignmentId: (_b = data.projectAssignmentId) !== null && _b !== void 0 ? _b : null,
1408
+ });
1409
+ const resolvedTask = data.taskId
1410
+ ? await this.getOwnedTaskRecord(tx, actor.collaboratorId, data.taskId)
1411
+ : null;
1412
+ if (resolvedTask && resolvedTask.projectAssignmentId !== assignment.id) {
1413
+ throw new common_1.BadRequestException('The selected task does not belong to the chosen project assignment.');
1414
+ }
1415
+ const activityLabel = (_e = (_d = (_c = resolvedTask === null || resolvedTask === void 0 ? void 0 : resolvedTask.name) !== null && _c !== void 0 ? _c : taskLabel) !== null && _d !== void 0 ? _d : assignment.roleLabel) !== null && _e !== void 0 ? _e : assignment.projectName;
1416
+ if (!activityLabel) {
1417
+ throw new common_1.BadRequestException('A task is required for the timesheet entry.');
1418
+ }
1419
+ const timesheetId = await this.getOrCreateTimesheetForWorkDate(tx, actor.collaboratorId, data.workDate);
1420
+ const created = (await tx.$queryRawUnsafe(`INSERT INTO operations_timesheet_entry (
1421
+ timesheet_id,
1422
+ project_assignment_id,
1423
+ task_id,
1424
+ activity_label,
1425
+ work_date,
1426
+ duration_minutes,
1427
+ hours,
1428
+ description,
1429
+ created_at,
1430
+ updated_at
1431
+ ) VALUES (
1432
+ $1,
1433
+ $2,
1434
+ $3,
1435
+ $4,
1436
+ $5::date,
1437
+ $6,
1438
+ $7,
1439
+ $8,
1440
+ NOW(),
1441
+ NOW()
1442
+ )
1443
+ RETURNING id`, timesheetId, assignment.id, (_g = (_f = resolvedTask === null || resolvedTask === void 0 ? void 0 : resolvedTask.id) !== null && _f !== void 0 ? _f : data.taskId) !== null && _g !== void 0 ? _g : null, activityLabel, data.workDate, durationMinutes, Number((durationMinutes / 60).toFixed(2)), this.normalizeOptionalText(data.description)));
1444
+ await this.refreshTimesheetTotal(tx, timesheetId);
1445
+ return (_j = (_h = created[0]) === null || _h === void 0 ? void 0 : _h.id) !== null && _j !== void 0 ? _j : 0;
1446
+ });
1447
+ return this.getTimesheetEntryByIdForActor(actor, createdEntryId);
1448
+ }
1449
+ async removeTimesheetEntry(userId, entryId) {
1450
+ const actor = await this.getActorContext(userId);
1451
+ this.ensureCollaborator(actor);
1452
+ if (!actor.collaboratorId && !actor.isDirector) {
1453
+ throw new common_1.BadRequestException('Collaborator context is required.');
1454
+ }
1455
+ const entry = await this.getTimesheetEntryByIdForActor(actor, entryId);
1456
+ if (!actor.isDirector && entry.collaboratorId !== actor.collaboratorId) {
1457
+ throw new common_1.ForbiddenException('Only the entry owner can delete this timesheet entry.');
1458
+ }
1459
+ if (!['draft', 'rejected'].includes(entry.status)) {
1460
+ throw new common_1.BadRequestException('Only draft or rejected timesheet entries can be deleted.');
1461
+ }
1462
+ await this.prisma.$transaction(async (tx) => {
1463
+ await tx.$executeRawUnsafe(`UPDATE operations_timesheet_entry
1464
+ SET deleted_at = NOW(),
1465
+ updated_at = NOW()
1466
+ WHERE id = $1
1467
+ AND deleted_at IS NULL`, entryId);
1468
+ await this.refreshTimesheetTotal(tx, entry.timesheetId);
1469
+ });
1470
+ return { success: true };
1471
+ }
708
1472
  async getProjectById(userId, projectId) {
709
1473
  const actor = await this.getActorContext(userId);
710
1474
  await this.assertProjectAccess(actor, projectId);
@@ -2101,13 +2865,22 @@ let OperationsService = OperationsService_1 = class OperationsService {
2101
2865
  pa.project_id AS "projectId",
2102
2866
  p.name AS "projectName",
2103
2867
  pa.role_label AS "roleLabel",
2104
- e.activity_label AS "activityLabel",
2868
+ e.task_id AS "taskId",
2869
+ task_record.name AS "taskName",
2870
+ COALESCE(task_record.name, e.activity_label) AS "activityLabel",
2105
2871
  e.work_date AS "workDate",
2106
- e.hours,
2872
+ COALESCE(NULLIF(e.duration_minutes, 0), ROUND(COALESCE(e.hours, 0)::numeric * 60))::int AS "durationMinutes",
2873
+ COALESCE(
2874
+ e.hours,
2875
+ ROUND((COALESCE(NULLIF(e.duration_minutes, 0), 0)::numeric / 60), 2)
2876
+ ) AS hours,
2107
2877
  e.description
2108
2878
  FROM operations_timesheet_entry e
2109
2879
  LEFT JOIN operations_project_assignment pa ON pa.id = e.project_assignment_id
2110
2880
  LEFT JOIN operations_project p ON p.id = pa.project_id
2881
+ LEFT JOIN operations_task task_record
2882
+ ON task_record.id = e.task_id
2883
+ AND task_record.deleted_at IS NULL
2111
2884
  WHERE e.deleted_at IS NULL
2112
2885
  AND e.timesheet_id = ANY($1::int[])
2113
2886
  ORDER BY e.work_date ASC, e.id ASC`, [headers.map((item) => item.id)]);
@@ -2192,6 +2965,18 @@ let OperationsService = OperationsService_1 = class OperationsService {
2192
2965
  }
2193
2966
  const collaborator = await this.getCollaboratorById(current.collaboratorId);
2194
2967
  const approverId = (_b = (_a = current.approverCollaboratorId) !== null && _a !== void 0 ? _a : collaborator.supervisorId) !== null && _b !== void 0 ? _b : null;
2968
+ if (!approverId) {
2969
+ throw new common_1.BadRequestException('An approver is required before submitting a timesheet.');
2970
+ }
2971
+ const activeEntries = await this.querySingle(`SELECT EXISTS (
2972
+ SELECT 1
2973
+ FROM operations_timesheet_entry
2974
+ WHERE timesheet_id = $1
2975
+ AND deleted_at IS NULL
2976
+ ) AS exists`, [timesheetId]);
2977
+ if (!(activeEntries === null || activeEntries === void 0 ? void 0 : activeEntries.exists)) {
2978
+ throw new common_1.BadRequestException('Cannot submit a timesheet without active entries.');
2979
+ }
2195
2980
  await this.prisma.$transaction(async (tx) => {
2196
2981
  await tx.$executeRawUnsafe(`UPDATE operations_timesheet
2197
2982
  SET status = 'submitted',
@@ -2606,6 +3391,9 @@ let OperationsService = OperationsService_1 = class OperationsService {
2606
3391
  submitted_at = COALESCE(submitted_at, NOW()),
2607
3392
  updated_at = NOW()
2608
3393
  WHERE id = $3`, nextStatus, actor.collaboratorId, approval.targetId);
3394
+ if (nextStatus === 'approved') {
3395
+ await this.applyApprovedScheduleAdjustmentIfNeeded(tx, approval.targetId);
3396
+ }
2609
3397
  }
2610
3398
  await this.insertApprovalHistory(tx, approvalId, actor.collaboratorId, nextStatus === 'approved' ? 'approved' : 'rejected', (_b = data.note) !== null && _b !== void 0 ? _b : null);
2611
3399
  });
@@ -2700,11 +3488,101 @@ let OperationsService = OperationsService_1 = class OperationsService {
2700
3488
  if (!person) {
2701
3489
  throw new common_1.NotFoundException('Person not found.');
2702
3490
  }
2703
- return person;
3491
+ return person;
3492
+ }
3493
+ async getDepartmentById(client, departmentId, includeInactive = false) {
3494
+ var _a;
3495
+ const departments = (await client.$queryRawUnsafe(`SELECT id,
3496
+ slug,
3497
+ code,
3498
+ name,
3499
+ description,
3500
+ created_at AS "createdAt",
3501
+ updated_at AS "updatedAt",
3502
+ deleted_at AS "deletedAt",
3503
+ CASE WHEN deleted_at IS NULL THEN 'active' ELSE 'inactive' END AS status
3504
+ FROM operations_department
3505
+ WHERE id = $1
3506
+ AND ($2::boolean OR deleted_at IS NULL)`, departmentId, includeInactive));
3507
+ const department = (_a = departments[0]) !== null && _a !== void 0 ? _a : null;
3508
+ if (!department) {
3509
+ throw new common_1.NotFoundException('Department not found.');
3510
+ }
3511
+ return department;
3512
+ }
3513
+ async assertDepartmentNameAvailable(client, name, excludeDepartmentId) {
3514
+ const existing = (await client.$queryRawUnsafe(`SELECT id
3515
+ FROM operations_department
3516
+ WHERE LOWER(name) = LOWER($1)
3517
+ AND ($2::int IS NULL OR id <> $2)
3518
+ LIMIT 1`, name, excludeDepartmentId !== null && excludeDepartmentId !== void 0 ? excludeDepartmentId : null));
3519
+ if (existing[0]) {
3520
+ throw new common_1.BadRequestException('A department with this name already exists.');
3521
+ }
3522
+ }
3523
+ async assertDepartmentCodeAvailable(client, code, excludeDepartmentId) {
3524
+ const existing = (await client.$queryRawUnsafe(`SELECT id
3525
+ FROM operations_department
3526
+ WHERE UPPER(COALESCE(code, '')) = UPPER($1)
3527
+ AND ($2::int IS NULL OR id <> $2)
3528
+ LIMIT 1`, code, excludeDepartmentId !== null && excludeDepartmentId !== void 0 ? excludeDepartmentId : null));
3529
+ if (existing[0]) {
3530
+ throw new common_1.BadRequestException('A department with this code already exists.');
3531
+ }
3532
+ }
3533
+ async resolveDepartmentReference(client, input) {
3534
+ var _a;
3535
+ if (input.departmentName !== undefined) {
3536
+ const normalizedDepartmentName = this.normalizeOptionalText(input.departmentName);
3537
+ if (!normalizedDepartmentName) {
3538
+ return null;
3539
+ }
3540
+ const existingByName = (await client.$queryRawUnsafe(`SELECT id, name
3541
+ FROM operations_department
3542
+ WHERE deleted_at IS NULL
3543
+ AND LOWER(name) = LOWER($1)
3544
+ ORDER BY id ASC
3545
+ LIMIT 1`, normalizedDepartmentName));
3546
+ if (existingByName[0]) {
3547
+ return existingByName[0];
3548
+ }
3549
+ const slug = await this.generateUniqueDepartmentSlug(client, normalizedDepartmentName);
3550
+ const created = (await client.$queryRawUnsafe(`INSERT INTO operations_department (
3551
+ slug,
3552
+ code,
3553
+ name,
3554
+ description,
3555
+ created_at,
3556
+ updated_at
3557
+ ) VALUES ($1, NULL, $2, NULL, NOW(), NOW())
3558
+ RETURNING id, name`, slug, normalizedDepartmentName));
3559
+ return (_a = created[0]) !== null && _a !== void 0 ? _a : null;
3560
+ }
3561
+ if (typeof input.departmentId === 'number' &&
3562
+ Number.isInteger(input.departmentId) &&
3563
+ input.departmentId > 0) {
3564
+ return this.getDepartmentById(client, input.departmentId);
3565
+ }
3566
+ return null;
3567
+ }
3568
+ async generateUniqueDepartmentSlug(client, label, excludeDepartmentId) {
3569
+ const baseSlug = this.slugifyValue(label) || `department-${Date.now().toString(36)}`;
3570
+ for (let attempt = 0; attempt < 25; attempt += 1) {
3571
+ const candidate = attempt === 0 ? baseSlug : `${baseSlug}-${attempt + 1}`;
3572
+ const existing = (await client.$queryRawUnsafe(`SELECT id
3573
+ FROM operations_department
3574
+ WHERE slug = $1
3575
+ AND ($2::int IS NULL OR id <> $2)
3576
+ LIMIT 1`, candidate, excludeDepartmentId !== null && excludeDepartmentId !== void 0 ? excludeDepartmentId : null));
3577
+ if (!existing.length) {
3578
+ return candidate;
3579
+ }
3580
+ }
3581
+ return `${baseSlug}-${Date.now().toString(36)}`;
2704
3582
  }
2705
- async getDepartmentById(client, departmentId, includeInactive = false) {
3583
+ async getJobTitleById(client, jobTitleId, includeInactive = false) {
2706
3584
  var _a;
2707
- const departments = (await client.$queryRawUnsafe(`SELECT id,
3585
+ const jobTitles = (await client.$queryRawUnsafe(`SELECT id,
2708
3586
  slug,
2709
3587
  code,
2710
3588
  name,
@@ -2713,53 +3591,53 @@ let OperationsService = OperationsService_1 = class OperationsService {
2713
3591
  updated_at AS "updatedAt",
2714
3592
  deleted_at AS "deletedAt",
2715
3593
  CASE WHEN deleted_at IS NULL THEN 'active' ELSE 'inactive' END AS status
2716
- FROM operations_department
3594
+ FROM operations_job_title
2717
3595
  WHERE id = $1
2718
- AND ($2::boolean OR deleted_at IS NULL)`, departmentId, includeInactive));
2719
- const department = (_a = departments[0]) !== null && _a !== void 0 ? _a : null;
2720
- if (!department) {
2721
- throw new common_1.NotFoundException('Department not found.');
3596
+ AND ($2::boolean OR deleted_at IS NULL)`, jobTitleId, includeInactive));
3597
+ const jobTitle = (_a = jobTitles[0]) !== null && _a !== void 0 ? _a : null;
3598
+ if (!jobTitle) {
3599
+ throw new common_1.NotFoundException('Job title not found.');
2722
3600
  }
2723
- return department;
3601
+ return jobTitle;
2724
3602
  }
2725
- async assertDepartmentNameAvailable(client, name, excludeDepartmentId) {
3603
+ async assertJobTitleNameAvailable(client, name, excludeJobTitleId) {
2726
3604
  const existing = (await client.$queryRawUnsafe(`SELECT id
2727
- FROM operations_department
3605
+ FROM operations_job_title
2728
3606
  WHERE LOWER(name) = LOWER($1)
2729
3607
  AND ($2::int IS NULL OR id <> $2)
2730
- LIMIT 1`, name, excludeDepartmentId !== null && excludeDepartmentId !== void 0 ? excludeDepartmentId : null));
3608
+ LIMIT 1`, name, excludeJobTitleId !== null && excludeJobTitleId !== void 0 ? excludeJobTitleId : null));
2731
3609
  if (existing[0]) {
2732
- throw new common_1.BadRequestException('A department with this name already exists.');
3610
+ throw new common_1.BadRequestException('A job title with this name already exists.');
2733
3611
  }
2734
3612
  }
2735
- async assertDepartmentCodeAvailable(client, code, excludeDepartmentId) {
3613
+ async assertJobTitleCodeAvailable(client, code, excludeJobTitleId) {
2736
3614
  const existing = (await client.$queryRawUnsafe(`SELECT id
2737
- FROM operations_department
3615
+ FROM operations_job_title
2738
3616
  WHERE UPPER(COALESCE(code, '')) = UPPER($1)
2739
3617
  AND ($2::int IS NULL OR id <> $2)
2740
- LIMIT 1`, code, excludeDepartmentId !== null && excludeDepartmentId !== void 0 ? excludeDepartmentId : null));
3618
+ LIMIT 1`, code, excludeJobTitleId !== null && excludeJobTitleId !== void 0 ? excludeJobTitleId : null));
2741
3619
  if (existing[0]) {
2742
- throw new common_1.BadRequestException('A department with this code already exists.');
3620
+ throw new common_1.BadRequestException('A job title with this code already exists.');
2743
3621
  }
2744
3622
  }
2745
- async resolveDepartmentReference(client, input) {
3623
+ async resolveJobTitleReference(client, input) {
2746
3624
  var _a;
2747
- if (input.departmentName !== undefined) {
2748
- const normalizedDepartmentName = this.normalizeOptionalText(input.departmentName);
2749
- if (!normalizedDepartmentName) {
3625
+ if (input.jobTitleName !== undefined) {
3626
+ const normalizedJobTitleName = this.normalizeOptionalText(input.jobTitleName);
3627
+ if (!normalizedJobTitleName) {
2750
3628
  return null;
2751
3629
  }
2752
3630
  const existingByName = (await client.$queryRawUnsafe(`SELECT id, name
2753
- FROM operations_department
3631
+ FROM operations_job_title
2754
3632
  WHERE deleted_at IS NULL
2755
3633
  AND LOWER(name) = LOWER($1)
2756
3634
  ORDER BY id ASC
2757
- LIMIT 1`, normalizedDepartmentName));
3635
+ LIMIT 1`, normalizedJobTitleName));
2758
3636
  if (existingByName[0]) {
2759
3637
  return existingByName[0];
2760
3638
  }
2761
- const slug = await this.generateUniqueDepartmentSlug(client, normalizedDepartmentName);
2762
- const created = (await client.$queryRawUnsafe(`INSERT INTO operations_department (
3639
+ const slug = await this.generateUniqueJobTitleSlug(client, normalizedJobTitleName);
3640
+ const created = (await client.$queryRawUnsafe(`INSERT INTO operations_job_title (
2763
3641
  slug,
2764
3642
  code,
2765
3643
  name,
@@ -2767,31 +3645,205 @@ let OperationsService = OperationsService_1 = class OperationsService {
2767
3645
  created_at,
2768
3646
  updated_at
2769
3647
  ) VALUES ($1, NULL, $2, NULL, NOW(), NOW())
2770
- RETURNING id, name`, slug, normalizedDepartmentName));
3648
+ RETURNING id, name`, slug, normalizedJobTitleName));
2771
3649
  return (_a = created[0]) !== null && _a !== void 0 ? _a : null;
2772
3650
  }
2773
- if (typeof input.departmentId === 'number' &&
2774
- Number.isInteger(input.departmentId) &&
2775
- input.departmentId > 0) {
2776
- return this.getDepartmentById(client, input.departmentId);
3651
+ if (typeof input.jobTitleId === 'number' &&
3652
+ Number.isInteger(input.jobTitleId) &&
3653
+ input.jobTitleId > 0) {
3654
+ return this.getJobTitleById(client, input.jobTitleId);
2777
3655
  }
2778
3656
  return null;
2779
3657
  }
2780
- async generateUniqueDepartmentSlug(client, label, excludeDepartmentId) {
2781
- const baseSlug = this.slugifyValue(label) || `department-${Date.now().toString(36)}`;
3658
+ async generateUniqueJobTitleSlug(client, label, excludeJobTitleId) {
3659
+ const baseSlug = this.slugifyValue(label) || `job-title-${Date.now().toString(36)}`;
2782
3660
  for (let attempt = 0; attempt < 25; attempt += 1) {
2783
3661
  const candidate = attempt === 0 ? baseSlug : `${baseSlug}-${attempt + 1}`;
2784
3662
  const existing = (await client.$queryRawUnsafe(`SELECT id
2785
- FROM operations_department
3663
+ FROM operations_job_title
2786
3664
  WHERE slug = $1
2787
3665
  AND ($2::int IS NULL OR id <> $2)
2788
- LIMIT 1`, candidate, excludeDepartmentId !== null && excludeDepartmentId !== void 0 ? excludeDepartmentId : null));
3666
+ LIMIT 1`, candidate, excludeJobTitleId !== null && excludeJobTitleId !== void 0 ? excludeJobTitleId : null));
3667
+ if (!existing.length) {
3668
+ return candidate;
3669
+ }
3670
+ }
3671
+ return `${baseSlug}-${Date.now().toString(36)}`;
3672
+ }
3673
+ async getCollaboratorTypeById(client, collaboratorTypeId, includeInactive = false) {
3674
+ var _a;
3675
+ const collaboratorTypes = (await client.$queryRawUnsafe(`SELECT ct.id,
3676
+ ct.slug,
3677
+ ct.name,
3678
+ ct.description,
3679
+ ct.category,
3680
+ ct.is_active AS "isActive",
3681
+ ct.sort_order AS "sortOrder",
3682
+ CASE
3683
+ WHEN ct.deleted_at IS NULL AND ct.is_active THEN 'active'
3684
+ ELSE 'inactive'
3685
+ END AS status,
3686
+ COUNT(DISTINCT c.id)::int AS "collaboratorCount",
3687
+ ct.created_at AS "createdAt",
3688
+ ct.updated_at AS "updatedAt",
3689
+ ct.deleted_at AS "deletedAt"
3690
+ FROM operations_collaborator_type ct
3691
+ LEFT JOIN operations_collaborator c
3692
+ ON c.deleted_at IS NULL
3693
+ AND c.collaborator_type_id = ct.id
3694
+ WHERE ct.id = $1
3695
+ AND ($2::boolean OR ct.deleted_at IS NULL)
3696
+ GROUP BY ct.id
3697
+ LIMIT 1`, collaboratorTypeId, includeInactive));
3698
+ const collaboratorType = (_a = collaboratorTypes[0]) !== null && _a !== void 0 ? _a : null;
3699
+ if (!collaboratorType) {
3700
+ throw new common_1.NotFoundException('Collaborator type not found.');
3701
+ }
3702
+ return collaboratorType;
3703
+ }
3704
+ async assertCollaboratorTypeNameAvailable(client, name, excludeCollaboratorTypeId) {
3705
+ const existing = (await client.$queryRawUnsafe(`SELECT id
3706
+ FROM operations_collaborator_type
3707
+ WHERE LOWER(name) = LOWER($1)
3708
+ AND ($2::int IS NULL OR id <> $2)
3709
+ LIMIT 1`, name, excludeCollaboratorTypeId !== null && excludeCollaboratorTypeId !== void 0 ? excludeCollaboratorTypeId : null));
3710
+ if (existing[0]) {
3711
+ throw new common_1.BadRequestException('A collaborator type with this name already exists.');
3712
+ }
3713
+ }
3714
+ async assertCollaboratorTypeSlugAvailable(client, slug, excludeCollaboratorTypeId) {
3715
+ const existing = (await client.$queryRawUnsafe(`SELECT id
3716
+ FROM operations_collaborator_type
3717
+ WHERE LOWER(slug) = LOWER($1)
3718
+ AND ($2::int IS NULL OR id <> $2)
3719
+ LIMIT 1`, slug, excludeCollaboratorTypeId !== null && excludeCollaboratorTypeId !== void 0 ? excludeCollaboratorTypeId : null));
3720
+ if (existing[0]) {
3721
+ throw new common_1.BadRequestException('A collaborator type with this slug already exists.');
3722
+ }
3723
+ }
3724
+ async buildCollaboratorTypeSlug(client, name, slugInput, excludeCollaboratorTypeId) {
3725
+ var _a;
3726
+ const normalizedSlugInput = this.slugifyValue((_a = this.normalizeOptionalText(slugInput)) !== null && _a !== void 0 ? _a : '');
3727
+ if (normalizedSlugInput) {
3728
+ await this.assertCollaboratorTypeSlugAvailable(client, normalizedSlugInput, excludeCollaboratorTypeId);
3729
+ return normalizedSlugInput;
3730
+ }
3731
+ return this.generateUniqueCollaboratorTypeSlug(client, name, excludeCollaboratorTypeId);
3732
+ }
3733
+ async generateUniqueCollaboratorTypeSlug(client, label, excludeCollaboratorTypeId) {
3734
+ const baseSlug = this.slugifyValue(label) || `collaborator-type-${Date.now().toString(36)}`;
3735
+ for (let attempt = 0; attempt < 25; attempt += 1) {
3736
+ const candidate = attempt === 0 ? baseSlug : `${baseSlug}-${attempt + 1}`;
3737
+ const existing = (await client.$queryRawUnsafe(`SELECT id
3738
+ FROM operations_collaborator_type
3739
+ WHERE slug = $1
3740
+ AND ($2::int IS NULL OR id <> $2)
3741
+ LIMIT 1`, candidate, excludeCollaboratorTypeId !== null && excludeCollaboratorTypeId !== void 0 ? excludeCollaboratorTypeId : null));
3742
+ if (!existing.length) {
3743
+ return candidate;
3744
+ }
3745
+ }
3746
+ return `${baseSlug}-${Date.now().toString(36)}`;
3747
+ }
3748
+ async getProjectRoleById(client, projectRoleId, includeInactive = false) {
3749
+ var _a;
3750
+ const localeId = await this.resolvePreferredLocaleId(client);
3751
+ const projectRoles = (await client.$queryRawUnsafe(`SELECT pr.id,
3752
+ pr.slug,
3753
+ pr.code,
3754
+ COALESCE(prl.name, pr.code, pr.slug) AS name,
3755
+ prl.description,
3756
+ pr.is_active AS "isActive",
3757
+ pr.sort_order AS "sortOrder",
3758
+ pr.created_at AS "createdAt",
3759
+ pr.updated_at AS "updatedAt",
3760
+ pr.deleted_at AS "deletedAt"
3761
+ FROM operations_project_role pr
3762
+ LEFT JOIN LATERAL (
3763
+ SELECT pr_locale.name,
3764
+ pr_locale.description
3765
+ FROM operations_project_role_locale pr_locale
3766
+ WHERE pr_locale.operations_project_role_id = pr.id
3767
+ ORDER BY CASE
3768
+ WHEN $3::int IS NOT NULL AND pr_locale.locale_id = $3 THEN 0
3769
+ ELSE 1
3770
+ END ASC,
3771
+ pr_locale.id ASC
3772
+ LIMIT 1
3773
+ ) prl ON TRUE
3774
+ WHERE pr.id = $1
3775
+ AND ($2::boolean OR pr.deleted_at IS NULL)`, projectRoleId, includeInactive, localeId));
3776
+ const projectRole = (_a = projectRoles[0]) !== null && _a !== void 0 ? _a : null;
3777
+ if (!projectRole) {
3778
+ throw new common_1.NotFoundException('Project role not found.');
3779
+ }
3780
+ return projectRole;
3781
+ }
3782
+ async assertProjectRoleNameAvailable(client, name, excludeProjectRoleId) {
3783
+ const existing = (await client.$queryRawUnsafe(`SELECT pr.id
3784
+ FROM operations_project_role pr
3785
+ JOIN operations_project_role_locale prl
3786
+ ON prl.operations_project_role_id = pr.id
3787
+ WHERE LOWER(prl.name) = LOWER($1)
3788
+ AND pr.deleted_at IS NULL
3789
+ AND ($2::int IS NULL OR pr.id <> $2)
3790
+ LIMIT 1`, name, excludeProjectRoleId !== null && excludeProjectRoleId !== void 0 ? excludeProjectRoleId : null));
3791
+ if (existing[0]) {
3792
+ throw new common_1.BadRequestException('A project role with this name already exists.');
3793
+ }
3794
+ }
3795
+ async assertProjectRoleCodeAvailable(client, code, excludeProjectRoleId) {
3796
+ const existing = (await client.$queryRawUnsafe(`SELECT id
3797
+ FROM operations_project_role
3798
+ WHERE UPPER(COALESCE(code, '')) = UPPER($1)
3799
+ AND ($2::int IS NULL OR id <> $2)
3800
+ LIMIT 1`, code, excludeProjectRoleId !== null && excludeProjectRoleId !== void 0 ? excludeProjectRoleId : null));
3801
+ if (existing[0]) {
3802
+ throw new common_1.BadRequestException('A project role with this code already exists.');
3803
+ }
3804
+ }
3805
+ async generateUniqueProjectRoleSlug(client, label, excludeProjectRoleId) {
3806
+ const baseSlug = this.slugifyValue(label) || `project-role-${Date.now().toString(36)}`;
3807
+ for (let attempt = 0; attempt < 25; attempt += 1) {
3808
+ const candidate = attempt === 0 ? baseSlug : `${baseSlug}-${attempt + 1}`;
3809
+ const existing = (await client.$queryRawUnsafe(`SELECT id
3810
+ FROM operations_project_role
3811
+ WHERE slug = $1
3812
+ AND ($2::int IS NULL OR id <> $2)
3813
+ LIMIT 1`, candidate, excludeProjectRoleId !== null && excludeProjectRoleId !== void 0 ? excludeProjectRoleId : null));
2789
3814
  if (!existing.length) {
2790
3815
  return candidate;
2791
3816
  }
2792
3817
  }
2793
3818
  return `${baseSlug}-${Date.now().toString(36)}`;
2794
3819
  }
3820
+ async resolveCollaboratorTypeReference(client, input) {
3821
+ var _a, _b;
3822
+ if (typeof input.collaboratorTypeId === 'number' &&
3823
+ Number.isInteger(input.collaboratorTypeId) &&
3824
+ input.collaboratorTypeId > 0) {
3825
+ const collaboratorTypes = (await client.$queryRawUnsafe(`SELECT id, slug, name
3826
+ FROM operations_collaborator_type
3827
+ WHERE id = $1
3828
+ AND deleted_at IS NULL
3829
+ LIMIT 1`, input.collaboratorTypeId));
3830
+ return (_a = collaboratorTypes[0]) !== null && _a !== void 0 ? _a : null;
3831
+ }
3832
+ const normalizedLookup = this.normalizeOptionalText(input.collaboratorTypeSlug);
3833
+ if (!normalizedLookup) {
3834
+ return null;
3835
+ }
3836
+ const collaboratorTypes = (await client.$queryRawUnsafe(`SELECT id, slug, name
3837
+ FROM operations_collaborator_type
3838
+ WHERE deleted_at IS NULL
3839
+ AND (
3840
+ LOWER(slug) = LOWER($1)
3841
+ OR LOWER(name) = LOWER($1)
3842
+ )
3843
+ ORDER BY id ASC
3844
+ LIMIT 1`, normalizedLookup));
3845
+ return (_b = collaboratorTypes[0]) !== null && _b !== void 0 ? _b : null;
3846
+ }
2795
3847
  async getContractTemplateRecord(client, templateId, includeInactive = false) {
2796
3848
  const template = (await client.$queryRawUnsafe(`SELECT t.id,
2797
3849
  t.slug,
@@ -2893,7 +3945,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
2893
3945
  c.contract_category AS "contractCategory",
2894
3946
  m.display_name AS "managerName",
2895
3947
  MAX(CASE WHEN pa.collaborator_id = $2 THEN pa.id END)::int AS "myAssignmentId",
2896
- MAX(CASE WHEN pa.collaborator_id = $2 THEN pa.role_label END) AS "myRoleLabel",
3948
+ MAX(CASE WHEN pa.collaborator_id = $2 THEN COALESCE(project_role_locale.name, pa.role_label) END) AS "myRoleLabel",
2897
3949
  COUNT(DISTINCT pa.id)::int AS "teamSize"
2898
3950
  FROM operations_project p
2899
3951
  LEFT JOIN operations_contract c ON c.id = p.contract_id
@@ -2901,6 +3953,16 @@ let OperationsService = OperationsService_1 = class OperationsService {
2901
3953
  LEFT JOIN operations_project_assignment pa
2902
3954
  ON pa.project_id = p.id
2903
3955
  AND pa.deleted_at IS NULL
3956
+ LEFT JOIN operations_project_role project_role
3957
+ ON project_role.id = pa.project_role_id
3958
+ AND project_role.deleted_at IS NULL
3959
+ LEFT JOIN LATERAL (
3960
+ SELECT prl.name
3961
+ FROM operations_project_role_locale prl
3962
+ WHERE prl.operations_project_role_id = project_role.id
3963
+ ORDER BY prl.id ASC
3964
+ LIMIT 1
3965
+ ) project_role_locale ON TRUE
2904
3966
  WHERE p.id = $1
2905
3967
  AND p.deleted_at IS NULL
2906
3968
  GROUP BY p.id, c.id, m.id`, [projectId, actorCollaboratorId !== null && actorCollaboratorId !== void 0 ? actorCollaboratorId : null]);
@@ -2911,7 +3973,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
2911
3973
  this.queryRows(`SELECT pa.id,
2912
3974
  pa.collaborator_id AS "collaboratorId",
2913
3975
  c.display_name AS "collaboratorName",
2914
- pa.role_label AS "roleLabel",
3976
+ pa.project_role_id AS "projectRoleId",
3977
+ COALESCE(project_role_locale.name, pa.role_label) AS "roleLabel",
2915
3978
  pa.allocation_percent AS "allocationPercent",
2916
3979
  pa.weekly_hours AS "weeklyHours",
2917
3980
  pa.is_billable AS "isBillable",
@@ -2920,6 +3983,16 @@ let OperationsService = OperationsService_1 = class OperationsService {
2920
3983
  pa.status
2921
3984
  FROM operations_project_assignment pa
2922
3985
  JOIN operations_collaborator c ON c.id = pa.collaborator_id
3986
+ LEFT JOIN operations_project_role project_role
3987
+ ON project_role.id = pa.project_role_id
3988
+ AND project_role.deleted_at IS NULL
3989
+ LEFT JOIN LATERAL (
3990
+ SELECT prl.name
3991
+ FROM operations_project_role_locale prl
3992
+ WHERE prl.operations_project_role_id = project_role.id
3993
+ ORDER BY prl.id ASC
3994
+ LIMIT 1
3995
+ ) project_role_locale ON TRUE
2923
3996
  WHERE pa.project_id = $1
2924
3997
  AND pa.deleted_at IS NULL
2925
3998
  ORDER BY c.display_name ASC`, [projectId]),
@@ -2982,11 +4055,14 @@ let OperationsService = OperationsService_1 = class OperationsService {
2982
4055
  person_record.name AS "personName",
2983
4056
  person_record.avatar_id AS "personAvatarId",
2984
4057
  c.code,
2985
- c.collaborator_type AS "collaboratorType",
4058
+ c.collaborator_type_id AS "collaboratorTypeId",
4059
+ collaborator_type.slug AS "collaboratorTypeSlug",
4060
+ collaborator_type.name AS "collaboratorType",
2986
4061
  COALESCE(NULLIF(c.display_name, ''), person_record.name) AS "displayName",
2987
4062
  c.department_id AS "departmentId",
2988
4063
  COALESCE(NULLIF(department_record.name, ''), NULLIF(c.department, '')) AS "department",
2989
- c.title,
4064
+ c.job_title_id AS "jobTitleId",
4065
+ COALESCE(NULLIF(job_title_record.name, ''), NULLIF(c.title, '')) AS "title",
2990
4066
  c.level_label AS "levelLabel",
2991
4067
  c.weekly_capacity_hours AS "weeklyCapacityHours",
2992
4068
  c.status,
@@ -2997,13 +4073,20 @@ let OperationsService = OperationsService_1 = class OperationsService {
2997
4073
  s.display_name AS "supervisorName",
2998
4074
  hiring_contract.id AS "contractId",
2999
4075
  hiring_contract.status AS "contractStatus",
4076
+ hiring_contract.budget_amount AS "compensationAmount",
3000
4077
  COUNT(DISTINCT pa.id)::int AS "activeAssignments"
3001
4078
  FROM operations_collaborator c
3002
4079
  LEFT JOIN person person_record
3003
4080
  ON person_record.id = c.person_id
4081
+ LEFT JOIN operations_collaborator_type collaborator_type
4082
+ ON collaborator_type.id = c.collaborator_type_id
4083
+ AND collaborator_type.deleted_at IS NULL
3004
4084
  LEFT JOIN operations_department department_record
3005
4085
  ON department_record.id = c.department_id
3006
4086
  AND department_record.deleted_at IS NULL
4087
+ LEFT JOIN operations_job_title job_title_record
4088
+ ON job_title_record.id = c.job_title_id
4089
+ AND job_title_record.deleted_at IS NULL
3007
4090
  LEFT JOIN operations_collaborator s
3008
4091
  ON s.id = c.supervisor_collaborator_id
3009
4092
  LEFT JOIN operations_project_assignment pa
@@ -3011,7 +4094,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
3011
4094
  AND pa.deleted_at IS NULL
3012
4095
  AND pa.status IN ('planned', 'active')
3013
4096
  LEFT JOIN LATERAL (
3014
- SELECT oc.id, oc.status
4097
+ SELECT oc.id, oc.status, oc.budget_amount
3015
4098
  FROM operations_contract oc
3016
4099
  WHERE oc.related_collaborator_id = c.id
3017
4100
  AND oc.deleted_at IS NULL
@@ -3021,22 +4104,33 @@ let OperationsService = OperationsService_1 = class OperationsService {
3021
4104
  ) hiring_contract ON TRUE
3022
4105
  WHERE c.id = $1
3023
4106
  AND c.deleted_at IS NULL
3024
- GROUP BY c.id, person_record.id, department_record.id, s.id, hiring_contract.id, hiring_contract.status`, [collaboratorId]);
4107
+ GROUP BY c.id, person_record.id, collaborator_type.id, department_record.id, job_title_record.id, s.id, hiring_contract.id, hiring_contract.status, hiring_contract.budget_amount`, [collaboratorId]);
3025
4108
  if (!collaborator) {
3026
4109
  throw new common_1.NotFoundException('Collaborator not found.');
3027
4110
  }
3028
- const [assignedProjects, relatedContracts, weeklySchedule, timesheetSummary, timeOffSummary, scheduleAdjustmentRequests] = await Promise.all([
4111
+ const [assignedProjects, relatedContracts, weeklySchedule, equityParticipation, timesheetSummary, timeOffSummary, scheduleAdjustmentRequests] = await Promise.all([
3029
4112
  this.queryRows(`SELECT p.id,
3030
4113
  p.code,
3031
4114
  p.name,
3032
4115
  p.status,
3033
- pa.role_label AS "roleLabel",
4116
+ pa.project_role_id AS "projectRoleId",
4117
+ COALESCE(project_role_locale.name, pa.role_label) AS "roleLabel",
3034
4118
  pa.allocation_percent AS "allocationPercent",
3035
4119
  pa.weekly_hours AS "weeklyHours",
3036
4120
  pa.start_date AS "startDate",
3037
4121
  pa.end_date AS "endDate"
3038
4122
  FROM operations_project_assignment pa
3039
4123
  JOIN operations_project p ON p.id = pa.project_id
4124
+ LEFT JOIN operations_project_role project_role
4125
+ ON project_role.id = pa.project_role_id
4126
+ AND project_role.deleted_at IS NULL
4127
+ LEFT JOIN LATERAL (
4128
+ SELECT prl.name
4129
+ FROM operations_project_role_locale prl
4130
+ WHERE prl.operations_project_role_id = project_role.id
4131
+ ORDER BY prl.id ASC
4132
+ LIMIT 1
4133
+ ) project_role_locale ON TRUE
3040
4134
  WHERE pa.collaborator_id = $1
3041
4135
  AND pa.deleted_at IS NULL
3042
4136
  AND p.deleted_at IS NULL
@@ -3068,6 +4162,17 @@ let OperationsService = OperationsService_1 = class OperationsService {
3068
4162
  WHERE collaborator_id = $1
3069
4163
  AND deleted_at IS NULL
3070
4164
  ORDER BY id ASC`, [collaboratorId]),
4165
+ this.querySingle(`SELECT participation_type AS "participationType",
4166
+ percentage,
4167
+ voting_power AS "votingPower",
4168
+ start_date AS "startDate",
4169
+ end_date AS "endDate",
4170
+ notes
4171
+ FROM operations_collaborator_equity_participation
4172
+ WHERE collaborator_id = $1
4173
+ AND deleted_at IS NULL
4174
+ ORDER BY updated_at DESC, id DESC
4175
+ LIMIT 1`, [collaboratorId]),
3071
4176
  this.querySingle(`SELECT COUNT(*)::text AS "totalTimesheets",
3072
4177
  COUNT(*) FILTER (WHERE status = 'submitted')::text AS "pendingTimesheets",
3073
4178
  COALESCE(SUM(total_hours), 0)::text AS "totalHours"
@@ -3093,7 +4198,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
3093
4198
  ]);
3094
4199
  return Object.assign(Object.assign({}, collaborator), { assignedProjects,
3095
4200
  relatedContracts,
3096
- weeklySchedule, timesheetSummary: {
4201
+ weeklySchedule,
4202
+ equityParticipation, timesheetSummary: {
3097
4203
  totalTimesheets: Number((_a = timesheetSummary === null || timesheetSummary === void 0 ? void 0 : timesheetSummary.totalTimesheets) !== null && _a !== void 0 ? _a : 0),
3098
4204
  pendingTimesheets: Number((_b = timesheetSummary === null || timesheetSummary === void 0 ? void 0 : timesheetSummary.pendingTimesheets) !== null && _b !== void 0 ? _b : 0),
3099
4205
  totalHours: Number((_c = timesheetSummary === null || timesheetSummary === void 0 ? void 0 : timesheetSummary.totalHours) !== null && _c !== void 0 ? _c : 0),
@@ -3119,6 +4225,156 @@ let OperationsService = OperationsService_1 = class OperationsService {
3119
4225
  AND status IN ('planned', 'active')
3120
4226
  AND collaborator_id = ANY($1::int[])`, [collaboratorIds])).map((row) => row.projectId);
3121
4227
  }
4228
+ async resolveOwnedProjectAssignment(client, collaboratorId, input) {
4229
+ if (!input.projectId && !input.projectAssignmentId) {
4230
+ throw new common_1.BadRequestException('Either projectId or projectAssignmentId is required.');
4231
+ }
4232
+ const params = [collaboratorId];
4233
+ const filters = [
4234
+ 'pa.deleted_at IS NULL',
4235
+ 'p.deleted_at IS NULL',
4236
+ `pa.collaborator_id = $1`,
4237
+ `pa.status IN ('planned', 'active')`,
4238
+ ];
4239
+ if (input.projectAssignmentId) {
4240
+ filters.push(`pa.id = ${this.param(params, input.projectAssignmentId)}`);
4241
+ }
4242
+ if (input.projectId) {
4243
+ filters.push(`pa.project_id = ${this.param(params, input.projectId)}`);
4244
+ }
4245
+ const assignment = (await client.$queryRawUnsafe(`SELECT pa.id,
4246
+ pa.project_id AS "projectId",
4247
+ p.name AS "projectName",
4248
+ p.code AS "projectCode",
4249
+ pa.role_label AS "roleLabel"
4250
+ FROM operations_project_assignment pa
4251
+ JOIN operations_project p
4252
+ ON p.id = pa.project_id
4253
+ WHERE ${filters.join(' AND ')}
4254
+ ORDER BY CASE WHEN pa.status = 'active' THEN 0 ELSE 1 END,
4255
+ pa.start_date DESC NULLS LAST,
4256
+ pa.id DESC
4257
+ LIMIT 1`, ...params));
4258
+ if (!assignment[0]) {
4259
+ throw new common_1.ForbiddenException('The selected project is not assigned to the authenticated collaborator.');
4260
+ }
4261
+ return assignment[0];
4262
+ }
4263
+ async getOwnedTaskRecord(client, collaboratorId, taskId) {
4264
+ const task = (await client.$queryRawUnsafe(`SELECT t.id,
4265
+ t.name,
4266
+ t.description,
4267
+ t.status,
4268
+ t.project_assignment_id AS "projectAssignmentId",
4269
+ pa.project_id AS "projectId",
4270
+ p.name AS "projectName",
4271
+ p.code AS "projectCode"
4272
+ FROM operations_task t
4273
+ JOIN operations_project_assignment pa
4274
+ ON pa.id = t.project_assignment_id
4275
+ AND pa.deleted_at IS NULL
4276
+ JOIN operations_project p
4277
+ ON p.id = pa.project_id
4278
+ AND p.deleted_at IS NULL
4279
+ WHERE t.id = $1
4280
+ AND t.deleted_at IS NULL
4281
+ AND pa.collaborator_id = $2
4282
+ LIMIT 1`, taskId, collaboratorId));
4283
+ if (!task[0]) {
4284
+ throw new common_1.ForbiddenException('The selected task is not assigned to the authenticated collaborator.');
4285
+ }
4286
+ return task[0];
4287
+ }
4288
+ async getTaskOptionById(collaboratorId, taskId) {
4289
+ const task = await this.getOwnedTaskRecord(this.prisma, collaboratorId, taskId);
4290
+ return Object.assign(Object.assign({}, task), { label: [task.name, task.projectName].filter(Boolean).join(' • ') });
4291
+ }
4292
+ async getOrCreateTimesheetForWorkDate(client, collaboratorId, workDate) {
4293
+ var _a, _b, _c;
4294
+ const normalizedWorkDate = this.normalizeDateOnly(workDate);
4295
+ const { weekStartDate, weekEndDate } = this.getWorkWeekRange(normalizedWorkDate);
4296
+ const existing = (await client.$queryRawUnsafe(`SELECT id, status
4297
+ FROM operations_timesheet
4298
+ WHERE collaborator_id = $1
4299
+ AND week_start_date = $2::date
4300
+ AND week_end_date = $3::date
4301
+ AND deleted_at IS NULL
4302
+ ORDER BY id DESC
4303
+ LIMIT 1`, collaboratorId, weekStartDate, weekEndDate));
4304
+ if (existing[0]) {
4305
+ if (!['draft', 'rejected'].includes(existing[0].status)) {
4306
+ throw new common_1.BadRequestException('The timesheet for this week has already been submitted or approved.');
4307
+ }
4308
+ return existing[0].id;
4309
+ }
4310
+ const collaborator = await this.getCollaboratorById(collaboratorId);
4311
+ const created = (await client.$queryRawUnsafe(`INSERT INTO operations_timesheet (
4312
+ collaborator_id,
4313
+ approver_collaborator_id,
4314
+ week_start_date,
4315
+ week_end_date,
4316
+ notes,
4317
+ status,
4318
+ created_at,
4319
+ updated_at
4320
+ ) VALUES (
4321
+ $1,
4322
+ $2,
4323
+ $3::date,
4324
+ $4::date,
4325
+ NULL,
4326
+ 'draft',
4327
+ NOW(),
4328
+ NOW()
4329
+ )
4330
+ RETURNING id`, collaboratorId, (_a = collaborator.supervisorId) !== null && _a !== void 0 ? _a : null, weekStartDate, weekEndDate));
4331
+ return (_c = (_b = created[0]) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : 0;
4332
+ }
4333
+ async getTimesheetEntryByIdForActor(actor, entryId) {
4334
+ const filter = this.buildIdFilter(actor.visibleCollaboratorIds, 't.collaborator_id', actor.isDirector);
4335
+ const params = [...filter.params, entryId];
4336
+ const entry = await this.querySingle(`SELECT e.id,
4337
+ e.timesheet_id AS "timesheetId",
4338
+ t.collaborator_id AS "collaboratorId",
4339
+ pa.project_id AS "projectId",
4340
+ e.project_assignment_id AS "projectAssignmentId",
4341
+ p.code AS "projectCode",
4342
+ p.name AS "projectName",
4343
+ e.task_id AS "taskId",
4344
+ COALESCE(task_record.name, e.activity_label) AS "taskName",
4345
+ e.activity_label AS "activityLabel",
4346
+ e.work_date AS "workDate",
4347
+ COALESCE(NULLIF(e.duration_minutes, 0), ROUND(COALESCE(e.hours, 0)::numeric * 60))::int AS "durationMinutes",
4348
+ COALESCE(
4349
+ e.hours,
4350
+ ROUND((COALESCE(NULLIF(e.duration_minutes, 0), 0)::numeric / 60), 2)
4351
+ ) AS hours,
4352
+ e.description,
4353
+ t.status,
4354
+ t.week_start_date AS "weekStartDate",
4355
+ t.week_end_date AS "weekEndDate",
4356
+ e.created_at AS "createdAt"
4357
+ FROM operations_timesheet_entry e
4358
+ JOIN operations_timesheet t
4359
+ ON t.id = e.timesheet_id
4360
+ LEFT JOIN operations_project_assignment pa
4361
+ ON pa.id = e.project_assignment_id
4362
+ LEFT JOIN operations_project p
4363
+ ON p.id = pa.project_id
4364
+ LEFT JOIN operations_task task_record
4365
+ ON task_record.id = e.task_id
4366
+ AND task_record.deleted_at IS NULL
4367
+ WHERE e.deleted_at IS NULL
4368
+ AND ${filter.clause}
4369
+ AND e.id = $${params.length}
4370
+ LIMIT 1`, params);
4371
+ if (!entry) {
4372
+ throw new common_1.NotFoundException('Timesheet entry not found.');
4373
+ }
4374
+ return Object.assign(Object.assign({}, entry), { label: [entry.projectCode, entry.projectName, entry.taskName]
4375
+ .filter(Boolean)
4376
+ .join(' • ') });
4377
+ }
3122
4378
  async getTimesheetById(timesheetId) {
3123
4379
  const timesheet = await this.querySingle(`SELECT id,
3124
4380
  collaborator_id AS "collaboratorId",
@@ -3133,7 +4389,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
3133
4389
  return timesheet;
3134
4390
  }
3135
4391
  async replaceTimesheetEntries(client, timesheetId, entries, collaboratorId) {
3136
- var _a, _b, _c;
4392
+ var _a, _b, _c, _d, _e, _f;
3137
4393
  await client.$executeRawUnsafe(`UPDATE operations_timesheet_entry
3138
4394
  SET deleted_at = NOW()
3139
4395
  WHERE timesheet_id = $1
@@ -3159,22 +4415,54 @@ let OperationsService = OperationsService_1 = class OperationsService {
3159
4415
  }
3160
4416
  }
3161
4417
  for (const entry of entries) {
4418
+ const durationMinutes = this.resolveEntryDurationMinutes(entry);
4419
+ const hours = Number((durationMinutes / 60).toFixed(2));
4420
+ const resolvedTask = entry.taskId
4421
+ ? await this.getOwnedTaskRecord(client, collaboratorId, entry.taskId)
4422
+ : null;
4423
+ if (resolvedTask &&
4424
+ entry.projectAssignmentId &&
4425
+ resolvedTask.projectAssignmentId !== entry.projectAssignmentId) {
4426
+ throw new common_1.BadRequestException('The selected task does not belong to the chosen project assignment.');
4427
+ }
4428
+ const resolvedAssignmentId = (_b = (_a = entry.projectAssignmentId) !== null && _a !== void 0 ? _a : resolvedTask === null || resolvedTask === void 0 ? void 0 : resolvedTask.projectAssignmentId) !== null && _b !== void 0 ? _b : null;
4429
+ if (entry.taskId && !resolvedAssignmentId) {
4430
+ throw new common_1.BadRequestException('The selected task must belong to a project assignment.');
4431
+ }
4432
+ const activityLabel = (_d = (_c = resolvedTask === null || resolvedTask === void 0 ? void 0 : resolvedTask.name) !== null && _c !== void 0 ? _c : this.normalizeOptionalText(entry.taskName)) !== null && _d !== void 0 ? _d : this.normalizeOptionalText(entry.activityLabel);
4433
+ if (!entry.workDate) {
4434
+ throw new common_1.BadRequestException('Timesheet entry workDate is required.');
4435
+ }
3162
4436
  await client.$executeRawUnsafe(`INSERT INTO operations_timesheet_entry (
3163
4437
  timesheet_id,
3164
4438
  project_assignment_id,
4439
+ task_id,
3165
4440
  activity_label,
3166
4441
  work_date,
4442
+ duration_minutes,
3167
4443
  hours,
3168
4444
  description,
3169
4445
  created_at,
3170
4446
  updated_at
3171
- ) VALUES ($1, $2, $3, $4::date, $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);
4447
+ ) VALUES ($1, $2, $3, $4, $5::date, $6, $7, $8, NOW(), NOW())`, timesheetId, resolvedAssignmentId, (_f = (_e = resolvedTask === null || resolvedTask === void 0 ? void 0 : resolvedTask.id) !== null && _e !== void 0 ? _e : entry.taskId) !== null && _f !== void 0 ? _f : null, activityLabel !== null && activityLabel !== void 0 ? activityLabel : null, entry.workDate, durationMinutes, hours, this.normalizeOptionalText(entry.description));
3172
4448
  }
3173
4449
  }
3174
4450
  async refreshTimesheetTotal(client, timesheetId) {
3175
4451
  await client.$executeRawUnsafe(`UPDATE operations_timesheet
3176
4452
  SET total_hours = (
3177
- SELECT COALESCE(SUM(hours), 0)
4453
+ SELECT COALESCE(
4454
+ SUM(
4455
+ COALESCE(
4456
+ CASE
4457
+ WHEN duration_minutes IS NOT NULL AND duration_minutes > 0
4458
+ THEN duration_minutes::numeric / 60
4459
+ ELSE hours
4460
+ END,
4461
+ 0
4462
+ )
4463
+ ),
4464
+ 0
4465
+ )
3178
4466
  FROM operations_timesheet_entry
3179
4467
  WHERE timesheet_id = $1
3180
4468
  AND deleted_at IS NULL
@@ -3222,6 +4510,30 @@ let OperationsService = OperationsService_1 = class OperationsService {
3222
4510
  }
3223
4511
  await this.insertApprovalHistory(client, approvalId, input.requesterCollaboratorId, 'submitted', null);
3224
4512
  }
4513
+ async applyApprovedScheduleAdjustmentIfNeeded(client, requestId) {
4514
+ const request = (await client.$queryRawUnsafe(`SELECT collaborator_id AS "collaboratorId",
4515
+ request_scope AS "requestScope"
4516
+ FROM operations_schedule_adjustment_request
4517
+ WHERE id = $1
4518
+ AND deleted_at IS NULL
4519
+ LIMIT 1`, requestId));
4520
+ const currentRequest = request[0];
4521
+ if (!currentRequest || currentRequest.requestScope !== 'permanent') {
4522
+ return;
4523
+ }
4524
+ const days = (await client.$queryRawUnsafe(`SELECT weekday,
4525
+ is_working_day AS "isWorkingDay",
4526
+ start_time AS "startTime",
4527
+ end_time AS "endTime",
4528
+ break_minutes AS "breakMinutes"
4529
+ FROM operations_schedule_adjustment_day
4530
+ WHERE schedule_adjustment_request_id = $1
4531
+ ORDER BY id ASC`, requestId));
4532
+ if (!days.length) {
4533
+ return;
4534
+ }
4535
+ await this.replaceCollaboratorScheduleDays(client, currentRequest.collaboratorId, days);
4536
+ }
3225
4537
  async insertApprovalHistory(client, approvalId, actorCollaboratorId, action, note) {
3226
4538
  await client.$executeRawUnsafe(`INSERT INTO operations_approval_history (
3227
4539
  approval_id,
@@ -3238,19 +4550,10 @@ let OperationsService = OperationsService_1 = class OperationsService {
3238
4550
  )`, approvalId, actorCollaboratorId, action, note);
3239
4551
  }
3240
4552
  async assertProjectAccess(actor, projectId) {
3241
- if (actor.isDirector)
3242
- return;
3243
- if (!actor.visibleProjectIds.includes(projectId)) {
3244
- throw new common_1.ForbiddenException('You do not have access to this project.');
3245
- }
4553
+ this.accessService.ensureProjectAccess(actor, projectId);
3246
4554
  }
3247
4555
  ensureCollaboratorAccess(actor, collaboratorId) {
3248
- if (actor.isDirector) {
3249
- return;
3250
- }
3251
- if (!actor.visibleCollaboratorIds.includes(collaboratorId)) {
3252
- throw new common_1.ForbiddenException('You do not have access to this collaborator.');
3253
- }
4556
+ this.accessService.ensureCollaboratorAccess(actor, collaboratorId);
3254
4557
  }
3255
4558
  async listSingleTimesheet(actor, timesheetId) {
3256
4559
  const timesheets = await this.listTimesheets(actor.userId);
@@ -3261,19 +4564,13 @@ let OperationsService = OperationsService_1 = class OperationsService {
3261
4564
  return timesheet;
3262
4565
  }
3263
4566
  ensureDirector(actor) {
3264
- if (!actor.isDirector) {
3265
- throw new common_1.ForbiddenException('Director access is required.');
3266
- }
4567
+ this.accessService.ensureDirector(actor);
3267
4568
  }
3268
4569
  ensureSupervisor(actor) {
3269
- if (!actor.isSupervisor) {
3270
- throw new common_1.ForbiddenException('Supervisor access is required.');
3271
- }
4570
+ this.accessService.ensureSupervisor(actor);
3272
4571
  }
3273
4572
  ensureCollaborator(actor) {
3274
- if (!actor.isCollaborator) {
3275
- throw new common_1.ForbiddenException('Operations collaborator access is required.');
3276
- }
4573
+ this.accessService.ensureCollaborator(actor);
3277
4574
  }
3278
4575
  defaultWeeklySchedule() {
3279
4576
  return [
@@ -3352,16 +4649,22 @@ let OperationsService = OperationsService_1 = class OperationsService {
3352
4649
  }
3353
4650
  }
3354
4651
  async replaceProjectAssignments(client, projectId, teamAssignments) {
3355
- var _a, _b, _c, _d, _e, _f, _g;
4652
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
3356
4653
  await client.$executeRawUnsafe(`UPDATE operations_project_assignment
3357
4654
  SET deleted_at = NOW(),
3358
4655
  updated_at = NOW()
3359
4656
  WHERE project_id = $1
3360
4657
  AND deleted_at IS NULL`, projectId);
3361
4658
  for (const assignment of teamAssignments !== null && teamAssignments !== void 0 ? teamAssignments : []) {
4659
+ const projectRole = typeof assignment.projectRoleId === 'number' &&
4660
+ Number.isInteger(assignment.projectRoleId) &&
4661
+ assignment.projectRoleId > 0
4662
+ ? await this.getProjectRoleById(client, assignment.projectRoleId)
4663
+ : null;
3362
4664
  await client.$executeRawUnsafe(`INSERT INTO operations_project_assignment (
3363
4665
  project_id,
3364
4666
  collaborator_id,
4667
+ project_role_id,
3365
4668
  role_label,
3366
4669
  allocation_percent,
3367
4670
  weekly_hours,
@@ -3372,30 +4675,98 @@ let OperationsService = OperationsService_1 = class OperationsService {
3372
4675
  created_at,
3373
4676
  updated_at
3374
4677
  ) VALUES (
3375
- $1, $2, $3, $4, $5, $6, $7::date, $8::date,
3376
- $9::operations_project_assignment_status_155b459bbf_enum,
4678
+ $1, $2, $3, $4, $5, $6, $7, $8::date, $9::date,
4679
+ $10::operations_project_assignment_status_155b459bbf_enum,
3377
4680
  NOW(), NOW()
3378
- )`, 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');
4681
+ )`, projectId, assignment.collaboratorId, (_a = projectRole === null || projectRole === void 0 ? void 0 : projectRole.id) !== null && _a !== void 0 ? _a : null, (_c = (_b = this.normalizeOptionalText(assignment.roleLabel)) !== null && _b !== void 0 ? _b : projectRole === null || projectRole === void 0 ? void 0 : projectRole.name) !== null && _c !== void 0 ? _c : 'Team Member', (_d = assignment.allocationPercent) !== null && _d !== void 0 ? _d : null, (_e = assignment.weeklyHours) !== null && _e !== void 0 ? _e : null, (_f = assignment.isBillable) !== null && _f !== void 0 ? _f : true, (_g = assignment.startDate) !== null && _g !== void 0 ? _g : null, (_h = assignment.endDate) !== null && _h !== void 0 ? _h : null, (_j = assignment.status) !== null && _j !== void 0 ? _j : 'active');
4682
+ }
4683
+ }
4684
+ async replaceCollaboratorEquityParticipation(client, collaboratorId, equityParticipation) {
4685
+ var _a, _b, _c, _d, _e;
4686
+ if (equityParticipation === undefined) {
4687
+ return;
4688
+ }
4689
+ await client.$executeRawUnsafe(`UPDATE operations_collaborator_equity_participation
4690
+ SET deleted_at = COALESCE(deleted_at, NOW()),
4691
+ updated_at = NOW()
4692
+ WHERE collaborator_id = $1
4693
+ AND deleted_at IS NULL`, collaboratorId);
4694
+ if (equityParticipation === null) {
4695
+ return;
4696
+ }
4697
+ const participationType = (_a = equityParticipation.participationType) !== null && _a !== void 0 ? _a : 'other';
4698
+ await client.$executeRawUnsafe(`INSERT INTO operations_collaborator_equity_participation (
4699
+ collaborator_id,
4700
+ participation_type,
4701
+ percentage,
4702
+ voting_power,
4703
+ start_date,
4704
+ end_date,
4705
+ notes,
4706
+ deleted_at,
4707
+ created_at,
4708
+ updated_at
4709
+ ) VALUES (
4710
+ $1,
4711
+ $2::operations_collaborator_equity_participation_pa_98792c1f1e_enum,
4712
+ $3,
4713
+ $4,
4714
+ $5::date,
4715
+ $6::date,
4716
+ $7,
4717
+ NULL,
4718
+ NOW(),
4719
+ NOW()
4720
+ )`, collaboratorId, participationType, (_b = equityParticipation.percentage) !== null && _b !== void 0 ? _b : null, (_c = equityParticipation.votingPower) !== null && _c !== void 0 ? _c : null, (_d = equityParticipation.startDate) !== null && _d !== void 0 ? _d : null, (_e = equityParticipation.endDate) !== null && _e !== void 0 ? _e : null, this.normalizeOptionalText(equityParticipation.notes));
4721
+ }
4722
+ normalizeCollaboratorTypeKey(collaboratorType) {
4723
+ const normalized = String(collaboratorType !== null && collaboratorType !== void 0 ? collaboratorType : '')
4724
+ .trim()
4725
+ .toLowerCase()
4726
+ .normalize('NFD')
4727
+ .replace(/[\u0300-\u036f]/g, '');
4728
+ switch (normalized) {
4729
+ case 'estagiario':
4730
+ return 'intern';
4731
+ case 'outro':
4732
+ return 'other';
4733
+ default:
4734
+ return normalized;
3379
4735
  }
3380
4736
  }
3381
4737
  mapContractCategoryForCollaboratorType(collaboratorType) {
3382
- switch (collaboratorType) {
4738
+ switch (this.normalizeCollaboratorTypeKey(collaboratorType)) {
3383
4739
  case 'clt':
4740
+ case 'intern':
3384
4741
  return 'employee';
3385
4742
  case 'pj':
3386
4743
  case 'freelancer':
3387
4744
  return 'contractor';
4745
+ case 'socio':
4746
+ case 'acionista':
4747
+ case 'parceiro':
4748
+ return 'partner';
4749
+ case 'administrador':
4750
+ case 'diretor':
4751
+ case 'conselheiro':
4752
+ return 'internal';
3388
4753
  default:
3389
4754
  return 'other';
3390
4755
  }
3391
4756
  }
3392
4757
  mapBillingModelForCollaboratorType(collaboratorType) {
3393
- return collaboratorType === 'freelancer'
3394
- ? 'time_and_material'
3395
- : 'monthly_retainer';
4758
+ switch (this.normalizeCollaboratorTypeKey(collaboratorType)) {
4759
+ case 'freelancer':
4760
+ return 'time_and_material';
4761
+ case 'pj':
4762
+ case 'parceiro':
4763
+ return 'fixed_price';
4764
+ default:
4765
+ return 'monthly_retainer';
4766
+ }
3396
4767
  }
3397
4768
  mapContractTypeForCollaboratorType(collaboratorType) {
3398
- switch (collaboratorType) {
4769
+ switch (this.normalizeCollaboratorTypeKey(collaboratorType)) {
3399
4770
  case 'clt':
3400
4771
  return 'clt';
3401
4772
  case 'pj':
@@ -3404,16 +4775,39 @@ let OperationsService = OperationsService_1 = class OperationsService {
3404
4775
  return 'freelancer_agreement';
3405
4776
  case 'intern':
3406
4777
  return 'fixed_term';
4778
+ case 'socio':
4779
+ case 'acionista':
4780
+ case 'administrador':
4781
+ case 'diretor':
4782
+ case 'conselheiro':
4783
+ case 'parceiro':
4784
+ return 'service_agreement';
3407
4785
  default:
3408
4786
  return 'other';
3409
4787
  }
3410
4788
  }
4789
+ buildHiringContractName(displayName, collaboratorType) {
4790
+ switch (this.normalizeCollaboratorTypeKey(collaboratorType)) {
4791
+ case 'clt':
4792
+ return `${displayName} Employment Contract`;
4793
+ case 'intern':
4794
+ return `${displayName} Internship Agreement`;
4795
+ case 'socio':
4796
+ case 'acionista':
4797
+ case 'parceiro':
4798
+ return `${displayName} Partnership Agreement`;
4799
+ case 'administrador':
4800
+ case 'diretor':
4801
+ case 'conselheiro':
4802
+ return `${displayName} Executive Engagement Agreement`;
4803
+ default:
4804
+ return `${displayName} Service Contract`;
4805
+ }
4806
+ }
3411
4807
  async createHiringContractDraft(client, createdByUserId, input) {
3412
4808
  var _a, _b, _c;
3413
4809
  const contractCode = `HIR-${input.collaboratorCode}`;
3414
- const contractName = input.collaboratorType === 'clt'
3415
- ? `${input.displayName} Employment Contract`
3416
- : `${input.displayName} Service Contract`;
4810
+ const contractName = this.buildHiringContractName(input.displayName, input.collaboratorType);
3417
4811
  await client.$executeRawUnsafe(`INSERT INTO operations_contract (
3418
4812
  code,
3419
4813
  name,
@@ -4479,6 +5873,97 @@ let OperationsService = OperationsService_1 = class OperationsService {
4479
5873
  ? ''
4480
5874
  : String(value).trim();
4481
5875
  }
5876
+ normalizeDurationMinutes(duration, unit) {
5877
+ const numeric = Number(duration);
5878
+ if (!Number.isFinite(numeric) || numeric <= 0) {
5879
+ throw new common_1.BadRequestException('Duration must be greater than 0.');
5880
+ }
5881
+ const minutes = unit === 'hours' ? numeric * 60 : numeric;
5882
+ const rounded = Math.round(minutes);
5883
+ if (!Number.isFinite(rounded) || rounded <= 0) {
5884
+ throw new common_1.BadRequestException('Duration must be greater than 0.');
5885
+ }
5886
+ return rounded;
5887
+ }
5888
+ resolveEntryDurationMinutes(entry) {
5889
+ var _a;
5890
+ if (entry.durationMinutes !== undefined &&
5891
+ entry.durationMinutes !== null) {
5892
+ return this.normalizeDurationMinutes(entry.durationMinutes, 'minutes');
5893
+ }
5894
+ if (entry.duration !== undefined && entry.duration !== null) {
5895
+ return this.normalizeDurationMinutes(entry.duration, (_a = entry.unit) !== null && _a !== void 0 ? _a : 'minutes');
5896
+ }
5897
+ if (entry.hours !== undefined && entry.hours !== null) {
5898
+ return this.normalizeDurationMinutes(entry.hours, 'hours');
5899
+ }
5900
+ throw new common_1.BadRequestException('Timesheet entry duration is required.');
5901
+ }
5902
+ normalizeDateOnly(value) {
5903
+ const normalized = this.normalizeExtractionString(value);
5904
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized)) {
5905
+ throw new common_1.BadRequestException('Date values must use the YYYY-MM-DD format.');
5906
+ }
5907
+ return normalized;
5908
+ }
5909
+ parseDateOnly(value) {
5910
+ const normalized = this.normalizeDateOnly(value);
5911
+ const parsed = new Date(`${normalized}T00:00:00.000Z`);
5912
+ if (Number.isNaN(parsed.getTime())) {
5913
+ throw new common_1.BadRequestException('Invalid date value provided.');
5914
+ }
5915
+ return parsed;
5916
+ }
5917
+ formatDateOnly(value) {
5918
+ return value.toISOString().slice(0, 10);
5919
+ }
5920
+ getWorkWeekRange(workDate) {
5921
+ const date = this.parseDateOnly(workDate);
5922
+ const day = date.getUTCDay();
5923
+ const diffToMonday = day === 0 ? -6 : 1 - day;
5924
+ const weekStart = new Date(date);
5925
+ weekStart.setUTCDate(weekStart.getUTCDate() + diffToMonday);
5926
+ const weekEnd = new Date(weekStart);
5927
+ weekEnd.setUTCDate(weekStart.getUTCDate() + 6);
5928
+ return {
5929
+ weekStartDate: this.formatDateOnly(weekStart),
5930
+ weekEndDate: this.formatDateOnly(weekEnd),
5931
+ };
5932
+ }
5933
+ normalizePaginationParams(input = {}, options) {
5934
+ var _a, _b, _c, _d, _e, _f, _g;
5935
+ const page = Math.max(Number((_a = input.page) !== null && _a !== void 0 ? _a : 1) || 1, 1);
5936
+ const pageSize = Math.min(Math.max(Number((_b = input.pageSize) !== null && _b !== void 0 ? _b : 10) || 10, 1), 100);
5937
+ const search = (_c = this.normalizeOptionalText(input.search)) !== null && _c !== void 0 ? _c : '';
5938
+ const requestedSortField = this.normalizeOptionalText(input.sortField);
5939
+ const sortField = requestedSortField && options.allowedSortFields.includes(requestedSortField)
5940
+ ? requestedSortField
5941
+ : ((_e = (_d = options.defaultSortField) !== null && _d !== void 0 ? _d : options.allowedSortFields[0]) !== null && _e !== void 0 ? _e : 'id');
5942
+ const sortOrder = String((_g = (_f = input.sortOrder) !== null && _f !== void 0 ? _f : options.defaultSortOrder) !== null && _g !== void 0 ? _g : 'asc')
5943
+ .toLowerCase() === 'desc'
5944
+ ? 'desc'
5945
+ : 'asc';
5946
+ return {
5947
+ page,
5948
+ pageSize,
5949
+ search,
5950
+ sortField,
5951
+ sortOrder,
5952
+ offset: (page - 1) * pageSize,
5953
+ };
5954
+ }
5955
+ buildPaginationResult(data, total, page, pageSize) {
5956
+ const lastPage = Math.max(1, Math.ceil(total / Math.max(pageSize, 1)));
5957
+ return {
5958
+ total,
5959
+ lastPage,
5960
+ page,
5961
+ pageSize,
5962
+ prev: page > 1 ? page - 1 : null,
5963
+ next: page < lastPage ? page + 1 : null,
5964
+ data,
5965
+ };
5966
+ }
4482
5967
  normalizeExtractionBoolean(value) {
4483
5968
  if (typeof value === 'boolean') {
4484
5969
  return value;
@@ -4621,6 +6106,8 @@ exports.OperationsService = OperationsService = OperationsService_1 = __decorate
4621
6106
  core_1.AiService,
4622
6107
  core_1.IntegrationDeveloperApiService,
4623
6108
  core_1.FileService,
4624
- core_1.SettingService])
6109
+ core_1.SettingService,
6110
+ operations_access_service_1.OperationsAccessService,
6111
+ api_locale_1.LocaleService])
4625
6112
  ], OperationsService);
4626
6113
  //# sourceMappingURL=operations.service.js.map