@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.
- package/dist/controllers/operations-timesheets.controller.d.ts +21 -0
- package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
- package/dist/controllers/operations-timesheets.controller.js +12 -0
- package/dist/controllers/operations-timesheets.controller.js.map +1 -1
- package/dist/dto/update-collaborator-type.dto.d.ts +3 -1
- package/dist/dto/update-collaborator-type.dto.d.ts.map +1 -1
- package/dist/dto/update-collaborator-type.dto.js +2 -1
- package/dist/dto/update-collaborator-type.dto.js.map +1 -1
- package/dist/operations.service.d.ts +22 -0
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +180 -47
- package/dist/operations.service.js.map +1 -1
- package/dist/operations.service.spec.js +73 -0
- package/dist/operations.service.spec.js.map +1 -1
- package/hedhog/data/menu.yaml +26 -26
- package/hedhog/data/operations_collaborator_type.yaml +76 -76
- package/hedhog/data/route.yaml +13 -0
- package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +5 -3
- package/hedhog/frontend/app/_components/timesheet-task-create-sheet.tsx.ejs +1 -0
- package/hedhog/frontend/app/approvals/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +26 -15
- package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +235 -72
- package/hedhog/frontend/app/timesheets/page.tsx.ejs +344 -134
- package/hedhog/frontend/messages/en.json +5 -0
- package/hedhog/frontend/messages/pt.json +7 -2
- package/hedhog/table/operations_collaborator.yaml +18 -18
- package/hedhog/table/operations_collaborator_equity_participation.yaml +43 -43
- package/hedhog/table/operations_collaborator_type.yaml +33 -33
- package/hedhog/table/operations_contract_document.yaml +33 -33
- package/package.json +4 -4
- package/src/controllers/operations-timesheets.controller.ts +13 -0
- package/src/dto/create-collaborator-type.dto.ts +43 -43
- package/src/dto/create-collaborator.dto.ts +223 -223
- package/src/dto/list-collaborator-types.dto.ts +15 -15
- package/src/dto/list-collaborators.dto.ts +30 -30
- package/src/dto/update-collaborator-type.dto.ts +4 -3
- package/src/dto/update-collaborator.dto.ts +3 -3
- package/src/operations.service.spec.ts +96 -0
- 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
|
|
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
|
|
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
|
-
|
|
1137
|
-
`
|
|
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.
|
|
1467
|
-
throw new common_1.BadRequestException('The selected task does not belong to the chosen project
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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(`
|
|
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
|
-
|
|
3537
|
-
|
|
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,
|
|
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
|
-
|
|
4603
|
-
assignment.id,
|
|
4604
|
-
|
|
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
|
-
|
|
4620
|
-
resolvedTask.
|
|
4621
|
-
throw new common_1.BadRequestException('The selected task does not belong to the chosen project
|
|
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 = (
|
|
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 = (
|
|
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, (
|
|
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
|