@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
|
@@ -2065,22 +2065,19 @@ export class OperationsService {
|
|
|
2065
2065
|
const actor = await this.getActorContext(userId);
|
|
2066
2066
|
this.ensureCollaborator(actor);
|
|
2067
2067
|
|
|
2068
|
-
if (!actor.collaboratorId) {
|
|
2069
|
-
throw new BadRequestException('Collaborator context is required.');
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
2068
|
const pagination = this.normalizePaginationParams(paginationParams, {
|
|
2073
2069
|
defaultSortField: 'name',
|
|
2074
2070
|
defaultSortOrder: 'asc',
|
|
2075
2071
|
allowedSortFields: ['name', 'code', 'clientName', 'startDate', 'endDate'],
|
|
2076
2072
|
});
|
|
2077
2073
|
|
|
2078
|
-
const
|
|
2074
|
+
const filter = this.buildIdFilter(actor.visibleProjectIds, 'p.id', actor.isDirector);
|
|
2075
|
+
const params: unknown[] = [...filter.params];
|
|
2079
2076
|
const filters = [
|
|
2080
2077
|
'p.deleted_at IS NULL',
|
|
2081
2078
|
'pa.deleted_at IS NULL',
|
|
2082
|
-
`pa.collaborator_id = $1`,
|
|
2083
2079
|
`pa.status IN ('planned', 'active')`,
|
|
2080
|
+
filter.clause,
|
|
2084
2081
|
];
|
|
2085
2082
|
|
|
2086
2083
|
if (pagination.search) {
|
|
@@ -2174,23 +2171,30 @@ export class OperationsService {
|
|
|
2174
2171
|
const actor = await this.getActorContext(userId);
|
|
2175
2172
|
this.ensureCollaborator(actor);
|
|
2176
2173
|
|
|
2177
|
-
if (!actor.collaboratorId) {
|
|
2178
|
-
throw new BadRequestException('Collaborator context is required.');
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
2174
|
const pagination = this.normalizePaginationParams(paginationParams, {
|
|
2182
2175
|
defaultSortField: 'name',
|
|
2183
2176
|
defaultSortOrder: 'asc',
|
|
2184
2177
|
allowedSortFields: ['name', 'projectName', 'status', 'createdAt'],
|
|
2185
2178
|
});
|
|
2186
2179
|
|
|
2187
|
-
const
|
|
2180
|
+
const projectFilter = this.buildIdFilter(
|
|
2181
|
+
actor.visibleProjectIds,
|
|
2182
|
+
'COALESCE(t.project_id, pa.project_id)',
|
|
2183
|
+
actor.isDirector
|
|
2184
|
+
);
|
|
2185
|
+
const params: unknown[] = [...projectFilter.params];
|
|
2188
2186
|
const filters = [
|
|
2189
2187
|
't.deleted_at IS NULL',
|
|
2190
|
-
'pa.deleted_at IS NULL',
|
|
2191
2188
|
'p.deleted_at IS NULL',
|
|
2192
|
-
|
|
2193
|
-
`
|
|
2189
|
+
projectFilter.clause,
|
|
2190
|
+
`(
|
|
2191
|
+
t.project_id IS NOT NULL
|
|
2192
|
+
OR (
|
|
2193
|
+
pa.id IS NOT NULL
|
|
2194
|
+
AND pa.deleted_at IS NULL
|
|
2195
|
+
AND pa.status IN ('planned', 'active')
|
|
2196
|
+
)
|
|
2197
|
+
)`,
|
|
2194
2198
|
];
|
|
2195
2199
|
|
|
2196
2200
|
if (pagination.search) {
|
|
@@ -2208,7 +2212,12 @@ export class OperationsService {
|
|
|
2208
2212
|
}
|
|
2209
2213
|
|
|
2210
2214
|
if (paginationParams.projectId) {
|
|
2211
|
-
filters.push(
|
|
2215
|
+
filters.push(
|
|
2216
|
+
`COALESCE(t.project_id, pa.project_id) = ${this.param(
|
|
2217
|
+
params,
|
|
2218
|
+
paginationParams.projectId
|
|
2219
|
+
)}`
|
|
2220
|
+
);
|
|
2212
2221
|
}
|
|
2213
2222
|
|
|
2214
2223
|
if (paginationParams.status) {
|
|
@@ -2219,10 +2228,10 @@ export class OperationsService {
|
|
|
2219
2228
|
const totalRow = await this.querySingle<{ total: string }>(
|
|
2220
2229
|
`SELECT COUNT(*)::text AS total
|
|
2221
2230
|
FROM operations_task t
|
|
2222
|
-
JOIN operations_project_assignment pa
|
|
2231
|
+
LEFT JOIN operations_project_assignment pa
|
|
2223
2232
|
ON pa.id = t.project_assignment_id
|
|
2224
2233
|
JOIN operations_project p
|
|
2225
|
-
ON p.id = pa.project_id
|
|
2234
|
+
ON p.id = COALESCE(t.project_id, pa.project_id)
|
|
2226
2235
|
WHERE ${whereClause}`,
|
|
2227
2236
|
params
|
|
2228
2237
|
);
|
|
@@ -2254,16 +2263,16 @@ export class OperationsService {
|
|
|
2254
2263
|
t.name,
|
|
2255
2264
|
t.description,
|
|
2256
2265
|
t.status,
|
|
2257
|
-
pa.project_id AS "projectId",
|
|
2266
|
+
COALESCE(t.project_id, pa.project_id) AS "projectId",
|
|
2258
2267
|
pa.id AS "projectAssignmentId",
|
|
2259
2268
|
p.name AS "projectName",
|
|
2260
2269
|
p.code AS "projectCode",
|
|
2261
2270
|
t.created_at AS "createdAt"
|
|
2262
2271
|
FROM operations_task t
|
|
2263
|
-
JOIN operations_project_assignment pa
|
|
2272
|
+
LEFT JOIN operations_project_assignment pa
|
|
2264
2273
|
ON pa.id = t.project_assignment_id
|
|
2265
2274
|
JOIN operations_project p
|
|
2266
|
-
ON p.id = pa.project_id
|
|
2275
|
+
ON p.id = COALESCE(t.project_id, pa.project_id)
|
|
2267
2276
|
WHERE ${whereClause}
|
|
2268
2277
|
ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, t.id ASC
|
|
2269
2278
|
LIMIT ${limitPlaceholder}
|
|
@@ -2699,9 +2708,9 @@ export class OperationsService {
|
|
|
2699
2708
|
? await this.getOwnedTaskRecord(tx as any, actor.collaboratorId as number, data.taskId)
|
|
2700
2709
|
: null;
|
|
2701
2710
|
|
|
2702
|
-
if (resolvedTask && resolvedTask.
|
|
2711
|
+
if (resolvedTask && resolvedTask.projectId !== assignment.projectId) {
|
|
2703
2712
|
throw new BadRequestException(
|
|
2704
|
-
'The selected task does not belong to the chosen project
|
|
2713
|
+
'The selected task does not belong to the chosen project.'
|
|
2705
2714
|
);
|
|
2706
2715
|
}
|
|
2707
2716
|
|
|
@@ -2760,6 +2769,116 @@ export class OperationsService {
|
|
|
2760
2769
|
return this.getTimesheetEntryByIdForActor(actor, createdEntryId);
|
|
2761
2770
|
}
|
|
2762
2771
|
|
|
2772
|
+
async updateTimesheetEntry(
|
|
2773
|
+
userId: number,
|
|
2774
|
+
entryId: number,
|
|
2775
|
+
data: QuickTimesheetEntryPayload
|
|
2776
|
+
) {
|
|
2777
|
+
const actor = await this.getActorContext(userId);
|
|
2778
|
+
this.ensureCollaborator(actor);
|
|
2779
|
+
this.requireFields(data as Record<string, unknown>, ['workDate', 'duration']);
|
|
2780
|
+
|
|
2781
|
+
if (!actor.collaboratorId && !actor.isDirector) {
|
|
2782
|
+
throw new BadRequestException('Collaborator context is required.');
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
const entry = await this.getTimesheetEntryByIdForActor(actor, entryId);
|
|
2786
|
+
|
|
2787
|
+
if (!actor.isDirector && entry.collaboratorId !== actor.collaboratorId) {
|
|
2788
|
+
throw new ForbiddenException(
|
|
2789
|
+
'Only the entry owner can update this timesheet entry.'
|
|
2790
|
+
);
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
if (!['draft', 'rejected'].includes(entry.status)) {
|
|
2794
|
+
throw new BadRequestException(
|
|
2795
|
+
'Only draft or rejected timesheet entries can be edited.'
|
|
2796
|
+
);
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
const collaboratorId = actor.isDirector
|
|
2800
|
+
? entry.collaboratorId
|
|
2801
|
+
: (actor.collaboratorId as number);
|
|
2802
|
+
const durationMinutes = this.normalizeDurationMinutes(data.duration, data.unit);
|
|
2803
|
+
const taskLabel =
|
|
2804
|
+
this.normalizeOptionalText(data.taskName) ??
|
|
2805
|
+
this.normalizeOptionalText(data.activityLabel);
|
|
2806
|
+
const targetWeek = this.getWorkWeekRange(data.workDate);
|
|
2807
|
+
const isSameWeek =
|
|
2808
|
+
entry.weekStartDate === targetWeek.weekStartDate &&
|
|
2809
|
+
entry.weekEndDate === targetWeek.weekEndDate;
|
|
2810
|
+
|
|
2811
|
+
await this.prisma.$transaction(async (tx) => {
|
|
2812
|
+
const assignment = await this.resolveOwnedProjectAssignment(
|
|
2813
|
+
tx as any,
|
|
2814
|
+
collaboratorId,
|
|
2815
|
+
{
|
|
2816
|
+
projectId: data.projectId ?? null,
|
|
2817
|
+
projectAssignmentId: data.projectAssignmentId ?? null,
|
|
2818
|
+
}
|
|
2819
|
+
);
|
|
2820
|
+
|
|
2821
|
+
const resolvedTask = data.taskId
|
|
2822
|
+
? await this.getOwnedTaskRecord(tx as any, collaboratorId, data.taskId)
|
|
2823
|
+
: null;
|
|
2824
|
+
|
|
2825
|
+
if (resolvedTask && resolvedTask.projectId !== assignment.projectId) {
|
|
2826
|
+
throw new BadRequestException(
|
|
2827
|
+
'The selected task does not belong to the chosen project.'
|
|
2828
|
+
);
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
const activityLabel =
|
|
2832
|
+
resolvedTask?.name ?? taskLabel ?? assignment.roleLabel ?? assignment.projectName;
|
|
2833
|
+
|
|
2834
|
+
if (!activityLabel) {
|
|
2835
|
+
throw new BadRequestException('A task is required for the timesheet entry.');
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
const targetTimesheetId = isSameWeek
|
|
2839
|
+
? entry.timesheetId
|
|
2840
|
+
: await this.getOrCreateTimesheetForWorkDate(
|
|
2841
|
+
tx as any,
|
|
2842
|
+
collaboratorId,
|
|
2843
|
+
data.workDate
|
|
2844
|
+
);
|
|
2845
|
+
|
|
2846
|
+
await (tx as any).$executeRawUnsafe(
|
|
2847
|
+
`UPDATE operations_timesheet_entry
|
|
2848
|
+
SET timesheet_id = $1,
|
|
2849
|
+
project_assignment_id = $2,
|
|
2850
|
+
task_id = $3,
|
|
2851
|
+
activity_label = $4,
|
|
2852
|
+
work_date = $5::date,
|
|
2853
|
+
duration_minutes = $6,
|
|
2854
|
+
hours = $7,
|
|
2855
|
+
description = $8,
|
|
2856
|
+
updated_at = NOW()
|
|
2857
|
+
WHERE id = $9
|
|
2858
|
+
AND deleted_at IS NULL`,
|
|
2859
|
+
targetTimesheetId,
|
|
2860
|
+
assignment.id,
|
|
2861
|
+
resolvedTask?.id ?? data.taskId ?? null,
|
|
2862
|
+
activityLabel,
|
|
2863
|
+
data.workDate,
|
|
2864
|
+
durationMinutes,
|
|
2865
|
+
Number((durationMinutes / 60).toFixed(2)),
|
|
2866
|
+
this.normalizeOptionalText(data.description),
|
|
2867
|
+
entryId
|
|
2868
|
+
);
|
|
2869
|
+
|
|
2870
|
+
await this.refreshTimesheetTotal(tx as any, entry.timesheetId);
|
|
2871
|
+
|
|
2872
|
+
if (targetTimesheetId !== entry.timesheetId) {
|
|
2873
|
+
await this.refreshTimesheetTotal(tx as any, targetTimesheetId);
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
await this.cleanupEmptyEditableTimesheet(tx as any, entry.timesheetId);
|
|
2877
|
+
});
|
|
2878
|
+
|
|
2879
|
+
return this.getTimesheetEntryByIdForActor(actor, entryId);
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2763
2882
|
async removeTimesheetEntry(userId: number, entryId: number) {
|
|
2764
2883
|
const actor = await this.getActorContext(userId);
|
|
2765
2884
|
this.ensureCollaborator(actor);
|
|
@@ -2793,6 +2912,7 @@ export class OperationsService {
|
|
|
2793
2912
|
);
|
|
2794
2913
|
|
|
2795
2914
|
await this.refreshTimesheetTotal(tx as any, entry.timesheetId);
|
|
2915
|
+
await this.cleanupEmptyEditableTimesheet(tx as any, entry.timesheetId);
|
|
2796
2916
|
});
|
|
2797
2917
|
|
|
2798
2918
|
return { success: true };
|
|
@@ -3129,7 +3249,11 @@ export class OperationsService {
|
|
|
3129
3249
|
updated_at
|
|
3130
3250
|
) VALUES (
|
|
3131
3251
|
$1, $2, $3, $4,
|
|
3132
|
-
$5,
|
|
3252
|
+
$5::text::operations_contract_template_contract_category_49bb07a713_enum,
|
|
3253
|
+
$6::text::operations_contract_template_contract_type_3962dbda6a_enum,
|
|
3254
|
+
$7::text::operations_contract_template_billing_model_384a7c60e2_enum,
|
|
3255
|
+
$8::text::operations_contract_template_signature_status_56cb6d625b_enum,
|
|
3256
|
+
$9, $10::text::operations_contract_template_status_c9d2e90231_enum, $11,
|
|
3133
3257
|
NOW(), NOW()
|
|
3134
3258
|
)
|
|
3135
3259
|
RETURNING id`,
|
|
@@ -3218,12 +3342,12 @@ export class OperationsService {
|
|
|
3218
3342
|
code = $2,
|
|
3219
3343
|
name = $3,
|
|
3220
3344
|
description = $4,
|
|
3221
|
-
contract_category = $5,
|
|
3222
|
-
contract_type = $6,
|
|
3223
|
-
billing_model = $7,
|
|
3224
|
-
signature_status = $8,
|
|
3345
|
+
contract_category = $5::text::operations_contract_template_contract_category_49bb07a713_enum,
|
|
3346
|
+
contract_type = $6::text::operations_contract_template_contract_type_3962dbda6a_enum,
|
|
3347
|
+
billing_model = $7::text::operations_contract_template_billing_model_384a7c60e2_enum,
|
|
3348
|
+
signature_status = $8::text::operations_contract_template_signature_status_56cb6d625b_enum,
|
|
3225
3349
|
is_active = $9,
|
|
3226
|
-
status = $10,
|
|
3350
|
+
status = $10::text::operations_contract_template_status_c9d2e90231_enum,
|
|
3227
3351
|
content_html = $11,
|
|
3228
3352
|
updated_at = NOW()
|
|
3229
3353
|
WHERE id = $12`,
|
|
@@ -5126,8 +5250,26 @@ export class OperationsService {
|
|
|
5126
5250
|
}
|
|
5127
5251
|
|
|
5128
5252
|
const collaborator = await this.getCollaboratorById(current.collaboratorId);
|
|
5253
|
+
|
|
5254
|
+
const projectManagerRow = await this.querySingle<{
|
|
5255
|
+
managerCollaboratorId: number | null;
|
|
5256
|
+
}>(
|
|
5257
|
+
`SELECT p.manager_collaborator_id AS "managerCollaboratorId"
|
|
5258
|
+
FROM operations_timesheet_entry e
|
|
5259
|
+
LEFT JOIN operations_project_assignment pa ON pa.id = e.project_assignment_id
|
|
5260
|
+
LEFT JOIN operations_project p ON p.id = pa.project_id
|
|
5261
|
+
WHERE e.timesheet_id = $1
|
|
5262
|
+
AND e.deleted_at IS NULL
|
|
5263
|
+
AND p.manager_collaborator_id IS NOT NULL
|
|
5264
|
+
LIMIT 1`,
|
|
5265
|
+
[timesheetId]
|
|
5266
|
+
);
|
|
5267
|
+
|
|
5129
5268
|
const approverId =
|
|
5130
|
-
|
|
5269
|
+
projectManagerRow?.managerCollaboratorId ??
|
|
5270
|
+
current.approverCollaboratorId ??
|
|
5271
|
+
collaborator.supervisorId ??
|
|
5272
|
+
null;
|
|
5131
5273
|
|
|
5132
5274
|
if (!approverId) {
|
|
5133
5275
|
throw new BadRequestException(
|
|
@@ -5360,7 +5502,8 @@ export class OperationsService {
|
|
|
5360
5502
|
endTime: string | null;
|
|
5361
5503
|
breakMinutes: number | null;
|
|
5362
5504
|
}>(
|
|
5363
|
-
`SELECT
|
|
5505
|
+
`SELECT DISTINCT ON (collaborator_id, weekday)
|
|
5506
|
+
collaborator_id AS "collaboratorId",
|
|
5364
5507
|
weekday,
|
|
5365
5508
|
is_working_day AS "isWorkingDay",
|
|
5366
5509
|
start_time AS "startTime",
|
|
@@ -5368,7 +5511,7 @@ export class OperationsService {
|
|
|
5368
5511
|
break_minutes AS "breakMinutes"
|
|
5369
5512
|
FROM operations_collaborator_schedule_day
|
|
5370
5513
|
WHERE collaborator_id = ANY($1::int[])
|
|
5371
|
-
ORDER BY id
|
|
5514
|
+
ORDER BY collaborator_id, weekday, id DESC`,
|
|
5372
5515
|
[this.uniqueNumbers(requests.map((item) => item.collaboratorId))]
|
|
5373
5516
|
);
|
|
5374
5517
|
|
|
@@ -5821,21 +5964,51 @@ export class OperationsService {
|
|
|
5821
5964
|
private async getCollaboratorByUserId(userId: number) {
|
|
5822
5965
|
return this.querySingle<{
|
|
5823
5966
|
id: number;
|
|
5967
|
+
userId: number | null;
|
|
5968
|
+
personId: number | null;
|
|
5824
5969
|
displayName: string;
|
|
5825
5970
|
supervisorId: number | null;
|
|
5826
5971
|
supervisorName: string | null;
|
|
5972
|
+
activeAssignments: number;
|
|
5827
5973
|
}>(
|
|
5828
|
-
`
|
|
5974
|
+
`WITH linked_collaborator AS (
|
|
5975
|
+
SELECT person_id
|
|
5976
|
+
FROM operations_collaborator
|
|
5977
|
+
WHERE user_id = $1
|
|
5978
|
+
AND deleted_at IS NULL
|
|
5979
|
+
ORDER BY id DESC
|
|
5980
|
+
LIMIT 1
|
|
5981
|
+
)
|
|
5982
|
+
SELECT c.id,
|
|
5983
|
+
c.user_id AS "userId",
|
|
5984
|
+
c.person_id AS "personId",
|
|
5829
5985
|
COALESCE(NULLIF(c.display_name, ''), person_record.name) AS "displayName",
|
|
5830
5986
|
s.id AS "supervisorId",
|
|
5831
|
-
s.display_name AS "supervisorName"
|
|
5987
|
+
s.display_name AS "supervisorName",
|
|
5988
|
+
COUNT(pa.id) FILTER (
|
|
5989
|
+
WHERE pa.deleted_at IS NULL
|
|
5990
|
+
AND pa.status IN ('planned', 'active')
|
|
5991
|
+
)::int AS "activeAssignments"
|
|
5832
5992
|
FROM operations_collaborator c
|
|
5833
5993
|
LEFT JOIN person person_record
|
|
5834
5994
|
ON person_record.id = c.person_id
|
|
5835
5995
|
LEFT JOIN operations_collaborator s
|
|
5836
5996
|
ON s.id = c.supervisor_collaborator_id
|
|
5837
|
-
|
|
5838
|
-
|
|
5997
|
+
LEFT JOIN operations_project_assignment pa
|
|
5998
|
+
ON pa.collaborator_id = c.id
|
|
5999
|
+
WHERE c.deleted_at IS NULL
|
|
6000
|
+
AND (
|
|
6001
|
+
c.user_id = $1
|
|
6002
|
+
OR (
|
|
6003
|
+
c.person_id IS NOT NULL
|
|
6004
|
+
AND c.person_id = (SELECT person_id FROM linked_collaborator)
|
|
6005
|
+
)
|
|
6006
|
+
)
|
|
6007
|
+
GROUP BY c.id, person_record.id, s.id
|
|
6008
|
+
ORDER BY "activeAssignments" DESC,
|
|
6009
|
+
CASE WHEN c.user_id = $1 THEN 0 ELSE 1 END,
|
|
6010
|
+
c.id ASC
|
|
6011
|
+
LIMIT 1`,
|
|
5839
6012
|
[userId]
|
|
5840
6013
|
);
|
|
5841
6014
|
}
|
|
@@ -7705,23 +7878,26 @@ export class OperationsService {
|
|
|
7705
7878
|
const assignmentIds = entries
|
|
7706
7879
|
.map((entry) => entry.projectAssignmentId)
|
|
7707
7880
|
.filter((value): value is number => typeof value === 'number');
|
|
7881
|
+
const assignmentMap = new Map<
|
|
7882
|
+
number,
|
|
7883
|
+
{ id: number; collaboratorId: number; projectId: number }
|
|
7884
|
+
>();
|
|
7708
7885
|
|
|
7709
7886
|
if (assignmentIds.length) {
|
|
7710
7887
|
const assignments = (await client.$queryRawUnsafe(
|
|
7711
|
-
`SELECT id,
|
|
7888
|
+
`SELECT id,
|
|
7889
|
+
collaborator_id AS "collaboratorId",
|
|
7890
|
+
project_id AS "projectId"
|
|
7712
7891
|
FROM operations_project_assignment
|
|
7713
7892
|
WHERE id = ANY($1::int[])
|
|
7714
7893
|
AND deleted_at IS NULL`,
|
|
7715
7894
|
assignmentIds
|
|
7716
|
-
)) as { id: number; collaboratorId: number }[];
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
assignment.collaboratorId,
|
|
7721
|
-
])
|
|
7722
|
-
);
|
|
7895
|
+
)) as { id: number; collaboratorId: number; projectId: number }[];
|
|
7896
|
+
assignments.forEach((assignment) => {
|
|
7897
|
+
assignmentMap.set(assignment.id, assignment);
|
|
7898
|
+
});
|
|
7723
7899
|
for (const assignmentId of assignmentIds) {
|
|
7724
|
-
if (assignmentMap.get(assignmentId) !== collaboratorId) {
|
|
7900
|
+
if (assignmentMap.get(assignmentId)?.collaboratorId !== collaboratorId) {
|
|
7725
7901
|
throw new ForbiddenException(
|
|
7726
7902
|
'Timesheet entries must use assignments owned by the target collaborator.'
|
|
7727
7903
|
);
|
|
@@ -7735,14 +7911,17 @@ export class OperationsService {
|
|
|
7735
7911
|
const resolvedTask = entry.taskId
|
|
7736
7912
|
? await this.getOwnedTaskRecord(client, collaboratorId, entry.taskId)
|
|
7737
7913
|
: null;
|
|
7914
|
+
const selectedAssignment = entry.projectAssignmentId
|
|
7915
|
+
? assignmentMap.get(entry.projectAssignmentId) ?? null
|
|
7916
|
+
: null;
|
|
7738
7917
|
|
|
7739
7918
|
if (
|
|
7740
7919
|
resolvedTask &&
|
|
7741
|
-
|
|
7742
|
-
resolvedTask.
|
|
7920
|
+
selectedAssignment &&
|
|
7921
|
+
resolvedTask.projectId !== selectedAssignment.projectId
|
|
7743
7922
|
) {
|
|
7744
7923
|
throw new BadRequestException(
|
|
7745
|
-
'The selected task does not belong to the chosen project
|
|
7924
|
+
'The selected task does not belong to the chosen project.'
|
|
7746
7925
|
);
|
|
7747
7926
|
}
|
|
7748
7927
|
|
|
@@ -7816,6 +7995,37 @@ export class OperationsService {
|
|
|
7816
7995
|
);
|
|
7817
7996
|
}
|
|
7818
7997
|
|
|
7998
|
+
private async cleanupEmptyEditableTimesheet(client: any, timesheetId: number) {
|
|
7999
|
+
const candidate = (await client.$queryRawUnsafe(
|
|
8000
|
+
`SELECT t.id
|
|
8001
|
+
FROM operations_timesheet t
|
|
8002
|
+
WHERE t.id = $1
|
|
8003
|
+
AND t.deleted_at IS NULL
|
|
8004
|
+
AND t.status IN ('draft', 'rejected')
|
|
8005
|
+
AND NOT EXISTS (
|
|
8006
|
+
SELECT 1
|
|
8007
|
+
FROM operations_timesheet_entry e
|
|
8008
|
+
WHERE e.timesheet_id = t.id
|
|
8009
|
+
AND e.deleted_at IS NULL
|
|
8010
|
+
)
|
|
8011
|
+
LIMIT 1`,
|
|
8012
|
+
timesheetId
|
|
8013
|
+
)) as Array<{ id: number }>;
|
|
8014
|
+
|
|
8015
|
+
if (!candidate[0]?.id) {
|
|
8016
|
+
return;
|
|
8017
|
+
}
|
|
8018
|
+
|
|
8019
|
+
await client.$executeRawUnsafe(
|
|
8020
|
+
`UPDATE operations_timesheet
|
|
8021
|
+
SET deleted_at = NOW(),
|
|
8022
|
+
updated_at = NOW()
|
|
8023
|
+
WHERE id = $1
|
|
8024
|
+
AND deleted_at IS NULL`,
|
|
8025
|
+
timesheetId
|
|
8026
|
+
);
|
|
8027
|
+
}
|
|
8028
|
+
|
|
7819
8029
|
private async upsertApproval(
|
|
7820
8030
|
client: any,
|
|
7821
8031
|
input: {
|