@hed-hog/operations 0.0.305 → 0.0.306

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 (39) hide show
  1. package/dist/controllers/operations-timesheets.controller.d.ts +21 -0
  2. package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
  3. package/dist/controllers/operations-timesheets.controller.js +12 -0
  4. package/dist/controllers/operations-timesheets.controller.js.map +1 -1
  5. package/dist/dto/update-collaborator-type.dto.d.ts +3 -1
  6. package/dist/dto/update-collaborator-type.dto.d.ts.map +1 -1
  7. package/dist/dto/update-collaborator-type.dto.js +2 -1
  8. package/dist/dto/update-collaborator-type.dto.js.map +1 -1
  9. package/dist/operations.service.d.ts +22 -0
  10. package/dist/operations.service.d.ts.map +1 -1
  11. package/dist/operations.service.js +180 -47
  12. package/dist/operations.service.js.map +1 -1
  13. package/dist/operations.service.spec.js +73 -0
  14. package/dist/operations.service.spec.js.map +1 -1
  15. package/hedhog/data/menu.yaml +26 -26
  16. package/hedhog/data/operations_collaborator_type.yaml +76 -76
  17. package/hedhog/data/route.yaml +13 -0
  18. package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +5 -3
  19. package/hedhog/frontend/app/_components/timesheet-task-create-sheet.tsx.ejs +1 -0
  20. package/hedhog/frontend/app/approvals/page.tsx.ejs +2 -2
  21. package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +26 -15
  22. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +235 -72
  23. package/hedhog/frontend/app/timesheets/page.tsx.ejs +344 -134
  24. package/hedhog/frontend/messages/en.json +5 -0
  25. package/hedhog/frontend/messages/pt.json +7 -2
  26. package/hedhog/table/operations_collaborator.yaml +18 -18
  27. package/hedhog/table/operations_collaborator_equity_participation.yaml +43 -43
  28. package/hedhog/table/operations_collaborator_type.yaml +33 -33
  29. package/hedhog/table/operations_contract_document.yaml +33 -33
  30. package/package.json +4 -4
  31. package/src/controllers/operations-timesheets.controller.ts +13 -0
  32. package/src/dto/create-collaborator-type.dto.ts +43 -43
  33. package/src/dto/create-collaborator.dto.ts +223 -223
  34. package/src/dto/list-collaborator-types.dto.ts +15 -15
  35. package/src/dto/list-collaborators.dto.ts +30 -30
  36. package/src/dto/update-collaborator-type.dto.ts +4 -3
  37. package/src/dto/update-collaborator.dto.ts +3 -3
  38. package/src/operations.service.spec.ts +96 -0
  39. package/src/operations.service.ts +257 -47
@@ -1057,20 +1057,18 @@ let OperationsService = OperationsService_1 = class OperationsService {
1057
1057
  var _a, _b;
1058
1058
  const actor = await this.getActorContext(userId);
1059
1059
  this.ensureCollaborator(actor);
1060
- if (!actor.collaboratorId) {
1061
- throw new common_1.BadRequestException('Collaborator context is required.');
1062
- }
1063
1060
  const pagination = this.normalizePaginationParams(paginationParams, {
1064
1061
  defaultSortField: 'name',
1065
1062
  defaultSortOrder: 'asc',
1066
1063
  allowedSortFields: ['name', 'code', 'clientName', 'startDate', 'endDate'],
1067
1064
  });
1068
- const params = [actor.collaboratorId];
1065
+ const filter = this.buildIdFilter(actor.visibleProjectIds, 'p.id', actor.isDirector);
1066
+ const params = [...filter.params];
1069
1067
  const filters = [
1070
1068
  'p.deleted_at IS NULL',
1071
1069
  'pa.deleted_at IS NULL',
1072
- `pa.collaborator_id = $1`,
1073
1070
  `pa.status IN ('planned', 'active')`,
1071
+ filter.clause,
1074
1072
  ];
1075
1073
  if (pagination.search) {
1076
1074
  const searchPlaceholder = this.param(params, `%${pagination.search}%`);
@@ -1120,21 +1118,25 @@ let OperationsService = OperationsService_1 = class OperationsService {
1120
1118
  var _a, _b;
1121
1119
  const actor = await this.getActorContext(userId);
1122
1120
  this.ensureCollaborator(actor);
1123
- if (!actor.collaboratorId) {
1124
- throw new common_1.BadRequestException('Collaborator context is required.');
1125
- }
1126
1121
  const pagination = this.normalizePaginationParams(paginationParams, {
1127
1122
  defaultSortField: 'name',
1128
1123
  defaultSortOrder: 'asc',
1129
1124
  allowedSortFields: ['name', 'projectName', 'status', 'createdAt'],
1130
1125
  });
1131
- const params = [actor.collaboratorId];
1126
+ const projectFilter = this.buildIdFilter(actor.visibleProjectIds, 'COALESCE(t.project_id, pa.project_id)', actor.isDirector);
1127
+ const params = [...projectFilter.params];
1132
1128
  const filters = [
1133
1129
  't.deleted_at IS NULL',
1134
- 'pa.deleted_at IS NULL',
1135
1130
  'p.deleted_at IS NULL',
1136
- `pa.collaborator_id = $1`,
1137
- `pa.status IN ('planned', 'active')`,
1131
+ projectFilter.clause,
1132
+ `(
1133
+ t.project_id IS NOT NULL
1134
+ OR (
1135
+ pa.id IS NOT NULL
1136
+ AND pa.deleted_at IS NULL
1137
+ AND pa.status IN ('planned', 'active')
1138
+ )
1139
+ )`,
1138
1140
  ];
1139
1141
  if (pagination.search) {
1140
1142
  const searchPlaceholder = this.param(params, `%${pagination.search}%`);
@@ -1149,7 +1151,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
1149
1151
  filters.push(`pa.id = ${this.param(params, paginationParams.projectAssignmentId)}`);
1150
1152
  }
1151
1153
  if (paginationParams.projectId) {
1152
- filters.push(`pa.project_id = ${this.param(params, paginationParams.projectId)}`);
1154
+ filters.push(`COALESCE(t.project_id, pa.project_id) = ${this.param(params, paginationParams.projectId)}`);
1153
1155
  }
1154
1156
  if (paginationParams.status) {
1155
1157
  filters.push(`t.status = ${this.param(params, paginationParams.status)}`);
@@ -1157,10 +1159,10 @@ let OperationsService = OperationsService_1 = class OperationsService {
1157
1159
  const whereClause = filters.join(' AND ');
1158
1160
  const totalRow = await this.querySingle(`SELECT COUNT(*)::text AS total
1159
1161
  FROM operations_task t
1160
- JOIN operations_project_assignment pa
1162
+ LEFT JOIN operations_project_assignment pa
1161
1163
  ON pa.id = t.project_assignment_id
1162
1164
  JOIN operations_project p
1163
- ON p.id = pa.project_id
1165
+ ON p.id = COALESCE(t.project_id, pa.project_id)
1164
1166
  WHERE ${whereClause}`, params);
1165
1167
  const sortColumn = (_a = {
1166
1168
  name: 't.name',
@@ -1175,16 +1177,16 @@ let OperationsService = OperationsService_1 = class OperationsService {
1175
1177
  t.name,
1176
1178
  t.description,
1177
1179
  t.status,
1178
- pa.project_id AS "projectId",
1180
+ COALESCE(t.project_id, pa.project_id) AS "projectId",
1179
1181
  pa.id AS "projectAssignmentId",
1180
1182
  p.name AS "projectName",
1181
1183
  p.code AS "projectCode",
1182
1184
  t.created_at AS "createdAt"
1183
1185
  FROM operations_task t
1184
- JOIN operations_project_assignment pa
1186
+ LEFT JOIN operations_project_assignment pa
1185
1187
  ON pa.id = t.project_assignment_id
1186
1188
  JOIN operations_project p
1187
- ON p.id = pa.project_id
1189
+ ON p.id = COALESCE(t.project_id, pa.project_id)
1188
1190
  WHERE ${whereClause}
1189
1191
  ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, t.id ASC
1190
1192
  LIMIT ${limitPlaceholder}
@@ -1463,8 +1465,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
1463
1465
  const resolvedTask = data.taskId
1464
1466
  ? await this.getOwnedTaskRecord(tx, actor.collaboratorId, data.taskId)
1465
1467
  : 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
+ if (resolvedTask && resolvedTask.projectId !== assignment.projectId) {
1469
+ throw new common_1.BadRequestException('The selected task does not belong to the chosen project.');
1468
1470
  }
1469
1471
  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
1472
  if (!activityLabel) {
@@ -1500,6 +1502,68 @@ let OperationsService = OperationsService_1 = class OperationsService {
1500
1502
  });
1501
1503
  return this.getTimesheetEntryByIdForActor(actor, createdEntryId);
1502
1504
  }
1505
+ async updateTimesheetEntry(userId, entryId, data) {
1506
+ var _a;
1507
+ const actor = await this.getActorContext(userId);
1508
+ this.ensureCollaborator(actor);
1509
+ this.requireFields(data, ['workDate', 'duration']);
1510
+ if (!actor.collaboratorId && !actor.isDirector) {
1511
+ throw new common_1.BadRequestException('Collaborator context is required.');
1512
+ }
1513
+ const entry = await this.getTimesheetEntryByIdForActor(actor, entryId);
1514
+ if (!actor.isDirector && entry.collaboratorId !== actor.collaboratorId) {
1515
+ throw new common_1.ForbiddenException('Only the entry owner can update this timesheet entry.');
1516
+ }
1517
+ if (!['draft', 'rejected'].includes(entry.status)) {
1518
+ throw new common_1.BadRequestException('Only draft or rejected timesheet entries can be edited.');
1519
+ }
1520
+ const collaboratorId = actor.isDirector
1521
+ ? entry.collaboratorId
1522
+ : actor.collaboratorId;
1523
+ const durationMinutes = this.normalizeDurationMinutes(data.duration, data.unit);
1524
+ const taskLabel = (_a = this.normalizeOptionalText(data.taskName)) !== null && _a !== void 0 ? _a : this.normalizeOptionalText(data.activityLabel);
1525
+ const targetWeek = this.getWorkWeekRange(data.workDate);
1526
+ const isSameWeek = entry.weekStartDate === targetWeek.weekStartDate &&
1527
+ entry.weekEndDate === targetWeek.weekEndDate;
1528
+ await this.prisma.$transaction(async (tx) => {
1529
+ var _a, _b, _c, _d, _e, _f, _g;
1530
+ const assignment = await this.resolveOwnedProjectAssignment(tx, collaboratorId, {
1531
+ projectId: (_a = data.projectId) !== null && _a !== void 0 ? _a : null,
1532
+ projectAssignmentId: (_b = data.projectAssignmentId) !== null && _b !== void 0 ? _b : null,
1533
+ });
1534
+ const resolvedTask = data.taskId
1535
+ ? await this.getOwnedTaskRecord(tx, collaboratorId, data.taskId)
1536
+ : null;
1537
+ if (resolvedTask && resolvedTask.projectId !== assignment.projectId) {
1538
+ throw new common_1.BadRequestException('The selected task does not belong to the chosen project.');
1539
+ }
1540
+ 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;
1541
+ if (!activityLabel) {
1542
+ throw new common_1.BadRequestException('A task is required for the timesheet entry.');
1543
+ }
1544
+ const targetTimesheetId = isSameWeek
1545
+ ? entry.timesheetId
1546
+ : await this.getOrCreateTimesheetForWorkDate(tx, collaboratorId, data.workDate);
1547
+ await tx.$executeRawUnsafe(`UPDATE operations_timesheet_entry
1548
+ SET timesheet_id = $1,
1549
+ project_assignment_id = $2,
1550
+ task_id = $3,
1551
+ activity_label = $4,
1552
+ work_date = $5::date,
1553
+ duration_minutes = $6,
1554
+ hours = $7,
1555
+ description = $8,
1556
+ updated_at = NOW()
1557
+ WHERE id = $9
1558
+ AND deleted_at IS NULL`, targetTimesheetId, 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), entryId);
1559
+ await this.refreshTimesheetTotal(tx, entry.timesheetId);
1560
+ if (targetTimesheetId !== entry.timesheetId) {
1561
+ await this.refreshTimesheetTotal(tx, targetTimesheetId);
1562
+ }
1563
+ await this.cleanupEmptyEditableTimesheet(tx, entry.timesheetId);
1564
+ });
1565
+ return this.getTimesheetEntryByIdForActor(actor, entryId);
1566
+ }
1503
1567
  async removeTimesheetEntry(userId, entryId) {
1504
1568
  const actor = await this.getActorContext(userId);
1505
1569
  this.ensureCollaborator(actor);
@@ -1520,6 +1584,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
1520
1584
  WHERE id = $1
1521
1585
  AND deleted_at IS NULL`, entryId);
1522
1586
  await this.refreshTimesheetTotal(tx, entry.timesheetId);
1587
+ await this.cleanupEmptyEditableTimesheet(tx, entry.timesheetId);
1523
1588
  });
1524
1589
  return { success: true };
1525
1590
  }
@@ -1733,7 +1798,11 @@ let OperationsService = OperationsService_1 = class OperationsService {
1733
1798
  updated_at
1734
1799
  ) VALUES (
1735
1800
  $1, $2, $3, $4,
1736
- $5, $6, $7, $8, $9, $10, $11,
1801
+ $5::text::operations_contract_template_contract_category_49bb07a713_enum,
1802
+ $6::text::operations_contract_template_contract_type_3962dbda6a_enum,
1803
+ $7::text::operations_contract_template_billing_model_384a7c60e2_enum,
1804
+ $8::text::operations_contract_template_signature_status_56cb6d625b_enum,
1805
+ $9, $10::text::operations_contract_template_status_c9d2e90231_enum, $11,
1737
1806
  NOW(), NOW()
1738
1807
  )
1739
1808
  RETURNING id`, await this.generateUniqueContractTemplateSlug(tx, name), nextCode, name, this.normalizeOptionalText(data.description), (_e = data.contractCategory) !== null && _e !== void 0 ? _e : 'client', (_f = data.contractType) !== null && _f !== void 0 ? _f : 'service_agreement', (_g = data.billingModel) !== null && _g !== void 0 ? _g : 'time_and_material', (_h = data.signatureStatus) !== null && _h !== void 0 ? _h : 'not_started', isActive, nextStatus, this.normalizeOptionalText(data.contentHtml)));
@@ -1775,12 +1844,12 @@ let OperationsService = OperationsService_1 = class OperationsService {
1775
1844
  code = $2,
1776
1845
  name = $3,
1777
1846
  description = $4,
1778
- contract_category = $5,
1779
- contract_type = $6,
1780
- billing_model = $7,
1781
- signature_status = $8,
1847
+ contract_category = $5::text::operations_contract_template_contract_category_49bb07a713_enum,
1848
+ contract_type = $6::text::operations_contract_template_contract_type_3962dbda6a_enum,
1849
+ billing_model = $7::text::operations_contract_template_billing_model_384a7c60e2_enum,
1850
+ signature_status = $8::text::operations_contract_template_signature_status_56cb6d625b_enum,
1782
1851
  is_active = $9,
1783
- status = $10,
1852
+ status = $10::text::operations_contract_template_status_c9d2e90231_enum,
1784
1853
  content_html = $11,
1785
1854
  updated_at = NOW()
1786
1855
  WHERE id = $12`, nextSlug, nextCode, nextName, data.description !== undefined
@@ -3026,7 +3095,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
3026
3095
  return this.listSingleTimesheet(actor, timesheetId);
3027
3096
  }
3028
3097
  async submitTimesheet(userId, timesheetId) {
3029
- var _a, _b;
3098
+ var _a, _b, _c;
3030
3099
  const actor = await this.getActorContext(userId);
3031
3100
  const current = await this.getTimesheetById(timesheetId);
3032
3101
  if (!actor.isDirector && current.collaboratorId !== actor.collaboratorId) {
@@ -3036,7 +3105,15 @@ let OperationsService = OperationsService_1 = class OperationsService {
3036
3105
  throw new common_1.BadRequestException('Only draft or rejected timesheets can be submitted.');
3037
3106
  }
3038
3107
  const collaborator = await this.getCollaboratorById(current.collaboratorId);
3039
- const approverId = (_b = (_a = current.approverCollaboratorId) !== null && _a !== void 0 ? _a : collaborator.supervisorId) !== null && _b !== void 0 ? _b : null;
3108
+ const projectManagerRow = await this.querySingle(`SELECT p.manager_collaborator_id AS "managerCollaboratorId"
3109
+ FROM operations_timesheet_entry e
3110
+ LEFT JOIN operations_project_assignment pa ON pa.id = e.project_assignment_id
3111
+ LEFT JOIN operations_project p ON p.id = pa.project_id
3112
+ WHERE e.timesheet_id = $1
3113
+ AND e.deleted_at IS NULL
3114
+ AND p.manager_collaborator_id IS NOT NULL
3115
+ LIMIT 1`, [timesheetId]);
3116
+ const approverId = (_c = (_b = (_a = projectManagerRow === null || projectManagerRow === void 0 ? void 0 : projectManagerRow.managerCollaboratorId) !== null && _a !== void 0 ? _a : current.approverCollaboratorId) !== null && _b !== void 0 ? _b : collaborator.supervisorId) !== null && _c !== void 0 ? _c : null;
3040
3117
  if (!approverId) {
3041
3118
  throw new common_1.BadRequestException('An approver is required before submitting a timesheet.');
3042
3119
  }
@@ -3192,7 +3269,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
3192
3269
  FROM operations_schedule_adjustment_day
3193
3270
  WHERE schedule_adjustment_request_id = ANY($1::int[])
3194
3271
  ORDER BY id ASC`, [requests.map((item) => item.id)]);
3195
- const currentSchedule = await this.queryRows(`SELECT collaborator_id AS "collaboratorId",
3272
+ const currentSchedule = await this.queryRows(`SELECT DISTINCT ON (collaborator_id, weekday)
3273
+ collaborator_id AS "collaboratorId",
3196
3274
  weekday,
3197
3275
  is_working_day AS "isWorkingDay",
3198
3276
  start_time AS "startTime",
@@ -3200,7 +3278,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
3200
3278
  break_minutes AS "breakMinutes"
3201
3279
  FROM operations_collaborator_schedule_day
3202
3280
  WHERE collaborator_id = ANY($1::int[])
3203
- ORDER BY id ASC`, [this.uniqueNumbers(requests.map((item) => item.collaboratorId))]);
3281
+ ORDER BY collaborator_id, weekday, id DESC`, [this.uniqueNumbers(requests.map((item) => item.collaboratorId))]);
3204
3282
  const grouped = this.groupBy(days, 'requestId');
3205
3283
  const currentScheduleByCollaborator = this.groupBy(currentSchedule, 'collaboratorId');
3206
3284
  return requests.map((request) => {
@@ -3524,17 +3602,44 @@ let OperationsService = OperationsService_1 = class OperationsService {
3524
3602
  };
3525
3603
  }
3526
3604
  async getCollaboratorByUserId(userId) {
3527
- return this.querySingle(`SELECT c.id,
3605
+ return this.querySingle(`WITH linked_collaborator AS (
3606
+ SELECT person_id
3607
+ FROM operations_collaborator
3608
+ WHERE user_id = $1
3609
+ AND deleted_at IS NULL
3610
+ ORDER BY id DESC
3611
+ LIMIT 1
3612
+ )
3613
+ SELECT c.id,
3614
+ c.user_id AS "userId",
3615
+ c.person_id AS "personId",
3528
3616
  COALESCE(NULLIF(c.display_name, ''), person_record.name) AS "displayName",
3529
3617
  s.id AS "supervisorId",
3530
- s.display_name AS "supervisorName"
3618
+ s.display_name AS "supervisorName",
3619
+ COUNT(pa.id) FILTER (
3620
+ WHERE pa.deleted_at IS NULL
3621
+ AND pa.status IN ('planned', 'active')
3622
+ )::int AS "activeAssignments"
3531
3623
  FROM operations_collaborator c
3532
3624
  LEFT JOIN person person_record
3533
3625
  ON person_record.id = c.person_id
3534
3626
  LEFT JOIN operations_collaborator s
3535
3627
  ON s.id = c.supervisor_collaborator_id
3536
- WHERE c.user_id = $1
3537
- AND c.deleted_at IS NULL`, [userId]);
3628
+ LEFT JOIN operations_project_assignment pa
3629
+ ON pa.collaborator_id = c.id
3630
+ WHERE c.deleted_at IS NULL
3631
+ AND (
3632
+ c.user_id = $1
3633
+ OR (
3634
+ c.person_id IS NOT NULL
3635
+ AND c.person_id = (SELECT person_id FROM linked_collaborator)
3636
+ )
3637
+ )
3638
+ GROUP BY c.id, person_record.id, s.id
3639
+ ORDER BY "activeAssignments" DESC,
3640
+ CASE WHEN c.user_id = $1 THEN 0 ELSE 1 END,
3641
+ c.id ASC
3642
+ LIMIT 1`, [userId]);
3538
3643
  }
3539
3644
  async getCollaboratorById(collaboratorId) {
3540
3645
  const collaborator = await this.querySingle(`SELECT c.id,
@@ -4584,7 +4689,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
4584
4689
  return timesheet;
4585
4690
  }
4586
4691
  async replaceTimesheetEntries(client, timesheetId, entries, collaboratorId) {
4587
- var _a, _b, _c, _d, _e, _f;
4692
+ var _a, _b, _c, _d, _e, _f, _g, _h;
4588
4693
  await client.$executeRawUnsafe(`UPDATE operations_timesheet_entry
4589
4694
  SET deleted_at = NOW()
4590
4695
  WHERE timesheet_id = $1
@@ -4594,17 +4699,19 @@ let OperationsService = OperationsService_1 = class OperationsService {
4594
4699
  const assignmentIds = entries
4595
4700
  .map((entry) => entry.projectAssignmentId)
4596
4701
  .filter((value) => typeof value === 'number');
4702
+ const assignmentMap = new Map();
4597
4703
  if (assignmentIds.length) {
4598
- const assignments = (await client.$queryRawUnsafe(`SELECT id, collaborator_id AS "collaboratorId"
4704
+ const assignments = (await client.$queryRawUnsafe(`SELECT id,
4705
+ collaborator_id AS "collaboratorId",
4706
+ project_id AS "projectId"
4599
4707
  FROM operations_project_assignment
4600
4708
  WHERE id = ANY($1::int[])
4601
4709
  AND deleted_at IS NULL`, assignmentIds));
4602
- const assignmentMap = new Map(assignments.map((assignment) => [
4603
- assignment.id,
4604
- assignment.collaboratorId,
4605
- ]));
4710
+ assignments.forEach((assignment) => {
4711
+ assignmentMap.set(assignment.id, assignment);
4712
+ });
4606
4713
  for (const assignmentId of assignmentIds) {
4607
- if (assignmentMap.get(assignmentId) !== collaboratorId) {
4714
+ if (((_a = assignmentMap.get(assignmentId)) === null || _a === void 0 ? void 0 : _a.collaboratorId) !== collaboratorId) {
4608
4715
  throw new common_1.ForbiddenException('Timesheet entries must use assignments owned by the target collaborator.');
4609
4716
  }
4610
4717
  }
@@ -4615,16 +4722,19 @@ let OperationsService = OperationsService_1 = class OperationsService {
4615
4722
  const resolvedTask = entry.taskId
4616
4723
  ? await this.getOwnedTaskRecord(client, collaboratorId, entry.taskId)
4617
4724
  : null;
4725
+ const selectedAssignment = entry.projectAssignmentId
4726
+ ? (_b = assignmentMap.get(entry.projectAssignmentId)) !== null && _b !== void 0 ? _b : null
4727
+ : null;
4618
4728
  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.');
4729
+ selectedAssignment &&
4730
+ resolvedTask.projectId !== selectedAssignment.projectId) {
4731
+ throw new common_1.BadRequestException('The selected task does not belong to the chosen project.');
4622
4732
  }
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;
4733
+ const resolvedAssignmentId = (_d = (_c = entry.projectAssignmentId) !== null && _c !== void 0 ? _c : resolvedTask === null || resolvedTask === void 0 ? void 0 : resolvedTask.projectAssignmentId) !== null && _d !== void 0 ? _d : null;
4624
4734
  if (entry.taskId && !resolvedAssignmentId) {
4625
4735
  throw new common_1.BadRequestException('The selected task must belong to a project assignment.');
4626
4736
  }
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);
4737
+ const activityLabel = (_f = (_e = resolvedTask === null || resolvedTask === void 0 ? void 0 : resolvedTask.name) !== null && _e !== void 0 ? _e : this.normalizeOptionalText(entry.taskName)) !== null && _f !== void 0 ? _f : this.normalizeOptionalText(entry.activityLabel);
4628
4738
  if (!entry.workDate) {
4629
4739
  throw new common_1.BadRequestException('Timesheet entry workDate is required.');
4630
4740
  }
@@ -4639,7 +4749,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
4639
4749
  description,
4640
4750
  created_at,
4641
4751
  updated_at
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));
4752
+ ) VALUES ($1, $2, $3, $4, $5::date, $6, $7, $8, NOW(), NOW())`, timesheetId, resolvedAssignmentId, (_h = (_g = resolvedTask === null || resolvedTask === void 0 ? void 0 : resolvedTask.id) !== null && _g !== void 0 ? _g : entry.taskId) !== null && _h !== void 0 ? _h : null, activityLabel !== null && activityLabel !== void 0 ? activityLabel : null, entry.workDate, durationMinutes, hours, this.normalizeOptionalText(entry.description));
4643
4753
  }
4644
4754
  }
4645
4755
  async refreshTimesheetTotal(client, timesheetId) {
@@ -4665,6 +4775,29 @@ let OperationsService = OperationsService_1 = class OperationsService {
4665
4775
  updated_at = NOW()
4666
4776
  WHERE id = $1`, timesheetId);
4667
4777
  }
4778
+ async cleanupEmptyEditableTimesheet(client, timesheetId) {
4779
+ var _a;
4780
+ const candidate = (await client.$queryRawUnsafe(`SELECT t.id
4781
+ FROM operations_timesheet t
4782
+ WHERE t.id = $1
4783
+ AND t.deleted_at IS NULL
4784
+ AND t.status IN ('draft', 'rejected')
4785
+ AND NOT EXISTS (
4786
+ SELECT 1
4787
+ FROM operations_timesheet_entry e
4788
+ WHERE e.timesheet_id = t.id
4789
+ AND e.deleted_at IS NULL
4790
+ )
4791
+ LIMIT 1`, timesheetId));
4792
+ if (!((_a = candidate[0]) === null || _a === void 0 ? void 0 : _a.id)) {
4793
+ return;
4794
+ }
4795
+ await client.$executeRawUnsafe(`UPDATE operations_timesheet
4796
+ SET deleted_at = NOW(),
4797
+ updated_at = NOW()
4798
+ WHERE id = $1
4799
+ AND deleted_at IS NULL`, timesheetId);
4800
+ }
4668
4801
  async upsertApproval(client, input) {
4669
4802
  var _a, _b;
4670
4803
  const existing = (await client.$queryRawUnsafe(`SELECT id