@hed-hog/operations 0.0.303 → 0.0.305

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 (178) 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 +184 -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 +85 -0
  23. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -0
  24. package/dist/controllers/operations-tasks.controller.js +90 -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 +14 -0
  43. package/dist/dto/create-task.dto.d.ts.map +1 -0
  44. package/dist/dto/create-task.dto.js +83 -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 +14 -0
  83. package/dist/dto/update-task.dto.d.ts.map +1 -0
  84. package/dist/dto/update-task.dto.js +84 -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 +429 -8
  94. package/dist/operations.service.d.ts.map +1 -1
  95. package/dist/operations.service.js +1931 -165
  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 +196 -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 +125 -40
  113. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +740 -106
  114. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +256 -256
  115. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +7 -7
  116. package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +306 -306
  117. package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +247 -247
  118. package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +3520 -3520
  119. package/hedhog/frontend/app/_components/department-select-with-create.tsx.ejs +38 -16
  120. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +1504 -52
  121. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +1017 -649
  122. package/hedhog/frontend/app/_components/section-card.tsx.ejs +25 -18
  123. package/hedhog/frontend/app/_components/system-user-select-with-create.tsx.ejs +609 -0
  124. package/hedhog/frontend/app/_components/timesheet-task-create-sheet.tsx.ejs +213 -0
  125. package/hedhog/frontend/app/_lib/api.ts.ejs +30 -1
  126. package/hedhog/frontend/app/_lib/types.ts.ejs +147 -39
  127. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +40 -9
  128. package/hedhog/frontend/app/_lib/utils/forms.ts.ejs +48 -1
  129. package/hedhog/frontend/app/approvals/page.tsx.ejs +116 -98
  130. package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +502 -0
  131. package/hedhog/frontend/app/collaborators/page.tsx.ejs +116 -72
  132. package/hedhog/frontend/app/contracts/page.tsx.ejs +938 -938
  133. package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +11 -9
  134. package/hedhog/frontend/app/departments/page.tsx.ejs +1 -1
  135. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +1 -1
  136. package/hedhog/frontend/app/projects/page.tsx.ejs +364 -133
  137. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +244 -120
  138. package/hedhog/frontend/app/team/page.tsx.ejs +15 -2
  139. package/hedhog/frontend/app/time-off/page.tsx.ejs +158 -82
  140. package/hedhog/frontend/app/timesheets/page.tsx.ejs +814 -357
  141. package/hedhog/frontend/messages/en.json +268 -53
  142. package/hedhog/frontend/messages/pt.json +484 -271
  143. package/hedhog/table/operations_collaborator.yaml +26 -13
  144. package/hedhog/table/operations_collaborator_equity_participation.yaml +43 -0
  145. package/hedhog/table/operations_collaborator_type.yaml +33 -0
  146. package/hedhog/table/operations_job_title.yaml +24 -0
  147. package/hedhog/table/operations_project.yaml +9 -0
  148. package/hedhog/table/operations_project_assignment.yaml +9 -0
  149. package/hedhog/table/operations_project_role.yaml +39 -0
  150. package/hedhog/table/operations_task.yaml +69 -0
  151. package/hedhog/table/operations_timesheet_entry.yaml +12 -0
  152. package/package.json +6 -6
  153. package/src/controllers/operations-approvals.controller.ts +24 -0
  154. package/src/controllers/operations-collaborators.controller.ts +60 -0
  155. package/src/controllers/operations-contracts.controller.ts +138 -0
  156. package/src/controllers/operations-org-structure.controller.ts +92 -0
  157. package/src/controllers/operations-projects.controller.ts +50 -0
  158. package/src/controllers/operations-tasks.controller.ts +63 -0
  159. package/src/controllers/operations-timesheets.controller.ts +100 -0
  160. package/src/dto/create-collaborator-type.dto.ts +43 -0
  161. package/src/dto/create-collaborator.dto.ts +223 -0
  162. package/src/dto/create-schedule-adjustment-request.dto.ts +91 -0
  163. package/src/dto/create-task.dto.ts +75 -0
  164. package/src/dto/create-time-off-request.dto.ts +53 -0
  165. package/src/dto/create-timesheet-entry.dto.ts +67 -0
  166. package/src/dto/list-collaborator-types.dto.ts +15 -0
  167. package/src/dto/list-collaborators.dto.ts +30 -0
  168. package/src/dto/list-project-options.dto.ts +3 -0
  169. package/src/dto/list-tasks.dto.ts +25 -0
  170. package/src/dto/list-timesheet-entries.dto.ts +40 -0
  171. package/src/dto/update-collaborator-type.dto.ts +3 -0
  172. package/src/dto/update-collaborator.dto.ts +3 -0
  173. package/src/dto/update-task.dto.ts +76 -0
  174. package/src/operations.controller.ts +1 -278
  175. package/src/operations.module.ts +23 -2
  176. package/src/operations.service.spec.ts +450 -0
  177. package/src/operations.service.ts +4507 -1561
  178. 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 = ['todo', 'doing', 'review', 'done'];
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,23 @@ 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
+ }
837
+ if (data.compensationAmount !== undefined ||
838
+ data.contractDescription !== undefined ||
839
+ data.autoGenerateContractDraft !== undefined ||
840
+ data.joinedAt !== undefined ||
841
+ data.weeklyCapacityHours !== undefined ||
842
+ data.supervisorCollaboratorId !== undefined ||
843
+ data.collaboratorType !== undefined ||
844
+ data.collaboratorTypeId !== undefined ||
845
+ data.collaboratorTypeSlug !== undefined ||
846
+ data.code !== undefined ||
847
+ data.personId !== undefined ||
848
+ data.displayName !== undefined) {
849
+ await this.syncHiringContractDraft(tx, actor.userId, collaboratorId, data);
850
+ }
564
851
  });
565
852
  return this.getCollaboratorByIdForUser(userId, collaboratorId);
566
853
  }
@@ -590,6 +877,67 @@ let OperationsService = OperationsService_1 = class OperationsService {
590
877
  ORDER BY CASE WHEN d.deleted_at IS NULL THEN 0 ELSE 1 END ASC,
591
878
  d.name ASC`);
592
879
  }
880
+ async listJobTitles(userId) {
881
+ const actor = await this.getActorContext(userId);
882
+ this.ensureCollaborator(actor);
883
+ return this.queryRows(`SELECT jt.id,
884
+ jt.slug,
885
+ jt.code,
886
+ jt.name,
887
+ jt.description,
888
+ CASE WHEN jt.deleted_at IS NULL THEN 'active' ELSE 'inactive' END AS status,
889
+ COUNT(DISTINCT c.id)::int AS "collaboratorCount",
890
+ jt.created_at AS "createdAt",
891
+ jt.updated_at AS "updatedAt"
892
+ FROM operations_job_title jt
893
+ LEFT JOIN operations_collaborator c
894
+ ON c.deleted_at IS NULL
895
+ AND (
896
+ c.job_title_id = jt.id
897
+ OR (
898
+ c.job_title_id IS NULL
899
+ AND LOWER(COALESCE(c.title, '')) = LOWER(jt.name)
900
+ )
901
+ )
902
+ GROUP BY jt.id
903
+ ORDER BY CASE WHEN jt.deleted_at IS NULL THEN 0 ELSE 1 END ASC,
904
+ jt.name ASC`);
905
+ }
906
+ async createJobTitle(userId, data) {
907
+ const actor = await this.getActorContext(userId);
908
+ this.ensureDirector(actor);
909
+ const name = this.normalizeOptionalText(data.name);
910
+ if (!name) {
911
+ throw new common_1.BadRequestException('Job title name is required.');
912
+ }
913
+ return this.prisma.$transaction(async (tx) => {
914
+ var _a, _b, _c, _d;
915
+ await this.assertJobTitleNameAvailable(tx, name);
916
+ const normalizedCode = (_b = (_a = this.normalizeOptionalText(data.code)) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : null;
917
+ if (normalizedCode) {
918
+ await this.assertJobTitleCodeAvailable(tx, normalizedCode);
919
+ }
920
+ const created = (await tx.$queryRawUnsafe(`INSERT INTO operations_job_title (
921
+ slug,
922
+ code,
923
+ name,
924
+ description,
925
+ deleted_at,
926
+ created_at,
927
+ updated_at
928
+ ) VALUES (
929
+ $1, $2, $3, $4,
930
+ CASE WHEN $5 = 'inactive' THEN NOW() ELSE NULL END,
931
+ NOW(), NOW()
932
+ )
933
+ RETURNING id`, await this.generateUniqueJobTitleSlug(tx, name), normalizedCode, name, this.normalizeOptionalText(data.description), (_c = data.status) !== null && _c !== void 0 ? _c : 'active'));
934
+ const createdJobTitleId = (_d = created[0]) === null || _d === void 0 ? void 0 : _d.id;
935
+ if (!createdJobTitleId) {
936
+ throw new common_1.BadRequestException('Unable to create the job title.');
937
+ }
938
+ return this.getJobTitleById(tx, createdJobTitleId, true);
939
+ });
940
+ }
593
941
  async createDepartment(userId, data) {
594
942
  const actor = await this.getActorContext(userId);
595
943
  this.ensureDirector(actor);
@@ -687,8 +1035,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
687
1035
  p.progress_percent AS "progressPercent",
688
1036
  p.delivery_model AS "deliveryModel",
689
1037
  p.budget_amount AS "budgetAmount",
690
- p.start_date AS "startDate",
691
- p.end_date AS "endDate",
1038
+ TO_CHAR(p.start_date, 'YYYY-MM-DD') AS "startDate",
1039
+ TO_CHAR(p.end_date, 'YYYY-MM-DD') AS "endDate",
692
1040
  c.name AS "contractName",
693
1041
  c.status AS "contractStatus",
694
1042
  m.display_name AS "managerName",
@@ -705,6 +1053,476 @@ let OperationsService = OperationsService_1 = class OperationsService {
705
1053
  GROUP BY p.id, c.id, m.id
706
1054
  ORDER BY p.name ASC`, [...assignmentParams, ...filter.params]);
707
1055
  }
1056
+ async listProjectOptions(userId, paginationParams) {
1057
+ var _a, _b;
1058
+ const actor = await this.getActorContext(userId);
1059
+ this.ensureCollaborator(actor);
1060
+ if (!actor.collaboratorId) {
1061
+ throw new common_1.BadRequestException('Collaborator context is required.');
1062
+ }
1063
+ const pagination = this.normalizePaginationParams(paginationParams, {
1064
+ defaultSortField: 'name',
1065
+ defaultSortOrder: 'asc',
1066
+ allowedSortFields: ['name', 'code', 'clientName', 'startDate', 'endDate'],
1067
+ });
1068
+ const params = [actor.collaboratorId];
1069
+ const filters = [
1070
+ 'p.deleted_at IS NULL',
1071
+ 'pa.deleted_at IS NULL',
1072
+ `pa.collaborator_id = $1`,
1073
+ `pa.status IN ('planned', 'active')`,
1074
+ ];
1075
+ if (pagination.search) {
1076
+ const searchPlaceholder = this.param(params, `%${pagination.search}%`);
1077
+ filters.push(`(
1078
+ COALESCE(p.name, '') ILIKE ${searchPlaceholder}
1079
+ OR COALESCE(p.code, '') ILIKE ${searchPlaceholder}
1080
+ OR COALESCE(p.client_name, '') ILIKE ${searchPlaceholder}
1081
+ OR COALESCE(pa.role_label, '') ILIKE ${searchPlaceholder}
1082
+ )`);
1083
+ }
1084
+ const whereClause = filters.join(' AND ');
1085
+ const totalRow = await this.querySingle(`SELECT COUNT(DISTINCT p.id)::text AS total
1086
+ FROM operations_project_assignment pa
1087
+ JOIN operations_project p
1088
+ ON p.id = pa.project_id
1089
+ WHERE ${whereClause}`, params);
1090
+ const sortColumn = (_a = {
1091
+ name: 'p.name',
1092
+ code: 'p.code',
1093
+ clientName: 'p.client_name',
1094
+ startDate: 'p.start_date',
1095
+ endDate: 'p.end_date',
1096
+ }[pagination.sortField]) !== null && _a !== void 0 ? _a : 'p.name';
1097
+ const queryParams = [...params];
1098
+ const limitPlaceholder = this.param(queryParams, pagination.pageSize);
1099
+ const offsetPlaceholder = this.param(queryParams, pagination.offset);
1100
+ const rows = await this.queryRows(`SELECT p.id,
1101
+ p.code,
1102
+ p.name,
1103
+ p.client_name AS "clientName",
1104
+ MAX(pa.id)::int AS "projectAssignmentId",
1105
+ MAX(pa.role_label) AS "roleLabel",
1106
+ p.status,
1107
+ TO_CHAR(p.start_date, 'YYYY-MM-DD') AS "startDate",
1108
+ TO_CHAR(p.end_date, 'YYYY-MM-DD') AS "endDate"
1109
+ FROM operations_project_assignment pa
1110
+ JOIN operations_project p
1111
+ ON p.id = pa.project_id
1112
+ WHERE ${whereClause}
1113
+ GROUP BY p.id
1114
+ ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, p.id ASC
1115
+ LIMIT ${limitPlaceholder}
1116
+ OFFSET ${offsetPlaceholder}`, queryParams);
1117
+ 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);
1118
+ }
1119
+ async listTasks(userId, paginationParams) {
1120
+ var _a, _b;
1121
+ const actor = await this.getActorContext(userId);
1122
+ this.ensureCollaborator(actor);
1123
+ if (!actor.collaboratorId) {
1124
+ throw new common_1.BadRequestException('Collaborator context is required.');
1125
+ }
1126
+ const pagination = this.normalizePaginationParams(paginationParams, {
1127
+ defaultSortField: 'name',
1128
+ defaultSortOrder: 'asc',
1129
+ allowedSortFields: ['name', 'projectName', 'status', 'createdAt'],
1130
+ });
1131
+ const params = [actor.collaboratorId];
1132
+ const filters = [
1133
+ 't.deleted_at IS NULL',
1134
+ 'pa.deleted_at IS NULL',
1135
+ 'p.deleted_at IS NULL',
1136
+ `pa.collaborator_id = $1`,
1137
+ `pa.status IN ('planned', 'active')`,
1138
+ ];
1139
+ if (pagination.search) {
1140
+ const searchPlaceholder = this.param(params, `%${pagination.search}%`);
1141
+ filters.push(`(
1142
+ COALESCE(t.name, '') ILIKE ${searchPlaceholder}
1143
+ OR COALESCE(t.description, '') ILIKE ${searchPlaceholder}
1144
+ OR COALESCE(p.name, '') ILIKE ${searchPlaceholder}
1145
+ OR COALESCE(p.code, '') ILIKE ${searchPlaceholder}
1146
+ )`);
1147
+ }
1148
+ if (paginationParams.projectAssignmentId) {
1149
+ filters.push(`pa.id = ${this.param(params, paginationParams.projectAssignmentId)}`);
1150
+ }
1151
+ if (paginationParams.projectId) {
1152
+ filters.push(`pa.project_id = ${this.param(params, paginationParams.projectId)}`);
1153
+ }
1154
+ if (paginationParams.status) {
1155
+ filters.push(`t.status = ${this.param(params, paginationParams.status)}`);
1156
+ }
1157
+ const whereClause = filters.join(' AND ');
1158
+ const totalRow = await this.querySingle(`SELECT COUNT(*)::text AS total
1159
+ FROM operations_task t
1160
+ JOIN operations_project_assignment pa
1161
+ ON pa.id = t.project_assignment_id
1162
+ JOIN operations_project p
1163
+ ON p.id = pa.project_id
1164
+ WHERE ${whereClause}`, params);
1165
+ const sortColumn = (_a = {
1166
+ name: 't.name',
1167
+ projectName: 'p.name',
1168
+ status: 't.status',
1169
+ createdAt: 't.created_at',
1170
+ }[pagination.sortField]) !== null && _a !== void 0 ? _a : 't.name';
1171
+ const queryParams = [...params];
1172
+ const limitPlaceholder = this.param(queryParams, pagination.pageSize);
1173
+ const offsetPlaceholder = this.param(queryParams, pagination.offset);
1174
+ const rows = await this.queryRows(`SELECT t.id,
1175
+ t.name,
1176
+ t.description,
1177
+ t.status,
1178
+ pa.project_id AS "projectId",
1179
+ pa.id AS "projectAssignmentId",
1180
+ p.name AS "projectName",
1181
+ p.code AS "projectCode",
1182
+ t.created_at AS "createdAt"
1183
+ FROM operations_task t
1184
+ JOIN operations_project_assignment pa
1185
+ ON pa.id = t.project_assignment_id
1186
+ JOIN operations_project p
1187
+ ON p.id = pa.project_id
1188
+ WHERE ${whereClause}
1189
+ ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, t.id ASC
1190
+ LIMIT ${limitPlaceholder}
1191
+ OFFSET ${offsetPlaceholder}`, queryParams);
1192
+ 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);
1193
+ }
1194
+ async createTask(userId, data) {
1195
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
1196
+ const actor = await this.getActorContext(userId);
1197
+ if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
1198
+ throw new common_1.ForbiddenException('Operations collaborator access is required.');
1199
+ }
1200
+ this.requireFields(data, ['name']);
1201
+ let assignmentId = null;
1202
+ let projectId = null;
1203
+ if (data.projectId || data.projectAssignmentId) {
1204
+ const assignment = await this.resolveProjectAssignmentForActor(this.prisma, actor, {
1205
+ projectId: (_a = data.projectId) !== null && _a !== void 0 ? _a : null,
1206
+ projectAssignmentId: (_b = data.projectAssignmentId) !== null && _b !== void 0 ? _b : null,
1207
+ });
1208
+ await this.assertProjectAccess(actor, assignment.projectId);
1209
+ assignmentId = assignment.id;
1210
+ projectId = assignment.projectId;
1211
+ }
1212
+ else if (data.projectId) {
1213
+ projectId = data.projectId;
1214
+ await this.assertProjectAccess(actor, projectId);
1215
+ }
1216
+ else {
1217
+ throw new common_1.BadRequestException('Either projectId or projectAssignmentId is required.');
1218
+ }
1219
+ if (!projectId) {
1220
+ projectId = (_c = data.projectId) !== null && _c !== void 0 ? _c : null;
1221
+ }
1222
+ const name = this.normalizeOptionalText(data.name);
1223
+ if (!name) {
1224
+ throw new common_1.BadRequestException('Field "name" is required.');
1225
+ }
1226
+ const maxPositionRow = await this.querySingle(`SELECT MAX(position) AS max_pos
1227
+ FROM operations_task
1228
+ WHERE project_id = $1
1229
+ AND status = $2::operations_task_status_574c143dbe_enum
1230
+ AND deleted_at IS NULL`, [projectId, (_d = data.status) !== null && _d !== void 0 ? _d : 'todo']);
1231
+ const nextPosition = ((_e = maxPositionRow === null || maxPositionRow === void 0 ? void 0 : maxPositionRow.max_pos) !== null && _e !== void 0 ? _e : -1) + 1;
1232
+ const created = await this.querySingle(`INSERT INTO operations_task (
1233
+ project_id,
1234
+ project_assignment_id,
1235
+ assignee_collaborator_id,
1236
+ name,
1237
+ description,
1238
+ priority,
1239
+ status,
1240
+ due_date,
1241
+ estimate_hours,
1242
+ position,
1243
+ tags,
1244
+ created_at,
1245
+ updated_at
1246
+ ) VALUES (
1247
+ $1, $2, $3, $4, $5,
1248
+ $6::operations_task_priority_394ab327eb_enum,
1249
+ $7::operations_task_status_574c143dbe_enum,
1250
+ $8::date, $9::decimal, $10, $11, NOW(), NOW()
1251
+ )
1252
+ RETURNING id`, [
1253
+ projectId,
1254
+ assignmentId,
1255
+ (_f = data.assigneeCollaboratorId) !== null && _f !== void 0 ? _f : null,
1256
+ name,
1257
+ this.normalizeOptionalText(data.description),
1258
+ (_g = data.priority) !== null && _g !== void 0 ? _g : 'medium',
1259
+ (_h = data.status) !== null && _h !== void 0 ? _h : 'todo',
1260
+ (_j = data.dueDate) !== null && _j !== void 0 ? _j : null,
1261
+ (_k = data.estimateHours) !== null && _k !== void 0 ? _k : null,
1262
+ (_l = data.position) !== null && _l !== void 0 ? _l : nextPosition,
1263
+ (_m = data.tags) !== null && _m !== void 0 ? _m : null,
1264
+ ]);
1265
+ return this.getProjectBoardTask((_o = created === null || created === void 0 ? void 0 : created.id) !== null && _o !== void 0 ? _o : 0);
1266
+ }
1267
+ async updateTask(userId, taskId, data) {
1268
+ const actor = await this.getActorContext(userId);
1269
+ if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
1270
+ throw new common_1.ForbiddenException('Operations collaborator access is required.');
1271
+ }
1272
+ const current = await this.getTaskRecordForActor(this.prisma, actor, taskId);
1273
+ await this.assertProjectAccess(actor, current.projectId);
1274
+ const nextName = data.name !== undefined
1275
+ ? this.normalizeOptionalText(data.name)
1276
+ : current.name;
1277
+ if (!nextName) {
1278
+ throw new common_1.BadRequestException('Field "name" is required.');
1279
+ }
1280
+ await this.prisma.$transaction(async (tx) => {
1281
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
1282
+ let nextAssignmentId = current.projectAssignmentId;
1283
+ let nextProjectId = current.projectId;
1284
+ if (data.projectId !== undefined || data.projectAssignmentId !== undefined) {
1285
+ const nextAssignment = await this.resolveProjectAssignmentForActor(tx, actor, {
1286
+ projectId: (_a = data.projectId) !== null && _a !== void 0 ? _a : null,
1287
+ projectAssignmentId: (_b = data.projectAssignmentId) !== null && _b !== void 0 ? _b : null,
1288
+ });
1289
+ await this.assertProjectAccess(actor, nextAssignment.projectId);
1290
+ nextAssignmentId = nextAssignment.id;
1291
+ nextProjectId = nextAssignment.projectId;
1292
+ }
1293
+ await tx.$executeRawUnsafe(`UPDATE operations_task
1294
+ SET project_id = $1,
1295
+ project_assignment_id = $2,
1296
+ assignee_collaborator_id = $3,
1297
+ name = $4,
1298
+ description = $5,
1299
+ priority = $6::operations_task_priority_394ab327eb_enum,
1300
+ status = $7::operations_task_status_574c143dbe_enum,
1301
+ due_date = $8::date,
1302
+ estimate_hours = $9::decimal,
1303
+ position = $10,
1304
+ tags = $11,
1305
+ updated_at = NOW()
1306
+ WHERE id = $12
1307
+ AND deleted_at IS NULL`, nextProjectId, nextAssignmentId, data.assigneeCollaboratorId !== undefined
1308
+ ? ((_c = data.assigneeCollaboratorId) !== null && _c !== void 0 ? _c : null)
1309
+ : current.assigneeCollaboratorId, nextName, data.description !== undefined
1310
+ ? this.normalizeOptionalText(data.description)
1311
+ : ((_d = current.description) !== null && _d !== void 0 ? _d : null), (_e = data.priority) !== null && _e !== void 0 ? _e : current.priority, (_f = data.status) !== null && _f !== void 0 ? _f : current.status, data.dueDate !== undefined ? ((_g = data.dueDate) !== null && _g !== void 0 ? _g : null) : current.dueDate, data.estimateHours !== undefined ? ((_h = data.estimateHours) !== null && _h !== void 0 ? _h : null) : current.estimateHours, data.position !== undefined ? data.position : current.position, data.tags !== undefined ? ((_j = data.tags) !== null && _j !== void 0 ? _j : null) : current.tags, taskId);
1312
+ });
1313
+ return this.getProjectBoardTask(taskId);
1314
+ }
1315
+ async removeTask(userId, taskId) {
1316
+ const actor = await this.getActorContext(userId);
1317
+ if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
1318
+ throw new common_1.ForbiddenException('Operations collaborator access is required.');
1319
+ }
1320
+ const current = await this.getTaskRecordForActor(this.prisma, actor, taskId);
1321
+ await this.assertProjectAccess(actor, current.projectId);
1322
+ await this.prisma.$transaction(async (tx) => {
1323
+ await tx.$executeRawUnsafe(`UPDATE operations_task
1324
+ SET deleted_at = COALESCE(deleted_at, NOW()),
1325
+ updated_at = NOW()
1326
+ WHERE id = $1
1327
+ AND deleted_at IS NULL`, taskId);
1328
+ });
1329
+ return { success: true };
1330
+ }
1331
+ async listTimesheetEntries(userId, paginationParams) {
1332
+ var _a, _b;
1333
+ const actor = await this.getActorContext(userId);
1334
+ this.ensureCollaborator(actor);
1335
+ if (!actor.collaboratorId) {
1336
+ throw new common_1.BadRequestException('Collaborator context is required.');
1337
+ }
1338
+ const pagination = this.normalizePaginationParams(paginationParams, {
1339
+ defaultSortField: 'workDate',
1340
+ defaultSortOrder: 'desc',
1341
+ allowedSortFields: [
1342
+ 'workDate',
1343
+ 'createdAt',
1344
+ 'durationMinutes',
1345
+ 'projectName',
1346
+ 'taskName',
1347
+ 'status',
1348
+ ],
1349
+ });
1350
+ const params = [actor.collaboratorId];
1351
+ const filters = [
1352
+ 'e.deleted_at IS NULL',
1353
+ 't.deleted_at IS NULL',
1354
+ `t.collaborator_id = $1`,
1355
+ ];
1356
+ if (pagination.search) {
1357
+ const searchPlaceholder = this.param(params, `%${pagination.search}%`);
1358
+ filters.push(`(
1359
+ COALESCE(p.name, '') ILIKE ${searchPlaceholder}
1360
+ OR COALESCE(p.code, '') ILIKE ${searchPlaceholder}
1361
+ OR COALESCE(task_record.name, '') ILIKE ${searchPlaceholder}
1362
+ OR COALESCE(e.activity_label, '') ILIKE ${searchPlaceholder}
1363
+ OR COALESCE(e.description, '') ILIKE ${searchPlaceholder}
1364
+ )`);
1365
+ }
1366
+ if (paginationParams.projectAssignmentId) {
1367
+ filters.push(`e.project_assignment_id = ${this.param(params, paginationParams.projectAssignmentId)}`);
1368
+ }
1369
+ if (paginationParams.projectId) {
1370
+ filters.push(`pa.project_id = ${this.param(params, paginationParams.projectId)}`);
1371
+ }
1372
+ if (paginationParams.taskId) {
1373
+ filters.push(`e.task_id = ${this.param(params, paginationParams.taskId)}`);
1374
+ }
1375
+ if (paginationParams.status) {
1376
+ filters.push(`t.status = ${this.param(params, paginationParams.status, 'operations_timesheet_status_128a8ecd30_enum')}`);
1377
+ }
1378
+ if (paginationParams.fromDate) {
1379
+ filters.push(`e.work_date >= ${this.param(params, paginationParams.fromDate, 'date')}`);
1380
+ }
1381
+ if (paginationParams.toDate) {
1382
+ filters.push(`e.work_date <= ${this.param(params, paginationParams.toDate, 'date')}`);
1383
+ }
1384
+ const whereClause = filters.join(' AND ');
1385
+ const totalRow = await this.querySingle(`SELECT COUNT(*)::text AS total
1386
+ FROM operations_timesheet_entry e
1387
+ JOIN operations_timesheet t
1388
+ ON t.id = e.timesheet_id
1389
+ LEFT JOIN operations_project_assignment pa
1390
+ ON pa.id = e.project_assignment_id
1391
+ LEFT JOIN operations_project p
1392
+ ON p.id = pa.project_id
1393
+ LEFT JOIN operations_task task_record
1394
+ ON task_record.id = e.task_id
1395
+ AND task_record.deleted_at IS NULL
1396
+ WHERE ${whereClause}`, params);
1397
+ const sortColumn = (_a = {
1398
+ workDate: 'e.work_date',
1399
+ createdAt: 'e.created_at',
1400
+ durationMinutes: 'COALESCE(NULLIF(e.duration_minutes, 0), ROUND(COALESCE(e.hours, 0)::numeric * 60))',
1401
+ projectName: 'p.name',
1402
+ taskName: 'COALESCE(task_record.name, e.activity_label)',
1403
+ status: 't.status',
1404
+ }[pagination.sortField]) !== null && _a !== void 0 ? _a : 'e.work_date';
1405
+ const queryParams = [...params];
1406
+ const limitPlaceholder = this.param(queryParams, pagination.pageSize);
1407
+ const offsetPlaceholder = this.param(queryParams, pagination.offset);
1408
+ const rows = await this.queryRows(`SELECT e.id,
1409
+ e.timesheet_id AS "timesheetId",
1410
+ t.collaborator_id AS "collaboratorId",
1411
+ pa.project_id AS "projectId",
1412
+ e.project_assignment_id AS "projectAssignmentId",
1413
+ p.code AS "projectCode",
1414
+ p.name AS "projectName",
1415
+ e.task_id AS "taskId",
1416
+ COALESCE(task_record.name, e.activity_label) AS "taskName",
1417
+ e.activity_label AS "activityLabel",
1418
+ e.work_date AS "workDate",
1419
+ COALESCE(NULLIF(e.duration_minutes, 0), ROUND(COALESCE(e.hours, 0)::numeric * 60))::int AS "durationMinutes",
1420
+ COALESCE(
1421
+ e.hours,
1422
+ ROUND((COALESCE(NULLIF(e.duration_minutes, 0), 0)::numeric / 60), 2)
1423
+ ) AS hours,
1424
+ e.description,
1425
+ t.status,
1426
+ t.week_start_date AS "weekStartDate",
1427
+ t.week_end_date AS "weekEndDate",
1428
+ e.created_at AS "createdAt"
1429
+ FROM operations_timesheet_entry e
1430
+ JOIN operations_timesheet t
1431
+ ON t.id = e.timesheet_id
1432
+ LEFT JOIN operations_project_assignment pa
1433
+ ON pa.id = e.project_assignment_id
1434
+ LEFT JOIN operations_project p
1435
+ ON p.id = pa.project_id
1436
+ LEFT JOIN operations_task task_record
1437
+ ON task_record.id = e.task_id
1438
+ AND task_record.deleted_at IS NULL
1439
+ WHERE ${whereClause}
1440
+ ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, e.id DESC
1441
+ LIMIT ${limitPlaceholder}
1442
+ OFFSET ${offsetPlaceholder}`, queryParams);
1443
+ return this.buildPaginationResult(rows.map((row) => (Object.assign(Object.assign({}, row), { label: [row.projectCode, row.projectName, row.taskName]
1444
+ .filter(Boolean)
1445
+ .join(' • ') }))), Number((_b = totalRow === null || totalRow === void 0 ? void 0 : totalRow.total) !== null && _b !== void 0 ? _b : 0), pagination.page, pagination.pageSize);
1446
+ }
1447
+ async createTimesheetEntry(userId, data) {
1448
+ var _a;
1449
+ const actor = await this.getActorContext(userId);
1450
+ this.ensureCollaborator(actor);
1451
+ this.requireFields(data, ['workDate', 'duration']);
1452
+ if (!actor.collaboratorId) {
1453
+ throw new common_1.BadRequestException('Collaborator context is required.');
1454
+ }
1455
+ const durationMinutes = this.normalizeDurationMinutes(data.duration, data.unit);
1456
+ const taskLabel = (_a = this.normalizeOptionalText(data.taskName)) !== null && _a !== void 0 ? _a : this.normalizeOptionalText(data.activityLabel);
1457
+ const createdEntryId = await this.prisma.$transaction(async (tx) => {
1458
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
1459
+ const assignment = await this.resolveOwnedProjectAssignment(tx, actor.collaboratorId, {
1460
+ projectId: (_a = data.projectId) !== null && _a !== void 0 ? _a : null,
1461
+ projectAssignmentId: (_b = data.projectAssignmentId) !== null && _b !== void 0 ? _b : null,
1462
+ });
1463
+ const resolvedTask = data.taskId
1464
+ ? await this.getOwnedTaskRecord(tx, actor.collaboratorId, data.taskId)
1465
+ : null;
1466
+ if (resolvedTask && resolvedTask.projectAssignmentId !== assignment.id) {
1467
+ throw new common_1.BadRequestException('The selected task does not belong to the chosen project assignment.');
1468
+ }
1469
+ 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;
1470
+ if (!activityLabel) {
1471
+ throw new common_1.BadRequestException('A task is required for the timesheet entry.');
1472
+ }
1473
+ const timesheetId = await this.getOrCreateTimesheetForWorkDate(tx, actor.collaboratorId, data.workDate);
1474
+ const created = (await tx.$queryRawUnsafe(`INSERT INTO operations_timesheet_entry (
1475
+ timesheet_id,
1476
+ project_assignment_id,
1477
+ task_id,
1478
+ activity_label,
1479
+ work_date,
1480
+ duration_minutes,
1481
+ hours,
1482
+ description,
1483
+ created_at,
1484
+ updated_at
1485
+ ) VALUES (
1486
+ $1,
1487
+ $2,
1488
+ $3,
1489
+ $4,
1490
+ $5::date,
1491
+ $6,
1492
+ $7,
1493
+ $8,
1494
+ NOW(),
1495
+ NOW()
1496
+ )
1497
+ 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)));
1498
+ await this.refreshTimesheetTotal(tx, timesheetId);
1499
+ return (_j = (_h = created[0]) === null || _h === void 0 ? void 0 : _h.id) !== null && _j !== void 0 ? _j : 0;
1500
+ });
1501
+ return this.getTimesheetEntryByIdForActor(actor, createdEntryId);
1502
+ }
1503
+ async removeTimesheetEntry(userId, entryId) {
1504
+ const actor = await this.getActorContext(userId);
1505
+ this.ensureCollaborator(actor);
1506
+ if (!actor.collaboratorId && !actor.isDirector) {
1507
+ throw new common_1.BadRequestException('Collaborator context is required.');
1508
+ }
1509
+ const entry = await this.getTimesheetEntryByIdForActor(actor, entryId);
1510
+ if (!actor.isDirector && entry.collaboratorId !== actor.collaboratorId) {
1511
+ throw new common_1.ForbiddenException('Only the entry owner can delete this timesheet entry.');
1512
+ }
1513
+ if (!['draft', 'rejected'].includes(entry.status)) {
1514
+ throw new common_1.BadRequestException('Only draft or rejected timesheet entries can be deleted.');
1515
+ }
1516
+ await this.prisma.$transaction(async (tx) => {
1517
+ await tx.$executeRawUnsafe(`UPDATE operations_timesheet_entry
1518
+ SET deleted_at = NOW(),
1519
+ updated_at = NOW()
1520
+ WHERE id = $1
1521
+ AND deleted_at IS NULL`, entryId);
1522
+ await this.refreshTimesheetTotal(tx, entry.timesheetId);
1523
+ });
1524
+ return { success: true };
1525
+ }
708
1526
  async getProjectById(userId, projectId) {
709
1527
  const actor = await this.getActorContext(userId);
710
1528
  await this.assertProjectAccess(actor, projectId);
@@ -715,10 +1533,11 @@ let OperationsService = OperationsService_1 = class OperationsService {
715
1533
  this.ensureDirector(actor);
716
1534
  this.requireFields(data, ['code', 'name']);
717
1535
  const createdProjectId = await this.prisma.$transaction(async (tx) => {
718
- 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;
1536
+ 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;
719
1537
  const created = await tx.$queryRawUnsafe(`INSERT INTO operations_project (
720
1538
  contract_id,
721
1539
  manager_collaborator_id,
1540
+ client_person_id,
722
1541
  code,
723
1542
  name,
724
1543
  client_name,
@@ -732,33 +1551,33 @@ let OperationsService = OperationsService_1 = class OperationsService {
732
1551
  created_at,
733
1552
  updated_at
734
1553
  ) VALUES (
735
- $1, $2, $3, $4, $5, $6,
736
- $7::operations_project_status_965e8d4b2d_enum,
737
- $8,
738
- $9::operations_project_delivery_model_75ee11b3b7_enum,
739
- $10, $11::date, $12::date, NOW(), NOW()
1554
+ $1, $2, $3, $4, $5, $6, $7,
1555
+ $8::operations_project_status_965e8d4b2d_enum,
1556
+ $9,
1557
+ $10::operations_project_delivery_model_75ee11b3b7_enum,
1558
+ $11, $12::date, $13::date, NOW(), NOW()
740
1559
  )
741
- RETURNING id`, (_a = data.contractId) !== null && _a !== void 0 ? _a : null, (_b = data.managerCollaboratorId) !== null && _b !== void 0 ? _b : null, data.code, data.name, (_c = data.clientName) !== null && _c !== void 0 ? _c : null, (_d = data.summary) !== null && _d !== void 0 ? _d : null, (_e = data.status) !== null && _e !== void 0 ? _e : 'planning', (_f = data.progressPercent) !== null && _f !== void 0 ? _f : null, (_g = data.deliveryModel) !== null && _g !== void 0 ? _g : 'project_delivery', (_h = data.budgetAmount) !== null && _h !== void 0 ? _h : null, (_j = data.startDate) !== null && _j !== void 0 ? _j : null, (_k = data.endDate) !== null && _k !== void 0 ? _k : null);
742
- const projectId = (_l = created[0]) === null || _l === void 0 ? void 0 : _l.id;
743
- if ((_m = data.teamAssignments) === null || _m === void 0 ? void 0 : _m.length) {
1560
+ RETURNING id`, (_a = data.contractId) !== null && _a !== void 0 ? _a : null, (_b = data.managerCollaboratorId) !== null && _b !== void 0 ? _b : null, (_c = data.clientPersonId) !== null && _c !== void 0 ? _c : null, data.code, data.name, (_d = data.clientName) !== null && _d !== void 0 ? _d : null, (_e = data.summary) !== null && _e !== void 0 ? _e : null, (_f = data.status) !== null && _f !== void 0 ? _f : 'planning', (_g = data.progressPercent) !== null && _g !== void 0 ? _g : null, (_h = data.deliveryModel) !== null && _h !== void 0 ? _h : 'project_delivery', (_j = data.budgetAmount) !== null && _j !== void 0 ? _j : null, (_k = data.startDate) !== null && _k !== void 0 ? _k : null, (_l = data.endDate) !== null && _l !== void 0 ? _l : null);
1561
+ const projectId = (_m = created[0]) === null || _m === void 0 ? void 0 : _m.id;
1562
+ if ((_o = data.teamAssignments) === null || _o === void 0 ? void 0 : _o.length) {
744
1563
  await this.replaceProjectAssignments(tx, projectId, data.teamAssignments);
745
1564
  }
746
1565
  if (!data.contractId && data.autoGenerateContractDraft !== false) {
747
1566
  const contractId = await this.createProjectContractDraft(tx, actor.userId, {
748
1567
  projectId,
749
- contractTemplateId: (_o = data.contractTemplateId) !== null && _o !== void 0 ? _o : null,
1568
+ contractTemplateId: (_p = data.contractTemplateId) !== null && _p !== void 0 ? _p : null,
750
1569
  projectCode: data.code,
751
1570
  projectName: data.name,
752
- clientName: (_p = data.clientName) !== null && _p !== void 0 ? _p : data.name,
753
- managerCollaboratorId: (_q = data.managerCollaboratorId) !== null && _q !== void 0 ? _q : null,
754
- startDate: (_r = data.startDate) !== null && _r !== void 0 ? _r : null,
755
- endDate: (_s = data.endDate) !== null && _s !== void 0 ? _s : null,
756
- budgetAmount: (_t = data.budgetAmount) !== null && _t !== void 0 ? _t : null,
757
- monthlyHourCap: (_u = data.monthlyHourCap) !== null && _u !== void 0 ? _u : null,
758
- billingModel: (_v = data.billingModel) !== null && _v !== void 0 ? _v : 'time_and_material',
759
- contractCode: (_w = data.contractCode) !== null && _w !== void 0 ? _w : null,
760
- contractName: (_x = data.contractName) !== null && _x !== void 0 ? _x : null,
761
- description: (_z = (_y = data.contractDescription) !== null && _y !== void 0 ? _y : data.summary) !== null && _z !== void 0 ? _z : null,
1571
+ clientName: (_q = data.clientName) !== null && _q !== void 0 ? _q : data.name,
1572
+ managerCollaboratorId: (_r = data.managerCollaboratorId) !== null && _r !== void 0 ? _r : null,
1573
+ startDate: (_s = data.startDate) !== null && _s !== void 0 ? _s : null,
1574
+ endDate: (_t = data.endDate) !== null && _t !== void 0 ? _t : null,
1575
+ budgetAmount: (_u = data.budgetAmount) !== null && _u !== void 0 ? _u : null,
1576
+ monthlyHourCap: (_v = data.monthlyHourCap) !== null && _v !== void 0 ? _v : null,
1577
+ billingModel: (_w = data.billingModel) !== null && _w !== void 0 ? _w : 'time_and_material',
1578
+ contractCode: (_x = data.contractCode) !== null && _x !== void 0 ? _x : null,
1579
+ contractName: (_y = data.contractName) !== null && _y !== void 0 ? _y : null,
1580
+ description: (_0 = (_z = data.contractDescription) !== null && _z !== void 0 ? _z : data.summary) !== null && _0 !== void 0 ? _0 : null,
762
1581
  });
763
1582
  await tx.$executeRawUnsafe(`UPDATE operations_project
764
1583
  SET contract_id = $1,
@@ -777,18 +1596,19 @@ let OperationsService = OperationsService_1 = class OperationsService {
777
1596
  const params = [];
778
1597
  this.pushUpdate(updates, params, 'contract_id', data.contractId);
779
1598
  this.pushUpdate(updates, params, 'manager_collaborator_id', data.managerCollaboratorId);
1599
+ this.pushUpdate(updates, params, 'client_person_id', data.clientPersonId);
780
1600
  this.pushUpdate(updates, params, 'code', data.code);
781
1601
  this.pushUpdate(updates, params, 'name', data.name);
782
1602
  this.pushUpdate(updates, params, 'client_name', data.clientName);
783
1603
  this.pushUpdate(updates, params, 'summary', data.summary);
784
1604
  this.pushUpdate(updates, params, 'status', data.status, 'operations_project_status_965e8d4b2d_enum');
785
1605
  this.pushUpdate(updates, params, 'progress_percent', data.progressPercent);
786
- this.pushUpdate(updates, params, 'delivery_model', data.deliveryModel, 'operations_project_delivery_model_75ee11b3b7_enum');
1606
+ this.pushUpdate(updates, params, 'delivery_model', data.deliveryModel === '' ? null : data.deliveryModel, 'operations_project_delivery_model_75ee11b3b7_enum');
787
1607
  this.pushUpdate(updates, params, 'budget_amount', data.budgetAmount);
788
1608
  this.pushUpdate(updates, params, 'start_date', data.startDate, 'date');
789
1609
  this.pushUpdate(updates, params, 'end_date', data.endDate, 'date');
790
1610
  await this.prisma.$transaction(async (tx) => {
791
- 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, _4, _5, _6;
1611
+ 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, _4, _5, _6, _7;
792
1612
  if (updates.length) {
793
1613
  params.push(projectId);
794
1614
  await tx.$executeRawUnsafe(`UPDATE operations_project
@@ -801,30 +1621,46 @@ let OperationsService = OperationsService_1 = class OperationsService {
801
1621
  }
802
1622
  const nextContractId = data.contractId !== undefined
803
1623
  ? data.contractId
804
- : ((_a = currentProject.contractId) !== null && _a !== void 0 ? _a : null);
1624
+ : ((_b = (_a = currentProject.relatedContract) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null);
805
1625
  const shouldGenerateDraft = !nextContractId && data.autoGenerateContractDraft === true;
806
1626
  if (shouldGenerateDraft) {
807
1627
  const contractId = await this.createProjectContractDraft(tx, actor.userId, {
808
1628
  projectId,
809
- contractTemplateId: (_b = data.contractTemplateId) !== null && _b !== void 0 ? _b : null,
810
- projectCode: (_c = data.code) !== null && _c !== void 0 ? _c : currentProject.code,
811
- projectName: (_d = data.name) !== null && _d !== void 0 ? _d : currentProject.name,
812
- clientName: (_f = (_e = data.clientName) !== null && _e !== void 0 ? _e : currentProject.clientName) !== null && _f !== void 0 ? _f : currentProject.name,
813
- managerCollaboratorId: (_h = (_g = data.managerCollaboratorId) !== null && _g !== void 0 ? _g : currentProject.managerCollaboratorId) !== null && _h !== void 0 ? _h : null,
814
- startDate: (_k = (_j = data.startDate) !== null && _j !== void 0 ? _j : currentProject.startDate) !== null && _k !== void 0 ? _k : null,
815
- endDate: (_m = (_l = data.endDate) !== null && _l !== void 0 ? _l : currentProject.endDate) !== null && _m !== void 0 ? _m : null,
816
- budgetAmount: (_p = (_o = data.budgetAmount) !== null && _o !== void 0 ? _o : currentProject.budgetAmount) !== null && _p !== void 0 ? _p : null,
817
- monthlyHourCap: (_s = (_q = data.monthlyHourCap) !== null && _q !== void 0 ? _q : (_r = currentProject.relatedContract) === null || _r === void 0 ? void 0 : _r.monthlyHourCap) !== null && _s !== void 0 ? _s : null,
818
- billingModel: (_v = (_t = data.billingModel) !== null && _t !== void 0 ? _t : (_u = currentProject.relatedContract) === null || _u === void 0 ? void 0 : _u.billingModel) !== null && _v !== void 0 ? _v : 'time_and_material',
819
- contractCode: (_y = (_w = data.contractCode) !== null && _w !== void 0 ? _w : (_x = currentProject.relatedContract) === null || _x === void 0 ? void 0 : _x.code) !== null && _y !== void 0 ? _y : null,
820
- contractName: (_1 = (_z = data.contractName) !== null && _z !== void 0 ? _z : (_0 = currentProject.relatedContract) === null || _0 === void 0 ? void 0 : _0.name) !== null && _1 !== void 0 ? _1 : null,
821
- description: (_6 = (_5 = (_4 = (_2 = data.contractDescription) !== null && _2 !== void 0 ? _2 : (_3 = currentProject.relatedContract) === null || _3 === void 0 ? void 0 : _3.description) !== null && _4 !== void 0 ? _4 : data.summary) !== null && _5 !== void 0 ? _5 : currentProject.summary) !== null && _6 !== void 0 ? _6 : null,
1629
+ contractTemplateId: (_c = data.contractTemplateId) !== null && _c !== void 0 ? _c : null,
1630
+ projectCode: (_d = data.code) !== null && _d !== void 0 ? _d : currentProject.code,
1631
+ projectName: (_e = data.name) !== null && _e !== void 0 ? _e : currentProject.name,
1632
+ clientName: (_g = (_f = data.clientName) !== null && _f !== void 0 ? _f : currentProject.clientName) !== null && _g !== void 0 ? _g : currentProject.name,
1633
+ managerCollaboratorId: (_j = (_h = data.managerCollaboratorId) !== null && _h !== void 0 ? _h : currentProject.managerCollaboratorId) !== null && _j !== void 0 ? _j : null,
1634
+ startDate: (_l = (_k = data.startDate) !== null && _k !== void 0 ? _k : currentProject.startDate) !== null && _l !== void 0 ? _l : null,
1635
+ endDate: (_o = (_m = data.endDate) !== null && _m !== void 0 ? _m : currentProject.endDate) !== null && _o !== void 0 ? _o : null,
1636
+ budgetAmount: (_q = (_p = data.budgetAmount) !== null && _p !== void 0 ? _p : currentProject.budgetAmount) !== null && _q !== void 0 ? _q : null,
1637
+ monthlyHourCap: (_t = (_r = data.monthlyHourCap) !== null && _r !== void 0 ? _r : (_s = currentProject.relatedContract) === null || _s === void 0 ? void 0 : _s.monthlyHourCap) !== null && _t !== void 0 ? _t : null,
1638
+ billingModel: (_w = (_u = data.billingModel) !== null && _u !== void 0 ? _u : (_v = currentProject.relatedContract) === null || _v === void 0 ? void 0 : _v.billingModel) !== null && _w !== void 0 ? _w : 'time_and_material',
1639
+ contractCode: (_z = (_x = data.contractCode) !== null && _x !== void 0 ? _x : (_y = currentProject.relatedContract) === null || _y === void 0 ? void 0 : _y.code) !== null && _z !== void 0 ? _z : null,
1640
+ contractName: (_2 = (_0 = data.contractName) !== null && _0 !== void 0 ? _0 : (_1 = currentProject.relatedContract) === null || _1 === void 0 ? void 0 : _1.name) !== null && _2 !== void 0 ? _2 : null,
1641
+ description: (_7 = (_6 = (_5 = (_3 = data.contractDescription) !== null && _3 !== void 0 ? _3 : (_4 = currentProject.relatedContract) === null || _4 === void 0 ? void 0 : _4.description) !== null && _5 !== void 0 ? _5 : data.summary) !== null && _6 !== void 0 ? _6 : currentProject.summary) !== null && _7 !== void 0 ? _7 : null,
822
1642
  });
823
1643
  await tx.$executeRawUnsafe(`UPDATE operations_project
824
1644
  SET contract_id = $1,
825
1645
  updated_at = NOW()
826
1646
  WHERE id = $2`, contractId, projectId);
827
1647
  }
1648
+ else if (nextContractId &&
1649
+ (data.monthlyHourCap !== undefined || data.billingModel !== undefined)) {
1650
+ const contractUpdates = [];
1651
+ const contractParams = [];
1652
+ this.pushUpdate(contractUpdates, contractParams, 'monthly_hour_cap', data.monthlyHourCap);
1653
+ this.pushUpdate(contractUpdates, contractParams, 'billing_model', data.billingModel === ''
1654
+ ? null
1655
+ : data.billingModel, 'operations_contract_billing_model_409dc7fea2_enum');
1656
+ if (contractUpdates.length) {
1657
+ contractParams.push(nextContractId);
1658
+ await tx.$executeRawUnsafe(`UPDATE operations_contract
1659
+ SET ${contractUpdates.join(', ')},
1660
+ updated_at = NOW()
1661
+ WHERE id = $${contractParams.length}`, ...contractParams);
1662
+ }
1663
+ }
828
1664
  });
829
1665
  return this.getProjectById(userId, projectId);
830
1666
  }
@@ -2101,13 +2937,22 @@ let OperationsService = OperationsService_1 = class OperationsService {
2101
2937
  pa.project_id AS "projectId",
2102
2938
  p.name AS "projectName",
2103
2939
  pa.role_label AS "roleLabel",
2104
- e.activity_label AS "activityLabel",
2940
+ e.task_id AS "taskId",
2941
+ task_record.name AS "taskName",
2942
+ COALESCE(task_record.name, e.activity_label) AS "activityLabel",
2105
2943
  e.work_date AS "workDate",
2106
- e.hours,
2944
+ COALESCE(NULLIF(e.duration_minutes, 0), ROUND(COALESCE(e.hours, 0)::numeric * 60))::int AS "durationMinutes",
2945
+ COALESCE(
2946
+ e.hours,
2947
+ ROUND((COALESCE(NULLIF(e.duration_minutes, 0), 0)::numeric / 60), 2)
2948
+ ) AS hours,
2107
2949
  e.description
2108
2950
  FROM operations_timesheet_entry e
2109
2951
  LEFT JOIN operations_project_assignment pa ON pa.id = e.project_assignment_id
2110
2952
  LEFT JOIN operations_project p ON p.id = pa.project_id
2953
+ LEFT JOIN operations_task task_record
2954
+ ON task_record.id = e.task_id
2955
+ AND task_record.deleted_at IS NULL
2111
2956
  WHERE e.deleted_at IS NULL
2112
2957
  AND e.timesheet_id = ANY($1::int[])
2113
2958
  ORDER BY e.work_date ASC, e.id ASC`, [headers.map((item) => item.id)]);
@@ -2192,6 +3037,18 @@ let OperationsService = OperationsService_1 = class OperationsService {
2192
3037
  }
2193
3038
  const collaborator = await this.getCollaboratorById(current.collaboratorId);
2194
3039
  const approverId = (_b = (_a = current.approverCollaboratorId) !== null && _a !== void 0 ? _a : collaborator.supervisorId) !== null && _b !== void 0 ? _b : null;
3040
+ if (!approverId) {
3041
+ throw new common_1.BadRequestException('An approver is required before submitting a timesheet.');
3042
+ }
3043
+ const activeEntries = await this.querySingle(`SELECT EXISTS (
3044
+ SELECT 1
3045
+ FROM operations_timesheet_entry
3046
+ WHERE timesheet_id = $1
3047
+ AND deleted_at IS NULL
3048
+ ) AS exists`, [timesheetId]);
3049
+ if (!(activeEntries === null || activeEntries === void 0 ? void 0 : activeEntries.exists)) {
3050
+ throw new common_1.BadRequestException('Cannot submit a timesheet without active entries.');
3051
+ }
2195
3052
  await this.prisma.$transaction(async (tx) => {
2196
3053
  await tx.$executeRawUnsafe(`UPDATE operations_timesheet
2197
3054
  SET status = 'submitted',
@@ -2606,6 +3463,9 @@ let OperationsService = OperationsService_1 = class OperationsService {
2606
3463
  submitted_at = COALESCE(submitted_at, NOW()),
2607
3464
  updated_at = NOW()
2608
3465
  WHERE id = $3`, nextStatus, actor.collaboratorId, approval.targetId);
3466
+ if (nextStatus === 'approved') {
3467
+ await this.applyApprovedScheduleAdjustmentIfNeeded(tx, approval.targetId);
3468
+ }
2609
3469
  }
2610
3470
  await this.insertApprovalHistory(tx, approvalId, actor.collaboratorId, nextStatus === 'approved' ? 'approved' : 'rejected', (_b = data.note) !== null && _b !== void 0 ? _b : null);
2611
3471
  });
@@ -2734,64 +3594,328 @@ let OperationsService = OperationsService_1 = class OperationsService {
2734
3594
  }
2735
3595
  async assertDepartmentCodeAvailable(client, code, excludeDepartmentId) {
2736
3596
  const existing = (await client.$queryRawUnsafe(`SELECT id
2737
- FROM operations_department
3597
+ FROM operations_department
3598
+ WHERE UPPER(COALESCE(code, '')) = UPPER($1)
3599
+ AND ($2::int IS NULL OR id <> $2)
3600
+ LIMIT 1`, code, excludeDepartmentId !== null && excludeDepartmentId !== void 0 ? excludeDepartmentId : null));
3601
+ if (existing[0]) {
3602
+ throw new common_1.BadRequestException('A department with this code already exists.');
3603
+ }
3604
+ }
3605
+ async resolveDepartmentReference(client, input) {
3606
+ var _a;
3607
+ if (input.departmentName !== undefined) {
3608
+ const normalizedDepartmentName = this.normalizeOptionalText(input.departmentName);
3609
+ if (!normalizedDepartmentName) {
3610
+ return null;
3611
+ }
3612
+ const existingByName = (await client.$queryRawUnsafe(`SELECT id, name
3613
+ FROM operations_department
3614
+ WHERE deleted_at IS NULL
3615
+ AND LOWER(name) = LOWER($1)
3616
+ ORDER BY id ASC
3617
+ LIMIT 1`, normalizedDepartmentName));
3618
+ if (existingByName[0]) {
3619
+ return existingByName[0];
3620
+ }
3621
+ const slug = await this.generateUniqueDepartmentSlug(client, normalizedDepartmentName);
3622
+ const created = (await client.$queryRawUnsafe(`INSERT INTO operations_department (
3623
+ slug,
3624
+ code,
3625
+ name,
3626
+ description,
3627
+ created_at,
3628
+ updated_at
3629
+ ) VALUES ($1, NULL, $2, NULL, NOW(), NOW())
3630
+ RETURNING id, name`, slug, normalizedDepartmentName));
3631
+ return (_a = created[0]) !== null && _a !== void 0 ? _a : null;
3632
+ }
3633
+ if (typeof input.departmentId === 'number' &&
3634
+ Number.isInteger(input.departmentId) &&
3635
+ input.departmentId > 0) {
3636
+ return this.getDepartmentById(client, input.departmentId);
3637
+ }
3638
+ return null;
3639
+ }
3640
+ async generateUniqueDepartmentSlug(client, label, excludeDepartmentId) {
3641
+ const baseSlug = this.slugifyValue(label) || `department-${Date.now().toString(36)}`;
3642
+ for (let attempt = 0; attempt < 25; attempt += 1) {
3643
+ const candidate = attempt === 0 ? baseSlug : `${baseSlug}-${attempt + 1}`;
3644
+ const existing = (await client.$queryRawUnsafe(`SELECT id
3645
+ FROM operations_department
3646
+ WHERE slug = $1
3647
+ AND ($2::int IS NULL OR id <> $2)
3648
+ LIMIT 1`, candidate, excludeDepartmentId !== null && excludeDepartmentId !== void 0 ? excludeDepartmentId : null));
3649
+ if (!existing.length) {
3650
+ return candidate;
3651
+ }
3652
+ }
3653
+ return `${baseSlug}-${Date.now().toString(36)}`;
3654
+ }
3655
+ async getJobTitleById(client, jobTitleId, includeInactive = false) {
3656
+ var _a;
3657
+ const jobTitles = (await client.$queryRawUnsafe(`SELECT id,
3658
+ slug,
3659
+ code,
3660
+ name,
3661
+ description,
3662
+ created_at AS "createdAt",
3663
+ updated_at AS "updatedAt",
3664
+ deleted_at AS "deletedAt",
3665
+ CASE WHEN deleted_at IS NULL THEN 'active' ELSE 'inactive' END AS status
3666
+ FROM operations_job_title
3667
+ WHERE id = $1
3668
+ AND ($2::boolean OR deleted_at IS NULL)`, jobTitleId, includeInactive));
3669
+ const jobTitle = (_a = jobTitles[0]) !== null && _a !== void 0 ? _a : null;
3670
+ if (!jobTitle) {
3671
+ throw new common_1.NotFoundException('Job title not found.');
3672
+ }
3673
+ return jobTitle;
3674
+ }
3675
+ async assertJobTitleNameAvailable(client, name, excludeJobTitleId) {
3676
+ const existing = (await client.$queryRawUnsafe(`SELECT id
3677
+ FROM operations_job_title
3678
+ WHERE LOWER(name) = LOWER($1)
3679
+ AND ($2::int IS NULL OR id <> $2)
3680
+ LIMIT 1`, name, excludeJobTitleId !== null && excludeJobTitleId !== void 0 ? excludeJobTitleId : null));
3681
+ if (existing[0]) {
3682
+ throw new common_1.BadRequestException('A job title with this name already exists.');
3683
+ }
3684
+ }
3685
+ async assertJobTitleCodeAvailable(client, code, excludeJobTitleId) {
3686
+ const existing = (await client.$queryRawUnsafe(`SELECT id
3687
+ FROM operations_job_title
3688
+ WHERE UPPER(COALESCE(code, '')) = UPPER($1)
3689
+ AND ($2::int IS NULL OR id <> $2)
3690
+ LIMIT 1`, code, excludeJobTitleId !== null && excludeJobTitleId !== void 0 ? excludeJobTitleId : null));
3691
+ if (existing[0]) {
3692
+ throw new common_1.BadRequestException('A job title with this code already exists.');
3693
+ }
3694
+ }
3695
+ async resolveJobTitleReference(client, input) {
3696
+ var _a;
3697
+ if (input.jobTitleName !== undefined) {
3698
+ const normalizedJobTitleName = this.normalizeOptionalText(input.jobTitleName);
3699
+ if (!normalizedJobTitleName) {
3700
+ return null;
3701
+ }
3702
+ const existingByName = (await client.$queryRawUnsafe(`SELECT id, name
3703
+ FROM operations_job_title
3704
+ WHERE deleted_at IS NULL
3705
+ AND LOWER(name) = LOWER($1)
3706
+ ORDER BY id ASC
3707
+ LIMIT 1`, normalizedJobTitleName));
3708
+ if (existingByName[0]) {
3709
+ return existingByName[0];
3710
+ }
3711
+ const slug = await this.generateUniqueJobTitleSlug(client, normalizedJobTitleName);
3712
+ const created = (await client.$queryRawUnsafe(`INSERT INTO operations_job_title (
3713
+ slug,
3714
+ code,
3715
+ name,
3716
+ description,
3717
+ created_at,
3718
+ updated_at
3719
+ ) VALUES ($1, NULL, $2, NULL, NOW(), NOW())
3720
+ RETURNING id, name`, slug, normalizedJobTitleName));
3721
+ return (_a = created[0]) !== null && _a !== void 0 ? _a : null;
3722
+ }
3723
+ if (typeof input.jobTitleId === 'number' &&
3724
+ Number.isInteger(input.jobTitleId) &&
3725
+ input.jobTitleId > 0) {
3726
+ return this.getJobTitleById(client, input.jobTitleId);
3727
+ }
3728
+ return null;
3729
+ }
3730
+ async generateUniqueJobTitleSlug(client, label, excludeJobTitleId) {
3731
+ const baseSlug = this.slugifyValue(label) || `job-title-${Date.now().toString(36)}`;
3732
+ for (let attempt = 0; attempt < 25; attempt += 1) {
3733
+ const candidate = attempt === 0 ? baseSlug : `${baseSlug}-${attempt + 1}`;
3734
+ const existing = (await client.$queryRawUnsafe(`SELECT id
3735
+ FROM operations_job_title
3736
+ WHERE slug = $1
3737
+ AND ($2::int IS NULL OR id <> $2)
3738
+ LIMIT 1`, candidate, excludeJobTitleId !== null && excludeJobTitleId !== void 0 ? excludeJobTitleId : null));
3739
+ if (!existing.length) {
3740
+ return candidate;
3741
+ }
3742
+ }
3743
+ return `${baseSlug}-${Date.now().toString(36)}`;
3744
+ }
3745
+ async getCollaboratorTypeById(client, collaboratorTypeId, includeInactive = false) {
3746
+ var _a;
3747
+ const collaboratorTypes = (await client.$queryRawUnsafe(`SELECT ct.id,
3748
+ ct.slug,
3749
+ ct.name,
3750
+ ct.description,
3751
+ ct.category,
3752
+ ct.is_active AS "isActive",
3753
+ ct.sort_order AS "sortOrder",
3754
+ CASE
3755
+ WHEN ct.deleted_at IS NULL AND ct.is_active THEN 'active'
3756
+ ELSE 'inactive'
3757
+ END AS status,
3758
+ COUNT(DISTINCT c.id)::int AS "collaboratorCount",
3759
+ ct.created_at AS "createdAt",
3760
+ ct.updated_at AS "updatedAt",
3761
+ ct.deleted_at AS "deletedAt"
3762
+ FROM operations_collaborator_type ct
3763
+ LEFT JOIN operations_collaborator c
3764
+ ON c.deleted_at IS NULL
3765
+ AND c.collaborator_type_id = ct.id
3766
+ WHERE ct.id = $1
3767
+ AND ($2::boolean OR ct.deleted_at IS NULL)
3768
+ GROUP BY ct.id
3769
+ LIMIT 1`, collaboratorTypeId, includeInactive));
3770
+ const collaboratorType = (_a = collaboratorTypes[0]) !== null && _a !== void 0 ? _a : null;
3771
+ if (!collaboratorType) {
3772
+ throw new common_1.NotFoundException('Collaborator type not found.');
3773
+ }
3774
+ return collaboratorType;
3775
+ }
3776
+ async assertCollaboratorTypeNameAvailable(client, name, excludeCollaboratorTypeId) {
3777
+ const existing = (await client.$queryRawUnsafe(`SELECT id
3778
+ FROM operations_collaborator_type
3779
+ WHERE LOWER(name) = LOWER($1)
3780
+ AND ($2::int IS NULL OR id <> $2)
3781
+ LIMIT 1`, name, excludeCollaboratorTypeId !== null && excludeCollaboratorTypeId !== void 0 ? excludeCollaboratorTypeId : null));
3782
+ if (existing[0]) {
3783
+ throw new common_1.BadRequestException('A collaborator type with this name already exists.');
3784
+ }
3785
+ }
3786
+ async assertCollaboratorTypeSlugAvailable(client, slug, excludeCollaboratorTypeId) {
3787
+ const existing = (await client.$queryRawUnsafe(`SELECT id
3788
+ FROM operations_collaborator_type
3789
+ WHERE LOWER(slug) = LOWER($1)
3790
+ AND ($2::int IS NULL OR id <> $2)
3791
+ LIMIT 1`, slug, excludeCollaboratorTypeId !== null && excludeCollaboratorTypeId !== void 0 ? excludeCollaboratorTypeId : null));
3792
+ if (existing[0]) {
3793
+ throw new common_1.BadRequestException('A collaborator type with this slug already exists.');
3794
+ }
3795
+ }
3796
+ async buildCollaboratorTypeSlug(client, name, slugInput, excludeCollaboratorTypeId) {
3797
+ var _a;
3798
+ const normalizedSlugInput = this.slugifyValue((_a = this.normalizeOptionalText(slugInput)) !== null && _a !== void 0 ? _a : '');
3799
+ if (normalizedSlugInput) {
3800
+ await this.assertCollaboratorTypeSlugAvailable(client, normalizedSlugInput, excludeCollaboratorTypeId);
3801
+ return normalizedSlugInput;
3802
+ }
3803
+ return this.generateUniqueCollaboratorTypeSlug(client, name, excludeCollaboratorTypeId);
3804
+ }
3805
+ async generateUniqueCollaboratorTypeSlug(client, label, excludeCollaboratorTypeId) {
3806
+ const baseSlug = this.slugifyValue(label) || `collaborator-type-${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_collaborator_type
3811
+ WHERE slug = $1
3812
+ AND ($2::int IS NULL OR id <> $2)
3813
+ LIMIT 1`, candidate, excludeCollaboratorTypeId !== null && excludeCollaboratorTypeId !== void 0 ? excludeCollaboratorTypeId : null));
3814
+ if (!existing.length) {
3815
+ return candidate;
3816
+ }
3817
+ }
3818
+ return `${baseSlug}-${Date.now().toString(36)}`;
3819
+ }
3820
+ async getProjectRoleById(client, projectRoleId, includeInactive = false) {
3821
+ var _a;
3822
+ const localeId = await this.resolvePreferredLocaleId(client);
3823
+ const projectRoles = (await client.$queryRawUnsafe(`SELECT pr.id,
3824
+ pr.slug,
3825
+ pr.code,
3826
+ COALESCE(prl.name, pr.code, pr.slug) AS name,
3827
+ prl.description,
3828
+ pr.is_active AS "isActive",
3829
+ pr.sort_order AS "sortOrder",
3830
+ pr.created_at AS "createdAt",
3831
+ pr.updated_at AS "updatedAt",
3832
+ pr.deleted_at AS "deletedAt"
3833
+ FROM operations_project_role pr
3834
+ LEFT JOIN LATERAL (
3835
+ SELECT pr_locale.name,
3836
+ pr_locale.description
3837
+ FROM operations_project_role_locale pr_locale
3838
+ WHERE pr_locale.operations_project_role_id = pr.id
3839
+ ORDER BY CASE
3840
+ WHEN $3::int IS NOT NULL AND pr_locale.locale_id = $3 THEN 0
3841
+ ELSE 1
3842
+ END ASC,
3843
+ pr_locale.id ASC
3844
+ LIMIT 1
3845
+ ) prl ON TRUE
3846
+ WHERE pr.id = $1
3847
+ AND ($2::boolean OR pr.deleted_at IS NULL)`, projectRoleId, includeInactive, localeId));
3848
+ const projectRole = (_a = projectRoles[0]) !== null && _a !== void 0 ? _a : null;
3849
+ if (!projectRole) {
3850
+ throw new common_1.NotFoundException('Project role not found.');
3851
+ }
3852
+ return projectRole;
3853
+ }
3854
+ async assertProjectRoleNameAvailable(client, name, excludeProjectRoleId) {
3855
+ const existing = (await client.$queryRawUnsafe(`SELECT pr.id
3856
+ FROM operations_project_role pr
3857
+ JOIN operations_project_role_locale prl
3858
+ ON prl.operations_project_role_id = pr.id
3859
+ WHERE LOWER(prl.name) = LOWER($1)
3860
+ AND pr.deleted_at IS NULL
3861
+ AND ($2::int IS NULL OR pr.id <> $2)
3862
+ LIMIT 1`, name, excludeProjectRoleId !== null && excludeProjectRoleId !== void 0 ? excludeProjectRoleId : null));
3863
+ if (existing[0]) {
3864
+ throw new common_1.BadRequestException('A project role with this name already exists.');
3865
+ }
3866
+ }
3867
+ async assertProjectRoleCodeAvailable(client, code, excludeProjectRoleId) {
3868
+ const existing = (await client.$queryRawUnsafe(`SELECT id
3869
+ FROM operations_project_role
2738
3870
  WHERE UPPER(COALESCE(code, '')) = UPPER($1)
2739
3871
  AND ($2::int IS NULL OR id <> $2)
2740
- LIMIT 1`, code, excludeDepartmentId !== null && excludeDepartmentId !== void 0 ? excludeDepartmentId : null));
3872
+ LIMIT 1`, code, excludeProjectRoleId !== null && excludeProjectRoleId !== void 0 ? excludeProjectRoleId : null));
2741
3873
  if (existing[0]) {
2742
- throw new common_1.BadRequestException('A department with this code already exists.');
2743
- }
2744
- }
2745
- async resolveDepartmentReference(client, input) {
2746
- var _a;
2747
- if (input.departmentName !== undefined) {
2748
- const normalizedDepartmentName = this.normalizeOptionalText(input.departmentName);
2749
- if (!normalizedDepartmentName) {
2750
- return null;
2751
- }
2752
- const existingByName = (await client.$queryRawUnsafe(`SELECT id, name
2753
- FROM operations_department
2754
- WHERE deleted_at IS NULL
2755
- AND LOWER(name) = LOWER($1)
2756
- ORDER BY id ASC
2757
- LIMIT 1`, normalizedDepartmentName));
2758
- if (existingByName[0]) {
2759
- return existingByName[0];
2760
- }
2761
- const slug = await this.generateUniqueDepartmentSlug(client, normalizedDepartmentName);
2762
- const created = (await client.$queryRawUnsafe(`INSERT INTO operations_department (
2763
- slug,
2764
- code,
2765
- name,
2766
- description,
2767
- created_at,
2768
- updated_at
2769
- ) VALUES ($1, NULL, $2, NULL, NOW(), NOW())
2770
- RETURNING id, name`, slug, normalizedDepartmentName));
2771
- return (_a = created[0]) !== null && _a !== void 0 ? _a : null;
3874
+ throw new common_1.BadRequestException('A project role with this code already exists.');
2772
3875
  }
2773
- if (typeof input.departmentId === 'number' &&
2774
- Number.isInteger(input.departmentId) &&
2775
- input.departmentId > 0) {
2776
- return this.getDepartmentById(client, input.departmentId);
2777
- }
2778
- return null;
2779
3876
  }
2780
- async generateUniqueDepartmentSlug(client, label, excludeDepartmentId) {
2781
- const baseSlug = this.slugifyValue(label) || `department-${Date.now().toString(36)}`;
3877
+ async generateUniqueProjectRoleSlug(client, label, excludeProjectRoleId) {
3878
+ const baseSlug = this.slugifyValue(label) || `project-role-${Date.now().toString(36)}`;
2782
3879
  for (let attempt = 0; attempt < 25; attempt += 1) {
2783
3880
  const candidate = attempt === 0 ? baseSlug : `${baseSlug}-${attempt + 1}`;
2784
3881
  const existing = (await client.$queryRawUnsafe(`SELECT id
2785
- FROM operations_department
3882
+ FROM operations_project_role
2786
3883
  WHERE slug = $1
2787
3884
  AND ($2::int IS NULL OR id <> $2)
2788
- LIMIT 1`, candidate, excludeDepartmentId !== null && excludeDepartmentId !== void 0 ? excludeDepartmentId : null));
3885
+ LIMIT 1`, candidate, excludeProjectRoleId !== null && excludeProjectRoleId !== void 0 ? excludeProjectRoleId : null));
2789
3886
  if (!existing.length) {
2790
3887
  return candidate;
2791
3888
  }
2792
3889
  }
2793
3890
  return `${baseSlug}-${Date.now().toString(36)}`;
2794
3891
  }
3892
+ async resolveCollaboratorTypeReference(client, input) {
3893
+ var _a, _b;
3894
+ if (typeof input.collaboratorTypeId === 'number' &&
3895
+ Number.isInteger(input.collaboratorTypeId) &&
3896
+ input.collaboratorTypeId > 0) {
3897
+ const collaboratorTypes = (await client.$queryRawUnsafe(`SELECT id, slug, name
3898
+ FROM operations_collaborator_type
3899
+ WHERE id = $1
3900
+ AND deleted_at IS NULL
3901
+ LIMIT 1`, input.collaboratorTypeId));
3902
+ return (_a = collaboratorTypes[0]) !== null && _a !== void 0 ? _a : null;
3903
+ }
3904
+ const normalizedLookup = this.normalizeOptionalText(input.collaboratorTypeSlug);
3905
+ if (!normalizedLookup) {
3906
+ return null;
3907
+ }
3908
+ const collaboratorTypes = (await client.$queryRawUnsafe(`SELECT id, slug, name
3909
+ FROM operations_collaborator_type
3910
+ WHERE deleted_at IS NULL
3911
+ AND (
3912
+ LOWER(slug) = LOWER($1)
3913
+ OR LOWER(name) = LOWER($1)
3914
+ )
3915
+ ORDER BY id ASC
3916
+ LIMIT 1`, normalizedLookup));
3917
+ return (_b = collaboratorTypes[0]) !== null && _b !== void 0 ? _b : null;
3918
+ }
2795
3919
  async getContractTemplateRecord(client, templateId, includeInactive = false) {
2796
3920
  const template = (await client.$queryRawUnsafe(`SELECT t.id,
2797
3921
  t.slug,
@@ -2878,6 +4002,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
2878
4002
  const project = await this.querySingle(`SELECT p.id,
2879
4003
  p.contract_id AS "contractId",
2880
4004
  p.manager_collaborator_id AS "managerCollaboratorId",
4005
+ p.client_person_id AS "clientPersonId",
4006
+ client_person.avatar_id AS "clientAvatarId",
2881
4007
  p.code,
2882
4008
  p.name,
2883
4009
  p.client_name AS "clientName",
@@ -2886,40 +4012,67 @@ let OperationsService = OperationsService_1 = class OperationsService {
2886
4012
  p.progress_percent AS "progressPercent",
2887
4013
  p.delivery_model AS "deliveryModel",
2888
4014
  p.budget_amount AS "budgetAmount",
2889
- p.start_date AS "startDate",
2890
- p.end_date AS "endDate",
4015
+ TO_CHAR(p.start_date, 'YYYY-MM-DD') AS "startDate",
4016
+ TO_CHAR(p.end_date, 'YYYY-MM-DD') AS "endDate",
2891
4017
  c.name AS "contractName",
2892
4018
  c.status AS "contractStatus",
2893
4019
  c.contract_category AS "contractCategory",
2894
4020
  m.display_name AS "managerName",
2895
4021
  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",
4022
+ MAX(CASE WHEN pa.collaborator_id = $2 THEN COALESCE(project_role_locale.name, pa.role_label) END) AS "myRoleLabel",
2897
4023
  COUNT(DISTINCT pa.id)::int AS "teamSize"
2898
4024
  FROM operations_project p
2899
4025
  LEFT JOIN operations_contract c ON c.id = p.contract_id
2900
4026
  LEFT JOIN operations_collaborator m ON m.id = p.manager_collaborator_id
4027
+ LEFT JOIN person client_person ON client_person.id = p.client_person_id
2901
4028
  LEFT JOIN operations_project_assignment pa
2902
4029
  ON pa.project_id = p.id
2903
4030
  AND pa.deleted_at IS NULL
4031
+ LEFT JOIN operations_project_role project_role
4032
+ ON project_role.id = pa.project_role_id
4033
+ AND project_role.deleted_at IS NULL
4034
+ LEFT JOIN LATERAL (
4035
+ SELECT prl.name
4036
+ FROM operations_project_role_locale prl
4037
+ WHERE prl.operations_project_role_id = project_role.id
4038
+ ORDER BY prl.id ASC
4039
+ LIMIT 1
4040
+ ) project_role_locale ON TRUE
2904
4041
  WHERE p.id = $1
2905
4042
  AND p.deleted_at IS NULL
2906
- GROUP BY p.id, c.id, m.id`, [projectId, actorCollaboratorId !== null && actorCollaboratorId !== void 0 ? actorCollaboratorId : null]);
4043
+ GROUP BY p.id, c.id, m.id, client_person.id`, [projectId, actorCollaboratorId !== null && actorCollaboratorId !== void 0 ? actorCollaboratorId : null]);
2907
4044
  if (!project) {
2908
4045
  throw new common_1.NotFoundException('Project not found.');
2909
4046
  }
2910
4047
  const [assignments, relatedContract, timesheetSummary, operationalIndicators] = await Promise.all([
2911
4048
  this.queryRows(`SELECT pa.id,
2912
4049
  pa.collaborator_id AS "collaboratorId",
4050
+ c.user_id AS "userId",
4051
+ person_record.avatar_id AS "personAvatarId",
4052
+ collaborator_user.photo_id AS "userPhotoId",
2913
4053
  c.display_name AS "collaboratorName",
2914
- pa.role_label AS "roleLabel",
4054
+ pa.project_role_id AS "projectRoleId",
4055
+ COALESCE(project_role_locale.name, pa.role_label) AS "roleLabel",
2915
4056
  pa.allocation_percent AS "allocationPercent",
2916
4057
  pa.weekly_hours AS "weeklyHours",
2917
4058
  pa.is_billable AS "isBillable",
2918
- pa.start_date AS "startDate",
2919
- pa.end_date AS "endDate",
4059
+ TO_CHAR(pa.start_date, 'YYYY-MM-DD') AS "startDate",
4060
+ TO_CHAR(pa.end_date, 'YYYY-MM-DD') AS "endDate",
2920
4061
  pa.status
2921
4062
  FROM operations_project_assignment pa
2922
4063
  JOIN operations_collaborator c ON c.id = pa.collaborator_id
4064
+ LEFT JOIN person person_record ON person_record.id = c.person_id
4065
+ LEFT JOIN "user" collaborator_user ON collaborator_user.id = c.user_id
4066
+ LEFT JOIN operations_project_role project_role
4067
+ ON project_role.id = pa.project_role_id
4068
+ AND project_role.deleted_at IS NULL
4069
+ LEFT JOIN LATERAL (
4070
+ SELECT prl.name
4071
+ FROM operations_project_role_locale prl
4072
+ WHERE prl.operations_project_role_id = project_role.id
4073
+ ORDER BY prl.id ASC
4074
+ LIMIT 1
4075
+ ) project_role_locale ON TRUE
2923
4076
  WHERE pa.project_id = $1
2924
4077
  AND pa.deleted_at IS NULL
2925
4078
  ORDER BY c.display_name ASC`, [projectId]),
@@ -2931,8 +4084,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
2931
4084
  contract_category AS "contractCategory",
2932
4085
  billing_model AS "billingModel",
2933
4086
  status,
2934
- start_date AS "startDate",
2935
- end_date AS "endDate",
4087
+ TO_CHAR(start_date, 'YYYY-MM-DD') AS "startDate",
4088
+ TO_CHAR(end_date, 'YYYY-MM-DD') AS "endDate",
2936
4089
  budget_amount AS "budgetAmount",
2937
4090
  monthly_hour_cap AS "monthlyHourCap",
2938
4091
  description,
@@ -2982,11 +4135,14 @@ let OperationsService = OperationsService_1 = class OperationsService {
2982
4135
  person_record.name AS "personName",
2983
4136
  person_record.avatar_id AS "personAvatarId",
2984
4137
  c.code,
2985
- c.collaborator_type AS "collaboratorType",
4138
+ c.collaborator_type_id AS "collaboratorTypeId",
4139
+ collaborator_type.slug AS "collaboratorTypeSlug",
4140
+ collaborator_type.name AS "collaboratorType",
2986
4141
  COALESCE(NULLIF(c.display_name, ''), person_record.name) AS "displayName",
2987
4142
  c.department_id AS "departmentId",
2988
4143
  COALESCE(NULLIF(department_record.name, ''), NULLIF(c.department, '')) AS "department",
2989
- c.title,
4144
+ c.job_title_id AS "jobTitleId",
4145
+ COALESCE(NULLIF(job_title_record.name, ''), NULLIF(c.title, '')) AS "title",
2990
4146
  c.level_label AS "levelLabel",
2991
4147
  c.weekly_capacity_hours AS "weeklyCapacityHours",
2992
4148
  c.status,
@@ -2997,13 +4153,20 @@ let OperationsService = OperationsService_1 = class OperationsService {
2997
4153
  s.display_name AS "supervisorName",
2998
4154
  hiring_contract.id AS "contractId",
2999
4155
  hiring_contract.status AS "contractStatus",
4156
+ hiring_contract.budget_amount AS "compensationAmount",
3000
4157
  COUNT(DISTINCT pa.id)::int AS "activeAssignments"
3001
4158
  FROM operations_collaborator c
3002
4159
  LEFT JOIN person person_record
3003
4160
  ON person_record.id = c.person_id
4161
+ LEFT JOIN operations_collaborator_type collaborator_type
4162
+ ON collaborator_type.id = c.collaborator_type_id
4163
+ AND collaborator_type.deleted_at IS NULL
3004
4164
  LEFT JOIN operations_department department_record
3005
4165
  ON department_record.id = c.department_id
3006
4166
  AND department_record.deleted_at IS NULL
4167
+ LEFT JOIN operations_job_title job_title_record
4168
+ ON job_title_record.id = c.job_title_id
4169
+ AND job_title_record.deleted_at IS NULL
3007
4170
  LEFT JOIN operations_collaborator s
3008
4171
  ON s.id = c.supervisor_collaborator_id
3009
4172
  LEFT JOIN operations_project_assignment pa
@@ -3011,7 +4174,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
3011
4174
  AND pa.deleted_at IS NULL
3012
4175
  AND pa.status IN ('planned', 'active')
3013
4176
  LEFT JOIN LATERAL (
3014
- SELECT oc.id, oc.status
4177
+ SELECT oc.id, oc.status, oc.budget_amount
3015
4178
  FROM operations_contract oc
3016
4179
  WHERE oc.related_collaborator_id = c.id
3017
4180
  AND oc.deleted_at IS NULL
@@ -3021,22 +4184,33 @@ let OperationsService = OperationsService_1 = class OperationsService {
3021
4184
  ) hiring_contract ON TRUE
3022
4185
  WHERE c.id = $1
3023
4186
  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]);
4187
+ 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
4188
  if (!collaborator) {
3026
4189
  throw new common_1.NotFoundException('Collaborator not found.');
3027
4190
  }
3028
- const [assignedProjects, relatedContracts, weeklySchedule, timesheetSummary, timeOffSummary, scheduleAdjustmentRequests] = await Promise.all([
4191
+ const [assignedProjects, relatedContracts, weeklySchedule, equityParticipation, timesheetSummary, timeOffSummary, scheduleAdjustmentRequests] = await Promise.all([
3029
4192
  this.queryRows(`SELECT p.id,
3030
4193
  p.code,
3031
4194
  p.name,
3032
4195
  p.status,
3033
- pa.role_label AS "roleLabel",
4196
+ pa.project_role_id AS "projectRoleId",
4197
+ COALESCE(project_role_locale.name, pa.role_label) AS "roleLabel",
3034
4198
  pa.allocation_percent AS "allocationPercent",
3035
4199
  pa.weekly_hours AS "weeklyHours",
3036
4200
  pa.start_date AS "startDate",
3037
4201
  pa.end_date AS "endDate"
3038
4202
  FROM operations_project_assignment pa
3039
4203
  JOIN operations_project p ON p.id = pa.project_id
4204
+ LEFT JOIN operations_project_role project_role
4205
+ ON project_role.id = pa.project_role_id
4206
+ AND project_role.deleted_at IS NULL
4207
+ LEFT JOIN LATERAL (
4208
+ SELECT prl.name
4209
+ FROM operations_project_role_locale prl
4210
+ WHERE prl.operations_project_role_id = project_role.id
4211
+ ORDER BY prl.id ASC
4212
+ LIMIT 1
4213
+ ) project_role_locale ON TRUE
3040
4214
  WHERE pa.collaborator_id = $1
3041
4215
  AND pa.deleted_at IS NULL
3042
4216
  AND p.deleted_at IS NULL
@@ -3058,7 +4232,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
3058
4232
  FROM operations_contract c
3059
4233
  WHERE c.related_collaborator_id = $1
3060
4234
  AND c.deleted_at IS NULL
3061
- ORDER BY c.created_at DESC`, [collaboratorId]),
4235
+ ORDER BY CASE WHEN c.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
4236
+ c.created_at DESC`, [collaboratorId]),
3062
4237
  this.queryRows(`SELECT weekday,
3063
4238
  is_working_day AS "isWorkingDay",
3064
4239
  start_time AS "startTime",
@@ -3068,6 +4243,17 @@ let OperationsService = OperationsService_1 = class OperationsService {
3068
4243
  WHERE collaborator_id = $1
3069
4244
  AND deleted_at IS NULL
3070
4245
  ORDER BY id ASC`, [collaboratorId]),
4246
+ this.querySingle(`SELECT participation_type AS "participationType",
4247
+ percentage,
4248
+ voting_power AS "votingPower",
4249
+ start_date AS "startDate",
4250
+ end_date AS "endDate",
4251
+ notes
4252
+ FROM operations_collaborator_equity_participation
4253
+ WHERE collaborator_id = $1
4254
+ AND deleted_at IS NULL
4255
+ ORDER BY updated_at DESC, id DESC
4256
+ LIMIT 1`, [collaboratorId]),
3071
4257
  this.querySingle(`SELECT COUNT(*)::text AS "totalTimesheets",
3072
4258
  COUNT(*) FILTER (WHERE status = 'submitted')::text AS "pendingTimesheets",
3073
4259
  COALESCE(SUM(total_hours), 0)::text AS "totalHours"
@@ -3093,7 +4279,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
3093
4279
  ]);
3094
4280
  return Object.assign(Object.assign({}, collaborator), { assignedProjects,
3095
4281
  relatedContracts,
3096
- weeklySchedule, timesheetSummary: {
4282
+ weeklySchedule,
4283
+ equityParticipation, timesheetSummary: {
3097
4284
  totalTimesheets: Number((_a = timesheetSummary === null || timesheetSummary === void 0 ? void 0 : timesheetSummary.totalTimesheets) !== null && _a !== void 0 ? _a : 0),
3098
4285
  pendingTimesheets: Number((_b = timesheetSummary === null || timesheetSummary === void 0 ? void 0 : timesheetSummary.pendingTimesheets) !== null && _b !== void 0 ? _b : 0),
3099
4286
  totalHours: Number((_c = timesheetSummary === null || timesheetSummary === void 0 ? void 0 : timesheetSummary.totalHours) !== null && _c !== void 0 ? _c : 0),
@@ -3119,6 +4306,270 @@ let OperationsService = OperationsService_1 = class OperationsService {
3119
4306
  AND status IN ('planned', 'active')
3120
4307
  AND collaborator_id = ANY($1::int[])`, [collaboratorIds])).map((row) => row.projectId);
3121
4308
  }
4309
+ async resolveOwnedProjectAssignment(client, collaboratorId, input) {
4310
+ if (!input.projectId && !input.projectAssignmentId) {
4311
+ throw new common_1.BadRequestException('Either projectId or projectAssignmentId is required.');
4312
+ }
4313
+ const params = [collaboratorId];
4314
+ const filters = [
4315
+ 'pa.deleted_at IS NULL',
4316
+ 'p.deleted_at IS NULL',
4317
+ `pa.collaborator_id = $1`,
4318
+ `pa.status IN ('planned', 'active')`,
4319
+ ];
4320
+ if (input.projectAssignmentId) {
4321
+ filters.push(`pa.id = ${this.param(params, input.projectAssignmentId)}`);
4322
+ }
4323
+ if (input.projectId) {
4324
+ filters.push(`pa.project_id = ${this.param(params, input.projectId)}`);
4325
+ }
4326
+ const assignment = (await client.$queryRawUnsafe(`SELECT pa.id,
4327
+ pa.project_id AS "projectId",
4328
+ p.name AS "projectName",
4329
+ p.code AS "projectCode",
4330
+ pa.role_label AS "roleLabel"
4331
+ FROM operations_project_assignment pa
4332
+ JOIN operations_project p
4333
+ ON p.id = pa.project_id
4334
+ WHERE ${filters.join(' AND ')}
4335
+ ORDER BY CASE WHEN pa.status = 'active' THEN 0 ELSE 1 END,
4336
+ pa.start_date DESC NULLS LAST,
4337
+ pa.id DESC
4338
+ LIMIT 1`, ...params));
4339
+ if (!assignment[0]) {
4340
+ throw new common_1.ForbiddenException('The selected project is not assigned to the authenticated collaborator.');
4341
+ }
4342
+ return assignment[0];
4343
+ }
4344
+ async resolveProjectAssignmentForActor(client, actor, input) {
4345
+ if (actor.collaboratorId && !actor.isDirector && !actor.isSupervisor) {
4346
+ return this.resolveOwnedProjectAssignment(client, actor.collaboratorId, input);
4347
+ }
4348
+ if (!input.projectId && !input.projectAssignmentId) {
4349
+ throw new common_1.BadRequestException('Either projectId or projectAssignmentId is required.');
4350
+ }
4351
+ const params = [];
4352
+ const filters = ['pa.deleted_at IS NULL', 'p.deleted_at IS NULL'];
4353
+ if (input.projectAssignmentId) {
4354
+ filters.push(`pa.id = ${this.param(params, input.projectAssignmentId)}`);
4355
+ }
4356
+ if (input.projectId) {
4357
+ filters.push(`pa.project_id = ${this.param(params, input.projectId)}`);
4358
+ }
4359
+ const assignment = (await client.$queryRawUnsafe(`SELECT pa.id,
4360
+ pa.project_id AS "projectId",
4361
+ p.name AS "projectName",
4362
+ p.code AS "projectCode",
4363
+ pa.role_label AS "roleLabel"
4364
+ FROM operations_project_assignment pa
4365
+ JOIN operations_project p
4366
+ ON p.id = pa.project_id
4367
+ WHERE ${filters.join(' AND ')}
4368
+ ORDER BY CASE WHEN pa.status = 'active' THEN 0 ELSE 1 END,
4369
+ pa.start_date DESC NULLS LAST,
4370
+ pa.id DESC
4371
+ LIMIT 1`, ...params));
4372
+ if (!assignment[0]) {
4373
+ throw new common_1.NotFoundException('Project assignment not found.');
4374
+ }
4375
+ return assignment[0];
4376
+ }
4377
+ async getOwnedTaskRecord(client, collaboratorId, taskId) {
4378
+ const task = (await client.$queryRawUnsafe(`SELECT t.id,
4379
+ t.name,
4380
+ t.description,
4381
+ t.priority,
4382
+ t.status,
4383
+ t.due_date AS "dueDate",
4384
+ t.estimate_hours AS "estimateHours",
4385
+ t.position,
4386
+ t.tags,
4387
+ t.assignee_collaborator_id AS "assigneeCollaboratorId",
4388
+ t.project_assignment_id AS "projectAssignmentId",
4389
+ COALESCE(t.project_id, pa.project_id) AS "projectId",
4390
+ p.name AS "projectName",
4391
+ p.code AS "projectCode"
4392
+ FROM operations_task t
4393
+ LEFT JOIN operations_project_assignment pa
4394
+ ON pa.id = t.project_assignment_id
4395
+ AND pa.deleted_at IS NULL
4396
+ LEFT JOIN operations_project p
4397
+ ON p.id = COALESCE(t.project_id, pa.project_id)
4398
+ AND p.deleted_at IS NULL
4399
+ WHERE t.id = $1
4400
+ AND t.deleted_at IS NULL
4401
+ AND (
4402
+ pa.collaborator_id = $2
4403
+ OR t.project_id IN (
4404
+ SELECT pa2.project_id FROM operations_project_assignment pa2
4405
+ WHERE pa2.collaborator_id = $2 AND pa2.deleted_at IS NULL
4406
+ )
4407
+ )
4408
+ LIMIT 1`, taskId, collaboratorId));
4409
+ if (!task[0]) {
4410
+ throw new common_1.ForbiddenException('The selected task is not assigned to the authenticated collaborator.');
4411
+ }
4412
+ return task[0];
4413
+ }
4414
+ async getTaskRecordForActor(client, actor, taskId) {
4415
+ if (actor.collaboratorId && !actor.isDirector && !actor.isSupervisor) {
4416
+ return this.getOwnedTaskRecord(client, actor.collaboratorId, taskId);
4417
+ }
4418
+ const task = await this.getProjectBoardTask(taskId);
4419
+ if (!(task === null || task === void 0 ? void 0 : task.projectId)) {
4420
+ throw new common_1.NotFoundException('Task not found.');
4421
+ }
4422
+ return task;
4423
+ }
4424
+ async listProjectBoardTasks(userId, projectId) {
4425
+ const actor = await this.getActorContext(userId);
4426
+ this.ensureCollaborator(actor);
4427
+ await this.assertProjectAccess(actor, projectId);
4428
+ const rows = await this.queryRows(`SELECT t.id,
4429
+ t.name,
4430
+ t.description,
4431
+ t.priority,
4432
+ t.status,
4433
+ t.due_date AS "dueDate",
4434
+ t.estimate_hours AS "estimateHours",
4435
+ t.position,
4436
+ t.tags,
4437
+ t.assignee_collaborator_id AS "assigneeCollaboratorId",
4438
+ ac.display_name AS "assigneeName",
4439
+ au.photo_id AS "assigneeUserPhotoId",
4440
+ ap.avatar_id AS "assigneePersonAvatarId",
4441
+ t.project_assignment_id AS "projectAssignmentId",
4442
+ t.created_at AS "createdAt"
4443
+ FROM operations_task t
4444
+ LEFT JOIN operations_collaborator ac
4445
+ ON ac.id = t.assignee_collaborator_id AND ac.deleted_at IS NULL
4446
+ LEFT JOIN "user" au
4447
+ ON au.id = ac.user_id
4448
+ LEFT JOIN person ap
4449
+ ON ap.id = ac.person_id
4450
+ WHERE COALESCE(t.project_id, (
4451
+ SELECT pa.project_id FROM operations_project_assignment pa
4452
+ WHERE pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
4453
+ LIMIT 1
4454
+ )) = $1
4455
+ AND t.deleted_at IS NULL
4456
+ ORDER BY t.status ASC, t.position ASC, t.id ASC`, [projectId]);
4457
+ return rows;
4458
+ }
4459
+ async getProjectBoardTask(taskId) {
4460
+ var _a;
4461
+ const rows = await this.queryRows(`SELECT t.id,
4462
+ t.name,
4463
+ t.description,
4464
+ t.priority,
4465
+ t.status,
4466
+ t.due_date AS "dueDate",
4467
+ t.estimate_hours AS "estimateHours",
4468
+ t.position,
4469
+ t.tags,
4470
+ t.assignee_collaborator_id AS "assigneeCollaboratorId",
4471
+ ac.display_name AS "assigneeName",
4472
+ au.photo_id AS "assigneeUserPhotoId",
4473
+ ap.avatar_id AS "assigneePersonAvatarId",
4474
+ t.project_assignment_id AS "projectAssignmentId",
4475
+ COALESCE(t.project_id, pa.project_id) AS "projectId",
4476
+ t.created_at AS "createdAt"
4477
+ FROM operations_task t
4478
+ LEFT JOIN operations_collaborator ac
4479
+ ON ac.id = t.assignee_collaborator_id AND ac.deleted_at IS NULL
4480
+ LEFT JOIN "user" au ON au.id = ac.user_id
4481
+ LEFT JOIN person ap ON ap.id = ac.person_id
4482
+ LEFT JOIN operations_project_assignment pa
4483
+ ON pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
4484
+ WHERE t.id = $1`, [taskId]);
4485
+ return (_a = rows[0]) !== null && _a !== void 0 ? _a : null;
4486
+ }
4487
+ async getOrCreateTimesheetForWorkDate(client, collaboratorId, workDate) {
4488
+ var _a, _b, _c;
4489
+ const normalizedWorkDate = this.normalizeDateOnly(workDate);
4490
+ const { weekStartDate, weekEndDate } = this.getWorkWeekRange(normalizedWorkDate);
4491
+ const existing = (await client.$queryRawUnsafe(`SELECT id, status
4492
+ FROM operations_timesheet
4493
+ WHERE collaborator_id = $1
4494
+ AND week_start_date = $2::date
4495
+ AND week_end_date = $3::date
4496
+ AND deleted_at IS NULL
4497
+ ORDER BY id DESC
4498
+ LIMIT 1`, collaboratorId, weekStartDate, weekEndDate));
4499
+ if (existing[0]) {
4500
+ if (!['draft', 'rejected'].includes(existing[0].status)) {
4501
+ throw new common_1.BadRequestException('The timesheet for this week has already been submitted or approved.');
4502
+ }
4503
+ return existing[0].id;
4504
+ }
4505
+ const collaborator = await this.getCollaboratorById(collaboratorId);
4506
+ const created = (await client.$queryRawUnsafe(`INSERT INTO operations_timesheet (
4507
+ collaborator_id,
4508
+ approver_collaborator_id,
4509
+ week_start_date,
4510
+ week_end_date,
4511
+ notes,
4512
+ status,
4513
+ created_at,
4514
+ updated_at
4515
+ ) VALUES (
4516
+ $1,
4517
+ $2,
4518
+ $3::date,
4519
+ $4::date,
4520
+ NULL,
4521
+ 'draft',
4522
+ NOW(),
4523
+ NOW()
4524
+ )
4525
+ RETURNING id`, collaboratorId, (_a = collaborator.supervisorId) !== null && _a !== void 0 ? _a : null, weekStartDate, weekEndDate));
4526
+ return (_c = (_b = created[0]) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : 0;
4527
+ }
4528
+ async getTimesheetEntryByIdForActor(actor, entryId) {
4529
+ const filter = this.buildIdFilter(actor.visibleCollaboratorIds, 't.collaborator_id', actor.isDirector);
4530
+ const params = [...filter.params, entryId];
4531
+ const entry = await this.querySingle(`SELECT e.id,
4532
+ e.timesheet_id AS "timesheetId",
4533
+ t.collaborator_id AS "collaboratorId",
4534
+ pa.project_id AS "projectId",
4535
+ e.project_assignment_id AS "projectAssignmentId",
4536
+ p.code AS "projectCode",
4537
+ p.name AS "projectName",
4538
+ e.task_id AS "taskId",
4539
+ COALESCE(task_record.name, e.activity_label) AS "taskName",
4540
+ e.activity_label AS "activityLabel",
4541
+ e.work_date AS "workDate",
4542
+ COALESCE(NULLIF(e.duration_minutes, 0), ROUND(COALESCE(e.hours, 0)::numeric * 60))::int AS "durationMinutes",
4543
+ COALESCE(
4544
+ e.hours,
4545
+ ROUND((COALESCE(NULLIF(e.duration_minutes, 0), 0)::numeric / 60), 2)
4546
+ ) AS hours,
4547
+ e.description,
4548
+ t.status,
4549
+ t.week_start_date AS "weekStartDate",
4550
+ t.week_end_date AS "weekEndDate",
4551
+ e.created_at AS "createdAt"
4552
+ FROM operations_timesheet_entry e
4553
+ JOIN operations_timesheet t
4554
+ ON t.id = e.timesheet_id
4555
+ LEFT JOIN operations_project_assignment pa
4556
+ ON pa.id = e.project_assignment_id
4557
+ LEFT JOIN operations_project p
4558
+ ON p.id = pa.project_id
4559
+ LEFT JOIN operations_task task_record
4560
+ ON task_record.id = e.task_id
4561
+ AND task_record.deleted_at IS NULL
4562
+ WHERE e.deleted_at IS NULL
4563
+ AND ${filter.clause}
4564
+ AND e.id = $${params.length}
4565
+ LIMIT 1`, params);
4566
+ if (!entry) {
4567
+ throw new common_1.NotFoundException('Timesheet entry not found.');
4568
+ }
4569
+ return Object.assign(Object.assign({}, entry), { label: [entry.projectCode, entry.projectName, entry.taskName]
4570
+ .filter(Boolean)
4571
+ .join(' • ') });
4572
+ }
3122
4573
  async getTimesheetById(timesheetId) {
3123
4574
  const timesheet = await this.querySingle(`SELECT id,
3124
4575
  collaborator_id AS "collaboratorId",
@@ -3133,7 +4584,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
3133
4584
  return timesheet;
3134
4585
  }
3135
4586
  async replaceTimesheetEntries(client, timesheetId, entries, collaboratorId) {
3136
- var _a, _b, _c;
4587
+ var _a, _b, _c, _d, _e, _f;
3137
4588
  await client.$executeRawUnsafe(`UPDATE operations_timesheet_entry
3138
4589
  SET deleted_at = NOW()
3139
4590
  WHERE timesheet_id = $1
@@ -3159,22 +4610,54 @@ let OperationsService = OperationsService_1 = class OperationsService {
3159
4610
  }
3160
4611
  }
3161
4612
  for (const entry of entries) {
4613
+ const durationMinutes = this.resolveEntryDurationMinutes(entry);
4614
+ const hours = Number((durationMinutes / 60).toFixed(2));
4615
+ const resolvedTask = entry.taskId
4616
+ ? await this.getOwnedTaskRecord(client, collaboratorId, entry.taskId)
4617
+ : null;
4618
+ if (resolvedTask &&
4619
+ entry.projectAssignmentId &&
4620
+ resolvedTask.projectAssignmentId !== entry.projectAssignmentId) {
4621
+ throw new common_1.BadRequestException('The selected task does not belong to the chosen project assignment.');
4622
+ }
4623
+ 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;
4624
+ if (entry.taskId && !resolvedAssignmentId) {
4625
+ throw new common_1.BadRequestException('The selected task must belong to a project assignment.');
4626
+ }
4627
+ 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);
4628
+ if (!entry.workDate) {
4629
+ throw new common_1.BadRequestException('Timesheet entry workDate is required.');
4630
+ }
3162
4631
  await client.$executeRawUnsafe(`INSERT INTO operations_timesheet_entry (
3163
4632
  timesheet_id,
3164
4633
  project_assignment_id,
4634
+ task_id,
3165
4635
  activity_label,
3166
4636
  work_date,
4637
+ duration_minutes,
3167
4638
  hours,
3168
4639
  description,
3169
4640
  created_at,
3170
4641
  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);
4642
+ ) 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
4643
  }
3173
4644
  }
3174
4645
  async refreshTimesheetTotal(client, timesheetId) {
3175
4646
  await client.$executeRawUnsafe(`UPDATE operations_timesheet
3176
4647
  SET total_hours = (
3177
- SELECT COALESCE(SUM(hours), 0)
4648
+ SELECT COALESCE(
4649
+ SUM(
4650
+ COALESCE(
4651
+ CASE
4652
+ WHEN duration_minutes IS NOT NULL AND duration_minutes > 0
4653
+ THEN duration_minutes::numeric / 60
4654
+ ELSE hours
4655
+ END,
4656
+ 0
4657
+ )
4658
+ ),
4659
+ 0
4660
+ )
3178
4661
  FROM operations_timesheet_entry
3179
4662
  WHERE timesheet_id = $1
3180
4663
  AND deleted_at IS NULL
@@ -3222,6 +4705,30 @@ let OperationsService = OperationsService_1 = class OperationsService {
3222
4705
  }
3223
4706
  await this.insertApprovalHistory(client, approvalId, input.requesterCollaboratorId, 'submitted', null);
3224
4707
  }
4708
+ async applyApprovedScheduleAdjustmentIfNeeded(client, requestId) {
4709
+ const request = (await client.$queryRawUnsafe(`SELECT collaborator_id AS "collaboratorId",
4710
+ request_scope AS "requestScope"
4711
+ FROM operations_schedule_adjustment_request
4712
+ WHERE id = $1
4713
+ AND deleted_at IS NULL
4714
+ LIMIT 1`, requestId));
4715
+ const currentRequest = request[0];
4716
+ if (!currentRequest || currentRequest.requestScope !== 'permanent') {
4717
+ return;
4718
+ }
4719
+ const days = (await client.$queryRawUnsafe(`SELECT weekday,
4720
+ is_working_day AS "isWorkingDay",
4721
+ start_time AS "startTime",
4722
+ end_time AS "endTime",
4723
+ break_minutes AS "breakMinutes"
4724
+ FROM operations_schedule_adjustment_day
4725
+ WHERE schedule_adjustment_request_id = $1
4726
+ ORDER BY id ASC`, requestId));
4727
+ if (!days.length) {
4728
+ return;
4729
+ }
4730
+ await this.replaceCollaboratorScheduleDays(client, currentRequest.collaboratorId, days);
4731
+ }
3225
4732
  async insertApprovalHistory(client, approvalId, actorCollaboratorId, action, note) {
3226
4733
  await client.$executeRawUnsafe(`INSERT INTO operations_approval_history (
3227
4734
  approval_id,
@@ -3238,19 +4745,10 @@ let OperationsService = OperationsService_1 = class OperationsService {
3238
4745
  )`, approvalId, actorCollaboratorId, action, note);
3239
4746
  }
3240
4747
  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
- }
4748
+ this.accessService.ensureProjectAccess(actor, projectId);
3246
4749
  }
3247
4750
  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
- }
4751
+ this.accessService.ensureCollaboratorAccess(actor, collaboratorId);
3254
4752
  }
3255
4753
  async listSingleTimesheet(actor, timesheetId) {
3256
4754
  const timesheets = await this.listTimesheets(actor.userId);
@@ -3261,19 +4759,13 @@ let OperationsService = OperationsService_1 = class OperationsService {
3261
4759
  return timesheet;
3262
4760
  }
3263
4761
  ensureDirector(actor) {
3264
- if (!actor.isDirector) {
3265
- throw new common_1.ForbiddenException('Director access is required.');
3266
- }
4762
+ this.accessService.ensureDirector(actor);
3267
4763
  }
3268
4764
  ensureSupervisor(actor) {
3269
- if (!actor.isSupervisor) {
3270
- throw new common_1.ForbiddenException('Supervisor access is required.');
3271
- }
4765
+ this.accessService.ensureSupervisor(actor);
3272
4766
  }
3273
4767
  ensureCollaborator(actor) {
3274
- if (!actor.isCollaborator) {
3275
- throw new common_1.ForbiddenException('Operations collaborator access is required.');
3276
- }
4768
+ this.accessService.ensureCollaborator(actor);
3277
4769
  }
3278
4770
  defaultWeeklySchedule() {
3279
4771
  return [
@@ -3352,16 +4844,22 @@ let OperationsService = OperationsService_1 = class OperationsService {
3352
4844
  }
3353
4845
  }
3354
4846
  async replaceProjectAssignments(client, projectId, teamAssignments) {
3355
- var _a, _b, _c, _d, _e, _f, _g;
4847
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
3356
4848
  await client.$executeRawUnsafe(`UPDATE operations_project_assignment
3357
4849
  SET deleted_at = NOW(),
3358
4850
  updated_at = NOW()
3359
4851
  WHERE project_id = $1
3360
4852
  AND deleted_at IS NULL`, projectId);
3361
4853
  for (const assignment of teamAssignments !== null && teamAssignments !== void 0 ? teamAssignments : []) {
4854
+ const projectRole = typeof assignment.projectRoleId === 'number' &&
4855
+ Number.isInteger(assignment.projectRoleId) &&
4856
+ assignment.projectRoleId > 0
4857
+ ? await this.getProjectRoleById(client, assignment.projectRoleId)
4858
+ : null;
3362
4859
  await client.$executeRawUnsafe(`INSERT INTO operations_project_assignment (
3363
4860
  project_id,
3364
4861
  collaborator_id,
4862
+ project_role_id,
3365
4863
  role_label,
3366
4864
  allocation_percent,
3367
4865
  weekly_hours,
@@ -3372,30 +4870,98 @@ let OperationsService = OperationsService_1 = class OperationsService {
3372
4870
  created_at,
3373
4871
  updated_at
3374
4872
  ) VALUES (
3375
- $1, $2, $3, $4, $5, $6, $7::date, $8::date,
3376
- $9::operations_project_assignment_status_155b459bbf_enum,
4873
+ $1, $2, $3, $4, $5, $6, $7, $8::date, $9::date,
4874
+ $10::operations_project_assignment_status_155b459bbf_enum,
3377
4875
  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');
4876
+ )`, 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');
4877
+ }
4878
+ }
4879
+ async replaceCollaboratorEquityParticipation(client, collaboratorId, equityParticipation) {
4880
+ var _a, _b, _c, _d, _e;
4881
+ if (equityParticipation === undefined) {
4882
+ return;
4883
+ }
4884
+ await client.$executeRawUnsafe(`UPDATE operations_collaborator_equity_participation
4885
+ SET deleted_at = COALESCE(deleted_at, NOW()),
4886
+ updated_at = NOW()
4887
+ WHERE collaborator_id = $1
4888
+ AND deleted_at IS NULL`, collaboratorId);
4889
+ if (equityParticipation === null) {
4890
+ return;
4891
+ }
4892
+ const participationType = (_a = equityParticipation.participationType) !== null && _a !== void 0 ? _a : 'other';
4893
+ await client.$executeRawUnsafe(`INSERT INTO operations_collaborator_equity_participation (
4894
+ collaborator_id,
4895
+ participation_type,
4896
+ percentage,
4897
+ voting_power,
4898
+ start_date,
4899
+ end_date,
4900
+ notes,
4901
+ deleted_at,
4902
+ created_at,
4903
+ updated_at
4904
+ ) VALUES (
4905
+ $1,
4906
+ $2::operations_collaborator_equity_participation_pa_98792c1f1e_enum,
4907
+ $3,
4908
+ $4,
4909
+ $5::date,
4910
+ $6::date,
4911
+ $7,
4912
+ NULL,
4913
+ NOW(),
4914
+ NOW()
4915
+ )`, 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));
4916
+ }
4917
+ normalizeCollaboratorTypeKey(collaboratorType) {
4918
+ const normalized = String(collaboratorType !== null && collaboratorType !== void 0 ? collaboratorType : '')
4919
+ .trim()
4920
+ .toLowerCase()
4921
+ .normalize('NFD')
4922
+ .replace(/[\u0300-\u036f]/g, '');
4923
+ switch (normalized) {
4924
+ case 'estagiario':
4925
+ return 'intern';
4926
+ case 'outro':
4927
+ return 'other';
4928
+ default:
4929
+ return normalized;
3379
4930
  }
3380
4931
  }
3381
4932
  mapContractCategoryForCollaboratorType(collaboratorType) {
3382
- switch (collaboratorType) {
4933
+ switch (this.normalizeCollaboratorTypeKey(collaboratorType)) {
3383
4934
  case 'clt':
4935
+ case 'intern':
3384
4936
  return 'employee';
3385
4937
  case 'pj':
3386
4938
  case 'freelancer':
3387
4939
  return 'contractor';
4940
+ case 'socio':
4941
+ case 'acionista':
4942
+ case 'parceiro':
4943
+ return 'partner';
4944
+ case 'administrador':
4945
+ case 'diretor':
4946
+ case 'conselheiro':
4947
+ return 'internal';
3388
4948
  default:
3389
4949
  return 'other';
3390
4950
  }
3391
4951
  }
3392
4952
  mapBillingModelForCollaboratorType(collaboratorType) {
3393
- return collaboratorType === 'freelancer'
3394
- ? 'time_and_material'
3395
- : 'monthly_retainer';
4953
+ switch (this.normalizeCollaboratorTypeKey(collaboratorType)) {
4954
+ case 'freelancer':
4955
+ return 'time_and_material';
4956
+ case 'pj':
4957
+ case 'parceiro':
4958
+ return 'fixed_price';
4959
+ default:
4960
+ return 'monthly_retainer';
4961
+ }
3396
4962
  }
3397
4963
  mapContractTypeForCollaboratorType(collaboratorType) {
3398
- switch (collaboratorType) {
4964
+ switch (this.normalizeCollaboratorTypeKey(collaboratorType)) {
3399
4965
  case 'clt':
3400
4966
  return 'clt';
3401
4967
  case 'pj':
@@ -3404,16 +4970,39 @@ let OperationsService = OperationsService_1 = class OperationsService {
3404
4970
  return 'freelancer_agreement';
3405
4971
  case 'intern':
3406
4972
  return 'fixed_term';
4973
+ case 'socio':
4974
+ case 'acionista':
4975
+ case 'administrador':
4976
+ case 'diretor':
4977
+ case 'conselheiro':
4978
+ case 'parceiro':
4979
+ return 'service_agreement';
3407
4980
  default:
3408
4981
  return 'other';
3409
4982
  }
3410
4983
  }
4984
+ buildHiringContractName(displayName, collaboratorType) {
4985
+ switch (this.normalizeCollaboratorTypeKey(collaboratorType)) {
4986
+ case 'clt':
4987
+ return `${displayName} Employment Contract`;
4988
+ case 'intern':
4989
+ return `${displayName} Internship Agreement`;
4990
+ case 'socio':
4991
+ case 'acionista':
4992
+ case 'parceiro':
4993
+ return `${displayName} Partnership Agreement`;
4994
+ case 'administrador':
4995
+ case 'diretor':
4996
+ case 'conselheiro':
4997
+ return `${displayName} Executive Engagement Agreement`;
4998
+ default:
4999
+ return `${displayName} Service Contract`;
5000
+ }
5001
+ }
3411
5002
  async createHiringContractDraft(client, createdByUserId, input) {
3412
5003
  var _a, _b, _c;
3413
5004
  const contractCode = `HIR-${input.collaboratorCode}`;
3414
- const contractName = input.collaboratorType === 'clt'
3415
- ? `${input.displayName} Employment Contract`
3416
- : `${input.displayName} Service Contract`;
5005
+ const contractName = this.buildHiringContractName(input.displayName, input.collaboratorType);
3417
5006
  await client.$executeRawUnsafe(`INSERT INTO operations_contract (
3418
5007
  code,
3419
5008
  name,
@@ -3452,6 +5041,90 @@ let OperationsService = OperationsService_1 = class OperationsService {
3452
5041
  ? Math.round(Number(input.weeklyCapacityHours) * 4)
3453
5042
  : null, (_c = input.description) !== null && _c !== void 0 ? _c : null, createdByUserId);
3454
5043
  }
5044
+ async syncHiringContractDraft(client, updatedByUserId, collaboratorId, data) {
5045
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
5046
+ const collaborator = await client.$queryRawUnsafe(`SELECT c.id,
5047
+ c.code,
5048
+ COALESCE(NULLIF(c.display_name, ''), person_record.name) AS "displayName",
5049
+ collaborator_type.slug AS "collaboratorTypeSlug",
5050
+ c.supervisor_collaborator_id AS "supervisorCollaboratorId",
5051
+ c.joined_at AS "joinedAt",
5052
+ c.weekly_capacity_hours AS "weeklyCapacityHours"
5053
+ FROM operations_collaborator c
5054
+ LEFT JOIN person person_record
5055
+ ON person_record.id = c.person_id
5056
+ LEFT JOIN operations_collaborator_type collaborator_type
5057
+ ON collaborator_type.id = c.collaborator_type_id
5058
+ AND collaborator_type.deleted_at IS NULL
5059
+ WHERE c.id = $1
5060
+ AND c.deleted_at IS NULL
5061
+ LIMIT 1`, collaboratorId);
5062
+ const currentCollaborator = (_a = collaborator[0]) !== null && _a !== void 0 ? _a : null;
5063
+ if (!currentCollaborator) {
5064
+ throw new common_1.NotFoundException('Collaborator not found.');
5065
+ }
5066
+ const hiringContracts = (await client.$queryRawUnsafe(`SELECT id,
5067
+ budget_amount AS "budgetAmount",
5068
+ description
5069
+ FROM operations_contract
5070
+ WHERE related_collaborator_id = $1
5071
+ AND origin_type = 'employee_hiring'
5072
+ AND deleted_at IS NULL
5073
+ ORDER BY created_at DESC
5074
+ LIMIT 1`, collaboratorId));
5075
+ const hiringContract = (_b = hiringContracts[0]) !== null && _b !== void 0 ? _b : null;
5076
+ const collaboratorCode = (_c = this.normalizeOptionalText(currentCollaborator.code)) !== null && _c !== void 0 ? _c : `COL-${collaboratorId}`;
5077
+ const displayName = (_d = this.normalizeOptionalText(currentCollaborator.displayName)) !== null && _d !== void 0 ? _d : `Collaborator ${collaboratorId}`;
5078
+ const collaboratorTypeSlug = (_e = this.normalizeOptionalText(currentCollaborator.collaboratorTypeSlug)) !== null && _e !== void 0 ? _e : 'other';
5079
+ const startDate = data.joinedAt !== undefined
5080
+ ? (_f = data.joinedAt) !== null && _f !== void 0 ? _f : null
5081
+ : (_g = currentCollaborator.joinedAt) !== null && _g !== void 0 ? _g : null;
5082
+ const weeklyCapacityHours = data.weeklyCapacityHours !== undefined
5083
+ ? (_h = data.weeklyCapacityHours) !== null && _h !== void 0 ? _h : null
5084
+ : (_j = currentCollaborator.weeklyCapacityHours) !== null && _j !== void 0 ? _j : null;
5085
+ const compensationAmount = data.compensationAmount !== undefined
5086
+ ? (_k = data.compensationAmount) !== null && _k !== void 0 ? _k : null
5087
+ : (_l = hiringContract === null || hiringContract === void 0 ? void 0 : hiringContract.budgetAmount) !== null && _l !== void 0 ? _l : null;
5088
+ const description = data.contractDescription !== undefined
5089
+ ? this.normalizeOptionalText(data.contractDescription)
5090
+ : (_m = hiringContract === null || hiringContract === void 0 ? void 0 : hiringContract.description) !== null && _m !== void 0 ? _m : null;
5091
+ const supervisorCollaboratorId = data.supervisorCollaboratorId !== undefined
5092
+ ? (_o = data.supervisorCollaboratorId) !== null && _o !== void 0 ? _o : null
5093
+ : (_p = currentCollaborator.supervisorCollaboratorId) !== null && _p !== void 0 ? _p : null;
5094
+ if (!hiringContract) {
5095
+ if (data.autoGenerateContractDraft === false) {
5096
+ return;
5097
+ }
5098
+ await this.createHiringContractDraft(client, updatedByUserId, {
5099
+ collaboratorId,
5100
+ collaboratorCode,
5101
+ displayName,
5102
+ collaboratorType: collaboratorTypeSlug,
5103
+ supervisorCollaboratorId,
5104
+ startDate,
5105
+ weeklyCapacityHours,
5106
+ compensationAmount,
5107
+ description,
5108
+ });
5109
+ return;
5110
+ }
5111
+ await client.$executeRawUnsafe(`UPDATE operations_contract
5112
+ SET code = $1,
5113
+ name = $2,
5114
+ contract_category = $3::operations_contract_contract_category_70d553ea09_enum,
5115
+ contract_type = $4::operations_contract_contract_type_48331e2ebf_enum,
5116
+ client_name = $5,
5117
+ billing_model = $6::operations_contract_billing_model_409dc7fea2_enum,
5118
+ account_manager_collaborator_id = $7,
5119
+ start_date = $8::date,
5120
+ effective_date = $8::date,
5121
+ budget_amount = $9,
5122
+ monthly_hour_cap = $10,
5123
+ description = $11,
5124
+ updated_by_user_id = $12,
5125
+ updated_at = NOW()
5126
+ WHERE id = $13`, `HIR-${collaboratorCode}`, this.buildHiringContractName(displayName, collaboratorTypeSlug), this.mapContractCategoryForCollaboratorType(collaboratorTypeSlug), this.mapContractTypeForCollaboratorType(collaboratorTypeSlug), displayName, this.mapBillingModelForCollaboratorType(collaboratorTypeSlug), supervisorCollaboratorId, startDate !== null && startDate !== void 0 ? startDate : new Date().toISOString().slice(0, 10), compensationAmount, weeklyCapacityHours ? Math.round(Number(weeklyCapacityHours) * 4) : null, description, updatedByUserId, hiringContract.id);
5127
+ }
3455
5128
  async createProjectContractDraft(client, createdByUserId, input) {
3456
5129
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
3457
5130
  const templateRows = input.contractTemplateId
@@ -4479,6 +6152,97 @@ let OperationsService = OperationsService_1 = class OperationsService {
4479
6152
  ? ''
4480
6153
  : String(value).trim();
4481
6154
  }
6155
+ normalizeDurationMinutes(duration, unit) {
6156
+ const numeric = Number(duration);
6157
+ if (!Number.isFinite(numeric) || numeric <= 0) {
6158
+ throw new common_1.BadRequestException('Duration must be greater than 0.');
6159
+ }
6160
+ const minutes = unit === 'hours' ? numeric * 60 : numeric;
6161
+ const rounded = Math.round(minutes);
6162
+ if (!Number.isFinite(rounded) || rounded <= 0) {
6163
+ throw new common_1.BadRequestException('Duration must be greater than 0.');
6164
+ }
6165
+ return rounded;
6166
+ }
6167
+ resolveEntryDurationMinutes(entry) {
6168
+ var _a;
6169
+ if (entry.durationMinutes !== undefined &&
6170
+ entry.durationMinutes !== null) {
6171
+ return this.normalizeDurationMinutes(entry.durationMinutes, 'minutes');
6172
+ }
6173
+ if (entry.duration !== undefined && entry.duration !== null) {
6174
+ return this.normalizeDurationMinutes(entry.duration, (_a = entry.unit) !== null && _a !== void 0 ? _a : 'minutes');
6175
+ }
6176
+ if (entry.hours !== undefined && entry.hours !== null) {
6177
+ return this.normalizeDurationMinutes(entry.hours, 'hours');
6178
+ }
6179
+ throw new common_1.BadRequestException('Timesheet entry duration is required.');
6180
+ }
6181
+ normalizeDateOnly(value) {
6182
+ const normalized = this.normalizeExtractionString(value);
6183
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized)) {
6184
+ throw new common_1.BadRequestException('Date values must use the YYYY-MM-DD format.');
6185
+ }
6186
+ return normalized;
6187
+ }
6188
+ parseDateOnly(value) {
6189
+ const normalized = this.normalizeDateOnly(value);
6190
+ const parsed = new Date(`${normalized}T00:00:00.000Z`);
6191
+ if (Number.isNaN(parsed.getTime())) {
6192
+ throw new common_1.BadRequestException('Invalid date value provided.');
6193
+ }
6194
+ return parsed;
6195
+ }
6196
+ formatDateOnly(value) {
6197
+ return value.toISOString().slice(0, 10);
6198
+ }
6199
+ getWorkWeekRange(workDate) {
6200
+ const date = this.parseDateOnly(workDate);
6201
+ const day = date.getUTCDay();
6202
+ const diffToMonday = day === 0 ? -6 : 1 - day;
6203
+ const weekStart = new Date(date);
6204
+ weekStart.setUTCDate(weekStart.getUTCDate() + diffToMonday);
6205
+ const weekEnd = new Date(weekStart);
6206
+ weekEnd.setUTCDate(weekStart.getUTCDate() + 6);
6207
+ return {
6208
+ weekStartDate: this.formatDateOnly(weekStart),
6209
+ weekEndDate: this.formatDateOnly(weekEnd),
6210
+ };
6211
+ }
6212
+ normalizePaginationParams(input = {}, options) {
6213
+ var _a, _b, _c, _d, _e, _f, _g;
6214
+ const page = Math.max(Number((_a = input.page) !== null && _a !== void 0 ? _a : 1) || 1, 1);
6215
+ const pageSize = Math.min(Math.max(Number((_b = input.pageSize) !== null && _b !== void 0 ? _b : 10) || 10, 1), 100);
6216
+ const search = (_c = this.normalizeOptionalText(input.search)) !== null && _c !== void 0 ? _c : '';
6217
+ const requestedSortField = this.normalizeOptionalText(input.sortField);
6218
+ const sortField = requestedSortField && options.allowedSortFields.includes(requestedSortField)
6219
+ ? requestedSortField
6220
+ : ((_e = (_d = options.defaultSortField) !== null && _d !== void 0 ? _d : options.allowedSortFields[0]) !== null && _e !== void 0 ? _e : 'id');
6221
+ const sortOrder = String((_g = (_f = input.sortOrder) !== null && _f !== void 0 ? _f : options.defaultSortOrder) !== null && _g !== void 0 ? _g : 'asc')
6222
+ .toLowerCase() === 'desc'
6223
+ ? 'desc'
6224
+ : 'asc';
6225
+ return {
6226
+ page,
6227
+ pageSize,
6228
+ search,
6229
+ sortField,
6230
+ sortOrder,
6231
+ offset: (page - 1) * pageSize,
6232
+ };
6233
+ }
6234
+ buildPaginationResult(data, total, page, pageSize) {
6235
+ const lastPage = Math.max(1, Math.ceil(total / Math.max(pageSize, 1)));
6236
+ return {
6237
+ total,
6238
+ lastPage,
6239
+ page,
6240
+ pageSize,
6241
+ prev: page > 1 ? page - 1 : null,
6242
+ next: page < lastPage ? page + 1 : null,
6243
+ data,
6244
+ };
6245
+ }
4482
6246
  normalizeExtractionBoolean(value) {
4483
6247
  if (typeof value === 'boolean') {
4484
6248
  return value;
@@ -4621,6 +6385,8 @@ exports.OperationsService = OperationsService = OperationsService_1 = __decorate
4621
6385
  core_1.AiService,
4622
6386
  core_1.IntegrationDeveloperApiService,
4623
6387
  core_1.FileService,
4624
- core_1.SettingService])
6388
+ core_1.SettingService,
6389
+ operations_access_service_1.OperationsAccessService,
6390
+ api_locale_1.LocaleService])
4625
6391
  ], OperationsService);
4626
6392
  //# sourceMappingURL=operations.service.js.map