@hed-hog/operations 0.0.304 → 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-projects.controller.d.ts +15 -0
- package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
- package/dist/controllers/operations-tasks.controller.d.ts +41 -10
- package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
- package/dist/controllers/operations-tasks.controller.js +11 -0
- package/dist/controllers/operations-tasks.controller.js.map +1 -1
- 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/create-task.dto.d.ts +7 -1
- package/dist/dto/create-task.dto.d.ts.map +1 -1
- package/dist/dto/create-task.dto.js +38 -5
- package/dist/dto/create-task.dto.js.map +1 -1
- package/dist/dto/list-tasks.dto.d.ts +1 -1
- package/dist/dto/list-tasks.dto.d.ts.map +1 -1
- package/dist/dto/list-tasks.dto.js +2 -2
- package/dist/dto/list-tasks.dto.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/dto/update-task.dto.d.ts +7 -1
- package/dist/dto/update-task.dto.d.ts.map +1 -1
- package/dist/dto/update-task.dto.js +38 -5
- package/dist/dto/update-task.dto.js.map +1 -1
- package/dist/operations.service.d.ts +90 -12
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +560 -148
- 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 +26 -0
- package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +5 -3
- package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +44 -44
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +168 -213
- package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +256 -256
- package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +7 -7
- package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +306 -306
- package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +247 -247
- package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +3520 -3520
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +1504 -52
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +528 -403
- package/hedhog/frontend/app/_components/section-card.tsx.ejs +25 -18
- package/hedhog/frontend/app/_components/system-user-select-with-create.tsx.ejs +609 -0
- package/hedhog/frontend/app/_components/timesheet-task-create-sheet.tsx.ejs +1 -0
- package/hedhog/frontend/app/_lib/types.ts.ejs +5 -0
- package/hedhog/frontend/app/_lib/utils/format.ts.ejs +7 -7
- package/hedhog/frontend/app/_lib/utils/forms.ts.ejs +48 -1
- package/hedhog/frontend/app/approvals/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +513 -502
- package/hedhog/frontend/app/collaborators/page.tsx.ejs +10 -7
- package/hedhog/frontend/app/contracts/page.tsx.ejs +938 -938
- package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/projects/page.tsx.ejs +360 -133
- 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 +32 -4
- package/hedhog/frontend/messages/pt.json +34 -6
- 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/hedhog/table/operations_project.yaml +9 -0
- package/hedhog/table/operations_task.yaml +43 -4
- package/package.json +6 -6
- package/src/controllers/operations-tasks.controller.ts +11 -0
- 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/create-task.dto.ts +47 -7
- package/src/dto/list-collaborator-types.dto.ts +15 -15
- package/src/dto/list-collaborators.dto.ts +30 -30
- package/src/dto/list-tasks.dto.ts +3 -3
- package/src/dto/update-collaborator-type.dto.ts +4 -3
- package/src/dto/update-collaborator.dto.ts +3 -3
- package/src/dto/update-task.dto.ts +47 -7
- package/src/operations.service.spec.ts +96 -0
- package/src/operations.service.ts +813 -135
|
@@ -107,7 +107,7 @@ const FINANCIAL_TERM_TYPE_VALUES = ['value', 'payment', 'revenue', 'fine', 'othe
|
|
|
107
107
|
const RECURRENCE_VALUES = ['one_time', 'monthly', 'quarterly', 'yearly', 'other'] as const;
|
|
108
108
|
const REVISION_TYPE_VALUES = ['amendment', 'renewal', 'revision', 'addendum', 'other'] as const;
|
|
109
109
|
const REVISION_STATUS_VALUES = ['draft', 'active', 'completed', 'cancelled'] as const;
|
|
110
|
-
const TASK_STATUS_VALUES = ['
|
|
110
|
+
const TASK_STATUS_VALUES = ['todo', 'doing', 'review', 'done'] as const;
|
|
111
111
|
|
|
112
112
|
type ApprovalAction = 'approve' | 'reject';
|
|
113
113
|
type ApprovalTargetType =
|
|
@@ -548,6 +548,7 @@ type ProjectPayload = {
|
|
|
548
548
|
contractId?: number | null;
|
|
549
549
|
contractTemplateId?: number | null;
|
|
550
550
|
managerCollaboratorId?: number | null;
|
|
551
|
+
clientPersonId?: number | null;
|
|
551
552
|
code: string;
|
|
552
553
|
name: string;
|
|
553
554
|
clientName?: string | null;
|
|
@@ -606,9 +607,15 @@ type TimesheetEntryPayload = {
|
|
|
606
607
|
type TaskPayload = {
|
|
607
608
|
projectId?: number | null;
|
|
608
609
|
projectAssignmentId?: number | null;
|
|
610
|
+
assigneeCollaboratorId?: number | null;
|
|
609
611
|
name?: string | null;
|
|
610
612
|
description?: string | null;
|
|
613
|
+
priority?: 'low' | 'medium' | 'high';
|
|
611
614
|
status?: (typeof TASK_STATUS_VALUES)[number];
|
|
615
|
+
dueDate?: string | null;
|
|
616
|
+
estimateHours?: number | null;
|
|
617
|
+
position?: number;
|
|
618
|
+
tags?: string | null;
|
|
612
619
|
};
|
|
613
620
|
|
|
614
621
|
type QuickTimesheetEntryPayload = {
|
|
@@ -1743,6 +1750,28 @@ export class OperationsService {
|
|
|
1743
1750
|
data.equityParticipation
|
|
1744
1751
|
);
|
|
1745
1752
|
}
|
|
1753
|
+
|
|
1754
|
+
if (
|
|
1755
|
+
data.compensationAmount !== undefined ||
|
|
1756
|
+
data.contractDescription !== undefined ||
|
|
1757
|
+
data.autoGenerateContractDraft !== undefined ||
|
|
1758
|
+
data.joinedAt !== undefined ||
|
|
1759
|
+
data.weeklyCapacityHours !== undefined ||
|
|
1760
|
+
data.supervisorCollaboratorId !== undefined ||
|
|
1761
|
+
data.collaboratorType !== undefined ||
|
|
1762
|
+
data.collaboratorTypeId !== undefined ||
|
|
1763
|
+
data.collaboratorTypeSlug !== undefined ||
|
|
1764
|
+
data.code !== undefined ||
|
|
1765
|
+
data.personId !== undefined ||
|
|
1766
|
+
data.displayName !== undefined
|
|
1767
|
+
) {
|
|
1768
|
+
await this.syncHiringContractDraft(
|
|
1769
|
+
tx as any,
|
|
1770
|
+
actor.userId,
|
|
1771
|
+
collaboratorId,
|
|
1772
|
+
data
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1746
1775
|
});
|
|
1747
1776
|
|
|
1748
1777
|
return this.getCollaboratorByIdForUser(userId, collaboratorId);
|
|
@@ -2002,8 +2031,8 @@ export class OperationsService {
|
|
|
2002
2031
|
p.progress_percent AS "progressPercent",
|
|
2003
2032
|
p.delivery_model AS "deliveryModel",
|
|
2004
2033
|
p.budget_amount AS "budgetAmount",
|
|
2005
|
-
p.start_date AS "startDate",
|
|
2006
|
-
p.end_date AS "endDate",
|
|
2034
|
+
TO_CHAR(p.start_date, 'YYYY-MM-DD') AS "startDate",
|
|
2035
|
+
TO_CHAR(p.end_date, 'YYYY-MM-DD') AS "endDate",
|
|
2007
2036
|
c.name AS "contractName",
|
|
2008
2037
|
c.status AS "contractStatus",
|
|
2009
2038
|
m.display_name AS "managerName",
|
|
@@ -2036,22 +2065,19 @@ export class OperationsService {
|
|
|
2036
2065
|
const actor = await this.getActorContext(userId);
|
|
2037
2066
|
this.ensureCollaborator(actor);
|
|
2038
2067
|
|
|
2039
|
-
if (!actor.collaboratorId) {
|
|
2040
|
-
throw new BadRequestException('Collaborator context is required.');
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
2068
|
const pagination = this.normalizePaginationParams(paginationParams, {
|
|
2044
2069
|
defaultSortField: 'name',
|
|
2045
2070
|
defaultSortOrder: 'asc',
|
|
2046
2071
|
allowedSortFields: ['name', 'code', 'clientName', 'startDate', 'endDate'],
|
|
2047
2072
|
});
|
|
2048
2073
|
|
|
2049
|
-
const
|
|
2074
|
+
const filter = this.buildIdFilter(actor.visibleProjectIds, 'p.id', actor.isDirector);
|
|
2075
|
+
const params: unknown[] = [...filter.params];
|
|
2050
2076
|
const filters = [
|
|
2051
2077
|
'p.deleted_at IS NULL',
|
|
2052
2078
|
'pa.deleted_at IS NULL',
|
|
2053
|
-
`pa.collaborator_id = $1`,
|
|
2054
2079
|
`pa.status IN ('planned', 'active')`,
|
|
2080
|
+
filter.clause,
|
|
2055
2081
|
];
|
|
2056
2082
|
|
|
2057
2083
|
if (pagination.search) {
|
|
@@ -2105,8 +2131,8 @@ export class OperationsService {
|
|
|
2105
2131
|
MAX(pa.id)::int AS "projectAssignmentId",
|
|
2106
2132
|
MAX(pa.role_label) AS "roleLabel",
|
|
2107
2133
|
p.status,
|
|
2108
|
-
p.start_date AS "startDate",
|
|
2109
|
-
p.end_date AS "endDate"
|
|
2134
|
+
TO_CHAR(p.start_date, 'YYYY-MM-DD') AS "startDate",
|
|
2135
|
+
TO_CHAR(p.end_date, 'YYYY-MM-DD') AS "endDate"
|
|
2110
2136
|
FROM operations_project_assignment pa
|
|
2111
2137
|
JOIN operations_project p
|
|
2112
2138
|
ON p.id = pa.project_id
|
|
@@ -2145,23 +2171,30 @@ export class OperationsService {
|
|
|
2145
2171
|
const actor = await this.getActorContext(userId);
|
|
2146
2172
|
this.ensureCollaborator(actor);
|
|
2147
2173
|
|
|
2148
|
-
if (!actor.collaboratorId) {
|
|
2149
|
-
throw new BadRequestException('Collaborator context is required.');
|
|
2150
|
-
}
|
|
2151
|
-
|
|
2152
2174
|
const pagination = this.normalizePaginationParams(paginationParams, {
|
|
2153
2175
|
defaultSortField: 'name',
|
|
2154
2176
|
defaultSortOrder: 'asc',
|
|
2155
2177
|
allowedSortFields: ['name', 'projectName', 'status', 'createdAt'],
|
|
2156
2178
|
});
|
|
2157
2179
|
|
|
2158
|
-
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];
|
|
2159
2186
|
const filters = [
|
|
2160
2187
|
't.deleted_at IS NULL',
|
|
2161
|
-
'pa.deleted_at IS NULL',
|
|
2162
2188
|
'p.deleted_at IS NULL',
|
|
2163
|
-
|
|
2164
|
-
`
|
|
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
|
+
)`,
|
|
2165
2198
|
];
|
|
2166
2199
|
|
|
2167
2200
|
if (pagination.search) {
|
|
@@ -2179,7 +2212,12 @@ export class OperationsService {
|
|
|
2179
2212
|
}
|
|
2180
2213
|
|
|
2181
2214
|
if (paginationParams.projectId) {
|
|
2182
|
-
filters.push(
|
|
2215
|
+
filters.push(
|
|
2216
|
+
`COALESCE(t.project_id, pa.project_id) = ${this.param(
|
|
2217
|
+
params,
|
|
2218
|
+
paginationParams.projectId
|
|
2219
|
+
)}`
|
|
2220
|
+
);
|
|
2183
2221
|
}
|
|
2184
2222
|
|
|
2185
2223
|
if (paginationParams.status) {
|
|
@@ -2190,10 +2228,10 @@ export class OperationsService {
|
|
|
2190
2228
|
const totalRow = await this.querySingle<{ total: string }>(
|
|
2191
2229
|
`SELECT COUNT(*)::text AS total
|
|
2192
2230
|
FROM operations_task t
|
|
2193
|
-
JOIN operations_project_assignment pa
|
|
2231
|
+
LEFT JOIN operations_project_assignment pa
|
|
2194
2232
|
ON pa.id = t.project_assignment_id
|
|
2195
2233
|
JOIN operations_project p
|
|
2196
|
-
ON p.id = pa.project_id
|
|
2234
|
+
ON p.id = COALESCE(t.project_id, pa.project_id)
|
|
2197
2235
|
WHERE ${whereClause}`,
|
|
2198
2236
|
params
|
|
2199
2237
|
);
|
|
@@ -2225,16 +2263,16 @@ export class OperationsService {
|
|
|
2225
2263
|
t.name,
|
|
2226
2264
|
t.description,
|
|
2227
2265
|
t.status,
|
|
2228
|
-
pa.project_id AS "projectId",
|
|
2266
|
+
COALESCE(t.project_id, pa.project_id) AS "projectId",
|
|
2229
2267
|
pa.id AS "projectAssignmentId",
|
|
2230
2268
|
p.name AS "projectName",
|
|
2231
2269
|
p.code AS "projectCode",
|
|
2232
2270
|
t.created_at AS "createdAt"
|
|
2233
2271
|
FROM operations_task t
|
|
2234
|
-
JOIN operations_project_assignment pa
|
|
2272
|
+
LEFT JOIN operations_project_assignment pa
|
|
2235
2273
|
ON pa.id = t.project_assignment_id
|
|
2236
2274
|
JOIN operations_project p
|
|
2237
|
-
ON p.id = pa.project_id
|
|
2275
|
+
ON p.id = COALESCE(t.project_id, pa.project_id)
|
|
2238
2276
|
WHERE ${whereClause}
|
|
2239
2277
|
ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, t.id ASC
|
|
2240
2278
|
LIMIT ${limitPlaceholder}
|
|
@@ -2255,54 +2293,92 @@ export class OperationsService {
|
|
|
2255
2293
|
|
|
2256
2294
|
async createTask(userId: number, data: TaskPayload) {
|
|
2257
2295
|
const actor = await this.getActorContext(userId);
|
|
2258
|
-
|
|
2296
|
+
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
2297
|
+
throw new ForbiddenException(
|
|
2298
|
+
'Operations collaborator access is required.'
|
|
2299
|
+
);
|
|
2300
|
+
}
|
|
2259
2301
|
this.requireFields(data as Record<string, unknown>, ['name']);
|
|
2260
2302
|
|
|
2261
|
-
|
|
2262
|
-
|
|
2303
|
+
let assignmentId: number | null = null;
|
|
2304
|
+
let projectId: number | null = null;
|
|
2305
|
+
|
|
2306
|
+
if (data.projectId || data.projectAssignmentId) {
|
|
2307
|
+
const assignment = await this.resolveProjectAssignmentForActor(
|
|
2308
|
+
this.prisma,
|
|
2309
|
+
actor,
|
|
2310
|
+
{
|
|
2311
|
+
projectId: data.projectId ?? null,
|
|
2312
|
+
projectAssignmentId: data.projectAssignmentId ?? null,
|
|
2313
|
+
}
|
|
2314
|
+
);
|
|
2315
|
+
await this.assertProjectAccess(actor, assignment.projectId);
|
|
2316
|
+
assignmentId = assignment.id;
|
|
2317
|
+
projectId = assignment.projectId;
|
|
2318
|
+
} else if (data.projectId) {
|
|
2319
|
+
projectId = data.projectId;
|
|
2320
|
+
await this.assertProjectAccess(actor, projectId);
|
|
2321
|
+
} else {
|
|
2322
|
+
throw new BadRequestException('Either projectId or projectAssignmentId is required.');
|
|
2263
2323
|
}
|
|
2264
2324
|
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
{
|
|
2269
|
-
projectId: data.projectId ?? null,
|
|
2270
|
-
projectAssignmentId: data.projectAssignmentId ?? null,
|
|
2271
|
-
}
|
|
2272
|
-
);
|
|
2273
|
-
await this.assertProjectAccess(actor, assignment.projectId);
|
|
2325
|
+
if (!projectId) {
|
|
2326
|
+
projectId = data.projectId ?? null;
|
|
2327
|
+
}
|
|
2274
2328
|
|
|
2275
2329
|
const name = this.normalizeOptionalText(data.name);
|
|
2276
2330
|
if (!name) {
|
|
2277
2331
|
throw new BadRequestException('Field "name" is required.');
|
|
2278
2332
|
}
|
|
2279
2333
|
|
|
2334
|
+
const maxPositionRow = await this.querySingle<{ max_pos: number | null }>(
|
|
2335
|
+
`SELECT MAX(position) AS max_pos
|
|
2336
|
+
FROM operations_task
|
|
2337
|
+
WHERE project_id = $1
|
|
2338
|
+
AND status = $2::operations_task_status_574c143dbe_enum
|
|
2339
|
+
AND deleted_at IS NULL`,
|
|
2340
|
+
[projectId, data.status ?? 'todo']
|
|
2341
|
+
);
|
|
2342
|
+
const nextPosition = ((maxPositionRow?.max_pos ?? -1) as number) + 1;
|
|
2343
|
+
|
|
2280
2344
|
const created = await this.querySingle<{ id: number }>(
|
|
2281
2345
|
`INSERT INTO operations_task (
|
|
2346
|
+
project_id,
|
|
2282
2347
|
project_assignment_id,
|
|
2348
|
+
assignee_collaborator_id,
|
|
2283
2349
|
name,
|
|
2284
2350
|
description,
|
|
2351
|
+
priority,
|
|
2285
2352
|
status,
|
|
2353
|
+
due_date,
|
|
2354
|
+
estimate_hours,
|
|
2355
|
+
position,
|
|
2356
|
+
tags,
|
|
2286
2357
|
created_at,
|
|
2287
2358
|
updated_at
|
|
2288
2359
|
) VALUES (
|
|
2289
|
-
$1,
|
|
2290
|
-
$
|
|
2291
|
-
$
|
|
2292
|
-
$
|
|
2293
|
-
NOW(),
|
|
2294
|
-
NOW()
|
|
2360
|
+
$1, $2, $3, $4, $5,
|
|
2361
|
+
$6::operations_task_priority_394ab327eb_enum,
|
|
2362
|
+
$7::operations_task_status_574c143dbe_enum,
|
|
2363
|
+
$8::date, $9::decimal, $10, $11, NOW(), NOW()
|
|
2295
2364
|
)
|
|
2296
2365
|
RETURNING id`,
|
|
2297
2366
|
[
|
|
2298
|
-
|
|
2367
|
+
projectId,
|
|
2368
|
+
assignmentId,
|
|
2369
|
+
data.assigneeCollaboratorId ?? null,
|
|
2299
2370
|
name,
|
|
2300
2371
|
this.normalizeOptionalText(data.description),
|
|
2301
|
-
data.
|
|
2372
|
+
data.priority ?? 'medium',
|
|
2373
|
+
data.status ?? 'todo',
|
|
2374
|
+
data.dueDate ?? null,
|
|
2375
|
+
data.estimateHours ?? null,
|
|
2376
|
+
data.position ?? nextPosition,
|
|
2377
|
+
data.tags ?? null,
|
|
2302
2378
|
]
|
|
2303
2379
|
);
|
|
2304
2380
|
|
|
2305
|
-
return this.
|
|
2381
|
+
return this.getProjectBoardTask(created?.id ?? 0);
|
|
2306
2382
|
}
|
|
2307
2383
|
|
|
2308
2384
|
async updateTask(
|
|
@@ -2311,15 +2387,15 @@ export class OperationsService {
|
|
|
2311
2387
|
data: Partial<TaskPayload>
|
|
2312
2388
|
) {
|
|
2313
2389
|
const actor = await this.getActorContext(userId);
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2390
|
+
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
2391
|
+
throw new ForbiddenException(
|
|
2392
|
+
'Operations collaborator access is required.'
|
|
2393
|
+
);
|
|
2318
2394
|
}
|
|
2319
2395
|
|
|
2320
|
-
const current = await this.
|
|
2396
|
+
const current = await this.getTaskRecordForActor(
|
|
2321
2397
|
this.prisma,
|
|
2322
|
-
actor
|
|
2398
|
+
actor,
|
|
2323
2399
|
taskId
|
|
2324
2400
|
);
|
|
2325
2401
|
await this.assertProjectAccess(actor, current.projectId);
|
|
@@ -2334,56 +2410,72 @@ export class OperationsService {
|
|
|
2334
2410
|
}
|
|
2335
2411
|
|
|
2336
2412
|
await this.prisma.$transaction(async (tx) => {
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
? await this.resolveOwnedProjectAssignment(
|
|
2340
|
-
tx as any,
|
|
2341
|
-
actor.collaboratorId as number,
|
|
2342
|
-
{
|
|
2343
|
-
projectId: data.projectId ?? null,
|
|
2344
|
-
projectAssignmentId: data.projectAssignmentId ?? null,
|
|
2345
|
-
}
|
|
2346
|
-
)
|
|
2347
|
-
: {
|
|
2348
|
-
id: current.projectAssignmentId,
|
|
2349
|
-
projectId: current.projectId,
|
|
2350
|
-
};
|
|
2413
|
+
let nextAssignmentId = current.projectAssignmentId;
|
|
2414
|
+
let nextProjectId = current.projectId;
|
|
2351
2415
|
|
|
2352
|
-
|
|
2416
|
+
if (data.projectId !== undefined || data.projectAssignmentId !== undefined) {
|
|
2417
|
+
const nextAssignment = await this.resolveProjectAssignmentForActor(
|
|
2418
|
+
tx as any,
|
|
2419
|
+
actor,
|
|
2420
|
+
{
|
|
2421
|
+
projectId: data.projectId ?? null,
|
|
2422
|
+
projectAssignmentId: data.projectAssignmentId ?? null,
|
|
2423
|
+
}
|
|
2424
|
+
);
|
|
2425
|
+
await this.assertProjectAccess(actor, nextAssignment.projectId);
|
|
2426
|
+
nextAssignmentId = nextAssignment.id;
|
|
2427
|
+
nextProjectId = nextAssignment.projectId;
|
|
2428
|
+
}
|
|
2353
2429
|
|
|
2354
2430
|
await (tx as any).$executeRawUnsafe(
|
|
2355
2431
|
`UPDATE operations_task
|
|
2356
|
-
SET
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2432
|
+
SET project_id = $1,
|
|
2433
|
+
project_assignment_id = $2,
|
|
2434
|
+
assignee_collaborator_id = $3,
|
|
2435
|
+
name = $4,
|
|
2436
|
+
description = $5,
|
|
2437
|
+
priority = $6::operations_task_priority_394ab327eb_enum,
|
|
2438
|
+
status = $7::operations_task_status_574c143dbe_enum,
|
|
2439
|
+
due_date = $8::date,
|
|
2440
|
+
estimate_hours = $9::decimal,
|
|
2441
|
+
position = $10,
|
|
2442
|
+
tags = $11,
|
|
2360
2443
|
updated_at = NOW()
|
|
2361
|
-
WHERE id = $
|
|
2444
|
+
WHERE id = $12
|
|
2362
2445
|
AND deleted_at IS NULL`,
|
|
2363
|
-
|
|
2446
|
+
nextProjectId,
|
|
2447
|
+
nextAssignmentId,
|
|
2448
|
+
data.assigneeCollaboratorId !== undefined
|
|
2449
|
+
? (data.assigneeCollaboratorId ?? null)
|
|
2450
|
+
: current.assigneeCollaboratorId,
|
|
2364
2451
|
nextName,
|
|
2365
2452
|
data.description !== undefined
|
|
2366
2453
|
? this.normalizeOptionalText(data.description)
|
|
2367
2454
|
: (current.description ?? null),
|
|
2455
|
+
data.priority ?? current.priority,
|
|
2368
2456
|
data.status ?? current.status,
|
|
2457
|
+
data.dueDate !== undefined ? (data.dueDate ?? null) : current.dueDate,
|
|
2458
|
+
data.estimateHours !== undefined ? (data.estimateHours ?? null) : current.estimateHours,
|
|
2459
|
+
data.position !== undefined ? data.position : current.position,
|
|
2460
|
+
data.tags !== undefined ? (data.tags ?? null) : current.tags,
|
|
2369
2461
|
taskId
|
|
2370
2462
|
);
|
|
2371
2463
|
});
|
|
2372
2464
|
|
|
2373
|
-
return this.
|
|
2465
|
+
return this.getProjectBoardTask(taskId);
|
|
2374
2466
|
}
|
|
2375
2467
|
|
|
2376
2468
|
async removeTask(userId: number, taskId: number) {
|
|
2377
2469
|
const actor = await this.getActorContext(userId);
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2470
|
+
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
2471
|
+
throw new ForbiddenException(
|
|
2472
|
+
'Operations collaborator access is required.'
|
|
2473
|
+
);
|
|
2382
2474
|
}
|
|
2383
2475
|
|
|
2384
|
-
const current = await this.
|
|
2476
|
+
const current = await this.getTaskRecordForActor(
|
|
2385
2477
|
this.prisma,
|
|
2386
|
-
actor
|
|
2478
|
+
actor,
|
|
2387
2479
|
taskId
|
|
2388
2480
|
);
|
|
2389
2481
|
await this.assertProjectAccess(actor, current.projectId);
|
|
@@ -2392,7 +2484,6 @@ export class OperationsService {
|
|
|
2392
2484
|
await (tx as any).$executeRawUnsafe(
|
|
2393
2485
|
`UPDATE operations_task
|
|
2394
2486
|
SET deleted_at = COALESCE(deleted_at, NOW()),
|
|
2395
|
-
status = 'archived',
|
|
2396
2487
|
updated_at = NOW()
|
|
2397
2488
|
WHERE id = $1
|
|
2398
2489
|
AND deleted_at IS NULL`,
|
|
@@ -2617,9 +2708,9 @@ export class OperationsService {
|
|
|
2617
2708
|
? await this.getOwnedTaskRecord(tx as any, actor.collaboratorId as number, data.taskId)
|
|
2618
2709
|
: null;
|
|
2619
2710
|
|
|
2620
|
-
if (resolvedTask && resolvedTask.
|
|
2711
|
+
if (resolvedTask && resolvedTask.projectId !== assignment.projectId) {
|
|
2621
2712
|
throw new BadRequestException(
|
|
2622
|
-
'The selected task does not belong to the chosen project
|
|
2713
|
+
'The selected task does not belong to the chosen project.'
|
|
2623
2714
|
);
|
|
2624
2715
|
}
|
|
2625
2716
|
|
|
@@ -2678,6 +2769,116 @@ export class OperationsService {
|
|
|
2678
2769
|
return this.getTimesheetEntryByIdForActor(actor, createdEntryId);
|
|
2679
2770
|
}
|
|
2680
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
|
+
|
|
2681
2882
|
async removeTimesheetEntry(userId: number, entryId: number) {
|
|
2682
2883
|
const actor = await this.getActorContext(userId);
|
|
2683
2884
|
this.ensureCollaborator(actor);
|
|
@@ -2711,6 +2912,7 @@ export class OperationsService {
|
|
|
2711
2912
|
);
|
|
2712
2913
|
|
|
2713
2914
|
await this.refreshTimesheetTotal(tx as any, entry.timesheetId);
|
|
2915
|
+
await this.cleanupEmptyEditableTimesheet(tx as any, entry.timesheetId);
|
|
2714
2916
|
});
|
|
2715
2917
|
|
|
2716
2918
|
return { success: true };
|
|
@@ -2732,6 +2934,7 @@ export class OperationsService {
|
|
|
2732
2934
|
`INSERT INTO operations_project (
|
|
2733
2935
|
contract_id,
|
|
2734
2936
|
manager_collaborator_id,
|
|
2937
|
+
client_person_id,
|
|
2735
2938
|
code,
|
|
2736
2939
|
name,
|
|
2737
2940
|
client_name,
|
|
@@ -2745,15 +2948,16 @@ export class OperationsService {
|
|
|
2745
2948
|
created_at,
|
|
2746
2949
|
updated_at
|
|
2747
2950
|
) VALUES (
|
|
2748
|
-
$1, $2, $3, $4, $5, $6,
|
|
2749
|
-
$
|
|
2750
|
-
$
|
|
2751
|
-
$
|
|
2752
|
-
$
|
|
2951
|
+
$1, $2, $3, $4, $5, $6, $7,
|
|
2952
|
+
$8::operations_project_status_965e8d4b2d_enum,
|
|
2953
|
+
$9,
|
|
2954
|
+
$10::operations_project_delivery_model_75ee11b3b7_enum,
|
|
2955
|
+
$11, $12::date, $13::date, NOW(), NOW()
|
|
2753
2956
|
)
|
|
2754
2957
|
RETURNING id`,
|
|
2755
2958
|
data.contractId ?? null,
|
|
2756
2959
|
data.managerCollaboratorId ?? null,
|
|
2960
|
+
data.clientPersonId ?? null,
|
|
2757
2961
|
data.code,
|
|
2758
2962
|
data.name,
|
|
2759
2963
|
data.clientName ?? null,
|
|
@@ -2823,6 +3027,7 @@ export class OperationsService {
|
|
|
2823
3027
|
const params: unknown[] = [];
|
|
2824
3028
|
this.pushUpdate(updates, params, 'contract_id', data.contractId);
|
|
2825
3029
|
this.pushUpdate(updates, params, 'manager_collaborator_id', data.managerCollaboratorId);
|
|
3030
|
+
this.pushUpdate(updates, params, 'client_person_id', data.clientPersonId);
|
|
2826
3031
|
this.pushUpdate(updates, params, 'code', data.code);
|
|
2827
3032
|
this.pushUpdate(updates, params, 'name', data.name);
|
|
2828
3033
|
this.pushUpdate(updates, params, 'client_name', data.clientName);
|
|
@@ -2839,7 +3044,7 @@ export class OperationsService {
|
|
|
2839
3044
|
updates,
|
|
2840
3045
|
params,
|
|
2841
3046
|
'delivery_model',
|
|
2842
|
-
data.deliveryModel,
|
|
3047
|
+
(data.deliveryModel as string | null | undefined) === '' ? null : data.deliveryModel,
|
|
2843
3048
|
'operations_project_delivery_model_75ee11b3b7_enum'
|
|
2844
3049
|
);
|
|
2845
3050
|
this.pushUpdate(updates, params, 'budget_amount', data.budgetAmount);
|
|
@@ -2869,7 +3074,7 @@ export class OperationsService {
|
|
|
2869
3074
|
const nextContractId =
|
|
2870
3075
|
data.contractId !== undefined
|
|
2871
3076
|
? data.contractId
|
|
2872
|
-
: (currentProject.
|
|
3077
|
+
: (currentProject.relatedContract?.id ?? null);
|
|
2873
3078
|
const shouldGenerateDraft =
|
|
2874
3079
|
!nextContractId && data.autoGenerateContractDraft === true;
|
|
2875
3080
|
|
|
@@ -2922,6 +3127,39 @@ export class OperationsService {
|
|
|
2922
3127
|
contractId,
|
|
2923
3128
|
projectId
|
|
2924
3129
|
);
|
|
3130
|
+
} else if (
|
|
3131
|
+
nextContractId &&
|
|
3132
|
+
(data.monthlyHourCap !== undefined || data.billingModel !== undefined)
|
|
3133
|
+
) {
|
|
3134
|
+
const contractUpdates: string[] = [];
|
|
3135
|
+
const contractParams: unknown[] = [];
|
|
3136
|
+
|
|
3137
|
+
this.pushUpdate(
|
|
3138
|
+
contractUpdates,
|
|
3139
|
+
contractParams,
|
|
3140
|
+
'monthly_hour_cap',
|
|
3141
|
+
data.monthlyHourCap
|
|
3142
|
+
);
|
|
3143
|
+
this.pushUpdate(
|
|
3144
|
+
contractUpdates,
|
|
3145
|
+
contractParams,
|
|
3146
|
+
'billing_model',
|
|
3147
|
+
(data.billingModel as string | null | undefined) === ''
|
|
3148
|
+
? null
|
|
3149
|
+
: data.billingModel,
|
|
3150
|
+
'operations_contract_billing_model_409dc7fea2_enum'
|
|
3151
|
+
);
|
|
3152
|
+
|
|
3153
|
+
if (contractUpdates.length) {
|
|
3154
|
+
contractParams.push(nextContractId);
|
|
3155
|
+
await (tx as any).$executeRawUnsafe(
|
|
3156
|
+
`UPDATE operations_contract
|
|
3157
|
+
SET ${contractUpdates.join(', ')},
|
|
3158
|
+
updated_at = NOW()
|
|
3159
|
+
WHERE id = $${contractParams.length}`,
|
|
3160
|
+
...contractParams
|
|
3161
|
+
);
|
|
3162
|
+
}
|
|
2925
3163
|
}
|
|
2926
3164
|
});
|
|
2927
3165
|
|
|
@@ -3011,7 +3249,11 @@ export class OperationsService {
|
|
|
3011
3249
|
updated_at
|
|
3012
3250
|
) VALUES (
|
|
3013
3251
|
$1, $2, $3, $4,
|
|
3014
|
-
$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,
|
|
3015
3257
|
NOW(), NOW()
|
|
3016
3258
|
)
|
|
3017
3259
|
RETURNING id`,
|
|
@@ -3100,12 +3342,12 @@ export class OperationsService {
|
|
|
3100
3342
|
code = $2,
|
|
3101
3343
|
name = $3,
|
|
3102
3344
|
description = $4,
|
|
3103
|
-
contract_category = $5,
|
|
3104
|
-
contract_type = $6,
|
|
3105
|
-
billing_model = $7,
|
|
3106
|
-
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,
|
|
3107
3349
|
is_active = $9,
|
|
3108
|
-
status = $10,
|
|
3350
|
+
status = $10::text::operations_contract_template_status_c9d2e90231_enum,
|
|
3109
3351
|
content_html = $11,
|
|
3110
3352
|
updated_at = NOW()
|
|
3111
3353
|
WHERE id = $12`,
|
|
@@ -5008,8 +5250,26 @@ export class OperationsService {
|
|
|
5008
5250
|
}
|
|
5009
5251
|
|
|
5010
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
|
+
|
|
5011
5268
|
const approverId =
|
|
5012
|
-
|
|
5269
|
+
projectManagerRow?.managerCollaboratorId ??
|
|
5270
|
+
current.approverCollaboratorId ??
|
|
5271
|
+
collaborator.supervisorId ??
|
|
5272
|
+
null;
|
|
5013
5273
|
|
|
5014
5274
|
if (!approverId) {
|
|
5015
5275
|
throw new BadRequestException(
|
|
@@ -5242,7 +5502,8 @@ export class OperationsService {
|
|
|
5242
5502
|
endTime: string | null;
|
|
5243
5503
|
breakMinutes: number | null;
|
|
5244
5504
|
}>(
|
|
5245
|
-
`SELECT
|
|
5505
|
+
`SELECT DISTINCT ON (collaborator_id, weekday)
|
|
5506
|
+
collaborator_id AS "collaboratorId",
|
|
5246
5507
|
weekday,
|
|
5247
5508
|
is_working_day AS "isWorkingDay",
|
|
5248
5509
|
start_time AS "startTime",
|
|
@@ -5250,7 +5511,7 @@ export class OperationsService {
|
|
|
5250
5511
|
break_minutes AS "breakMinutes"
|
|
5251
5512
|
FROM operations_collaborator_schedule_day
|
|
5252
5513
|
WHERE collaborator_id = ANY($1::int[])
|
|
5253
|
-
ORDER BY id
|
|
5514
|
+
ORDER BY collaborator_id, weekday, id DESC`,
|
|
5254
5515
|
[this.uniqueNumbers(requests.map((item) => item.collaboratorId))]
|
|
5255
5516
|
);
|
|
5256
5517
|
|
|
@@ -5703,21 +5964,51 @@ export class OperationsService {
|
|
|
5703
5964
|
private async getCollaboratorByUserId(userId: number) {
|
|
5704
5965
|
return this.querySingle<{
|
|
5705
5966
|
id: number;
|
|
5967
|
+
userId: number | null;
|
|
5968
|
+
personId: number | null;
|
|
5706
5969
|
displayName: string;
|
|
5707
5970
|
supervisorId: number | null;
|
|
5708
5971
|
supervisorName: string | null;
|
|
5972
|
+
activeAssignments: number;
|
|
5709
5973
|
}>(
|
|
5710
|
-
`
|
|
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",
|
|
5711
5985
|
COALESCE(NULLIF(c.display_name, ''), person_record.name) AS "displayName",
|
|
5712
5986
|
s.id AS "supervisorId",
|
|
5713
|
-
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"
|
|
5714
5992
|
FROM operations_collaborator c
|
|
5715
5993
|
LEFT JOIN person person_record
|
|
5716
5994
|
ON person_record.id = c.person_id
|
|
5717
5995
|
LEFT JOIN operations_collaborator s
|
|
5718
5996
|
ON s.id = c.supervisor_collaborator_id
|
|
5719
|
-
|
|
5720
|
-
|
|
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`,
|
|
5721
6012
|
[userId]
|
|
5722
6013
|
);
|
|
5723
6014
|
}
|
|
@@ -6608,6 +6899,8 @@ export class OperationsService {
|
|
|
6608
6899
|
id: number;
|
|
6609
6900
|
contractId: number | null;
|
|
6610
6901
|
managerCollaboratorId: number | null;
|
|
6902
|
+
clientPersonId: number | null;
|
|
6903
|
+
clientAvatarId: number | null;
|
|
6611
6904
|
code: string;
|
|
6612
6905
|
name: string;
|
|
6613
6906
|
clientName: string | null;
|
|
@@ -6629,6 +6922,8 @@ export class OperationsService {
|
|
|
6629
6922
|
`SELECT p.id,
|
|
6630
6923
|
p.contract_id AS "contractId",
|
|
6631
6924
|
p.manager_collaborator_id AS "managerCollaboratorId",
|
|
6925
|
+
p.client_person_id AS "clientPersonId",
|
|
6926
|
+
client_person.avatar_id AS "clientAvatarId",
|
|
6632
6927
|
p.code,
|
|
6633
6928
|
p.name,
|
|
6634
6929
|
p.client_name AS "clientName",
|
|
@@ -6637,8 +6932,8 @@ export class OperationsService {
|
|
|
6637
6932
|
p.progress_percent AS "progressPercent",
|
|
6638
6933
|
p.delivery_model AS "deliveryModel",
|
|
6639
6934
|
p.budget_amount AS "budgetAmount",
|
|
6640
|
-
p.start_date AS "startDate",
|
|
6641
|
-
p.end_date AS "endDate",
|
|
6935
|
+
TO_CHAR(p.start_date, 'YYYY-MM-DD') AS "startDate",
|
|
6936
|
+
TO_CHAR(p.end_date, 'YYYY-MM-DD') AS "endDate",
|
|
6642
6937
|
c.name AS "contractName",
|
|
6643
6938
|
c.status AS "contractStatus",
|
|
6644
6939
|
c.contract_category AS "contractCategory",
|
|
@@ -6649,6 +6944,7 @@ export class OperationsService {
|
|
|
6649
6944
|
FROM operations_project p
|
|
6650
6945
|
LEFT JOIN operations_contract c ON c.id = p.contract_id
|
|
6651
6946
|
LEFT JOIN operations_collaborator m ON m.id = p.manager_collaborator_id
|
|
6947
|
+
LEFT JOIN person client_person ON client_person.id = p.client_person_id
|
|
6652
6948
|
LEFT JOIN operations_project_assignment pa
|
|
6653
6949
|
ON pa.project_id = p.id
|
|
6654
6950
|
AND pa.deleted_at IS NULL
|
|
@@ -6664,7 +6960,7 @@ export class OperationsService {
|
|
|
6664
6960
|
) project_role_locale ON TRUE
|
|
6665
6961
|
WHERE p.id = $1
|
|
6666
6962
|
AND p.deleted_at IS NULL
|
|
6667
|
-
GROUP BY p.id, c.id, m.id`,
|
|
6963
|
+
GROUP BY p.id, c.id, m.id, client_person.id`,
|
|
6668
6964
|
[projectId, actorCollaboratorId ?? null]
|
|
6669
6965
|
);
|
|
6670
6966
|
|
|
@@ -6677,6 +6973,9 @@ export class OperationsService {
|
|
|
6677
6973
|
this.queryRows<{
|
|
6678
6974
|
id: number;
|
|
6679
6975
|
collaboratorId: number;
|
|
6976
|
+
userId: number | null;
|
|
6977
|
+
personAvatarId: number | null;
|
|
6978
|
+
userPhotoId: number | null;
|
|
6680
6979
|
collaboratorName: string;
|
|
6681
6980
|
projectRoleId: number | null;
|
|
6682
6981
|
roleLabel: string | null;
|
|
@@ -6689,17 +6988,22 @@ export class OperationsService {
|
|
|
6689
6988
|
}>(
|
|
6690
6989
|
`SELECT pa.id,
|
|
6691
6990
|
pa.collaborator_id AS "collaboratorId",
|
|
6991
|
+
c.user_id AS "userId",
|
|
6992
|
+
person_record.avatar_id AS "personAvatarId",
|
|
6993
|
+
collaborator_user.photo_id AS "userPhotoId",
|
|
6692
6994
|
c.display_name AS "collaboratorName",
|
|
6693
6995
|
pa.project_role_id AS "projectRoleId",
|
|
6694
6996
|
COALESCE(project_role_locale.name, pa.role_label) AS "roleLabel",
|
|
6695
6997
|
pa.allocation_percent AS "allocationPercent",
|
|
6696
6998
|
pa.weekly_hours AS "weeklyHours",
|
|
6697
6999
|
pa.is_billable AS "isBillable",
|
|
6698
|
-
pa.start_date AS "startDate",
|
|
6699
|
-
pa.end_date AS "endDate",
|
|
7000
|
+
TO_CHAR(pa.start_date, 'YYYY-MM-DD') AS "startDate",
|
|
7001
|
+
TO_CHAR(pa.end_date, 'YYYY-MM-DD') AS "endDate",
|
|
6700
7002
|
pa.status
|
|
6701
7003
|
FROM operations_project_assignment pa
|
|
6702
7004
|
JOIN operations_collaborator c ON c.id = pa.collaborator_id
|
|
7005
|
+
LEFT JOIN person person_record ON person_record.id = c.person_id
|
|
7006
|
+
LEFT JOIN "user" collaborator_user ON collaborator_user.id = c.user_id
|
|
6703
7007
|
LEFT JOIN operations_project_role project_role
|
|
6704
7008
|
ON project_role.id = pa.project_role_id
|
|
6705
7009
|
AND project_role.deleted_at IS NULL
|
|
@@ -6739,8 +7043,8 @@ export class OperationsService {
|
|
|
6739
7043
|
contract_category AS "contractCategory",
|
|
6740
7044
|
billing_model AS "billingModel",
|
|
6741
7045
|
status,
|
|
6742
|
-
start_date AS "startDate",
|
|
6743
|
-
end_date AS "endDate",
|
|
7046
|
+
TO_CHAR(start_date, 'YYYY-MM-DD') AS "startDate",
|
|
7047
|
+
TO_CHAR(end_date, 'YYYY-MM-DD') AS "endDate",
|
|
6744
7048
|
budget_amount AS "budgetAmount",
|
|
6745
7049
|
monthly_hour_cap AS "monthlyHourCap",
|
|
6746
7050
|
description,
|
|
@@ -6951,7 +7255,8 @@ export class OperationsService {
|
|
|
6951
7255
|
FROM operations_contract c
|
|
6952
7256
|
WHERE c.related_collaborator_id = $1
|
|
6953
7257
|
AND c.deleted_at IS NULL
|
|
6954
|
-
ORDER BY c.
|
|
7258
|
+
ORDER BY CASE WHEN c.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
|
|
7259
|
+
c.created_at DESC`,
|
|
6955
7260
|
[collaboratorId]
|
|
6956
7261
|
),
|
|
6957
7262
|
this.queryRows(
|
|
@@ -7137,6 +7442,69 @@ export class OperationsService {
|
|
|
7137
7442
|
return assignment[0];
|
|
7138
7443
|
}
|
|
7139
7444
|
|
|
7445
|
+
private async resolveProjectAssignmentForActor(
|
|
7446
|
+
client: any,
|
|
7447
|
+
actor: ActorContext,
|
|
7448
|
+
input: {
|
|
7449
|
+
projectId?: number | null;
|
|
7450
|
+
projectAssignmentId?: number | null;
|
|
7451
|
+
}
|
|
7452
|
+
) {
|
|
7453
|
+
if (actor.collaboratorId && !actor.isDirector && !actor.isSupervisor) {
|
|
7454
|
+
return this.resolveOwnedProjectAssignment(
|
|
7455
|
+
client,
|
|
7456
|
+
actor.collaboratorId,
|
|
7457
|
+
input
|
|
7458
|
+
);
|
|
7459
|
+
}
|
|
7460
|
+
|
|
7461
|
+
if (!input.projectId && !input.projectAssignmentId) {
|
|
7462
|
+
throw new BadRequestException(
|
|
7463
|
+
'Either projectId or projectAssignmentId is required.'
|
|
7464
|
+
);
|
|
7465
|
+
}
|
|
7466
|
+
|
|
7467
|
+
const params: unknown[] = [];
|
|
7468
|
+
const filters = ['pa.deleted_at IS NULL', 'p.deleted_at IS NULL'];
|
|
7469
|
+
|
|
7470
|
+
if (input.projectAssignmentId) {
|
|
7471
|
+
filters.push(`pa.id = ${this.param(params, input.projectAssignmentId)}`);
|
|
7472
|
+
}
|
|
7473
|
+
|
|
7474
|
+
if (input.projectId) {
|
|
7475
|
+
filters.push(`pa.project_id = ${this.param(params, input.projectId)}`);
|
|
7476
|
+
}
|
|
7477
|
+
|
|
7478
|
+
const assignment = (await client.$queryRawUnsafe(
|
|
7479
|
+
`SELECT pa.id,
|
|
7480
|
+
pa.project_id AS "projectId",
|
|
7481
|
+
p.name AS "projectName",
|
|
7482
|
+
p.code AS "projectCode",
|
|
7483
|
+
pa.role_label AS "roleLabel"
|
|
7484
|
+
FROM operations_project_assignment pa
|
|
7485
|
+
JOIN operations_project p
|
|
7486
|
+
ON p.id = pa.project_id
|
|
7487
|
+
WHERE ${filters.join(' AND ')}
|
|
7488
|
+
ORDER BY CASE WHEN pa.status = 'active' THEN 0 ELSE 1 END,
|
|
7489
|
+
pa.start_date DESC NULLS LAST,
|
|
7490
|
+
pa.id DESC
|
|
7491
|
+
LIMIT 1`,
|
|
7492
|
+
...params
|
|
7493
|
+
)) as Array<{
|
|
7494
|
+
id: number;
|
|
7495
|
+
projectId: number;
|
|
7496
|
+
projectName: string;
|
|
7497
|
+
projectCode: string | null;
|
|
7498
|
+
roleLabel: string | null;
|
|
7499
|
+
}>;
|
|
7500
|
+
|
|
7501
|
+
if (!assignment[0]) {
|
|
7502
|
+
throw new NotFoundException('Project assignment not found.');
|
|
7503
|
+
}
|
|
7504
|
+
|
|
7505
|
+
return assignment[0];
|
|
7506
|
+
}
|
|
7507
|
+
|
|
7140
7508
|
private async getOwnedTaskRecord(
|
|
7141
7509
|
client: any,
|
|
7142
7510
|
collaboratorId: number,
|
|
@@ -7146,21 +7514,33 @@ export class OperationsService {
|
|
|
7146
7514
|
`SELECT t.id,
|
|
7147
7515
|
t.name,
|
|
7148
7516
|
t.description,
|
|
7517
|
+
t.priority,
|
|
7149
7518
|
t.status,
|
|
7519
|
+
t.due_date AS "dueDate",
|
|
7520
|
+
t.estimate_hours AS "estimateHours",
|
|
7521
|
+
t.position,
|
|
7522
|
+
t.tags,
|
|
7523
|
+
t.assignee_collaborator_id AS "assigneeCollaboratorId",
|
|
7150
7524
|
t.project_assignment_id AS "projectAssignmentId",
|
|
7151
|
-
pa.project_id AS "projectId",
|
|
7525
|
+
COALESCE(t.project_id, pa.project_id) AS "projectId",
|
|
7152
7526
|
p.name AS "projectName",
|
|
7153
7527
|
p.code AS "projectCode"
|
|
7154
7528
|
FROM operations_task t
|
|
7155
|
-
JOIN operations_project_assignment pa
|
|
7529
|
+
LEFT JOIN operations_project_assignment pa
|
|
7156
7530
|
ON pa.id = t.project_assignment_id
|
|
7157
7531
|
AND pa.deleted_at IS NULL
|
|
7158
|
-
JOIN operations_project p
|
|
7159
|
-
ON p.id = pa.project_id
|
|
7532
|
+
LEFT JOIN operations_project p
|
|
7533
|
+
ON p.id = COALESCE(t.project_id, pa.project_id)
|
|
7160
7534
|
AND p.deleted_at IS NULL
|
|
7161
7535
|
WHERE t.id = $1
|
|
7162
7536
|
AND t.deleted_at IS NULL
|
|
7163
|
-
AND
|
|
7537
|
+
AND (
|
|
7538
|
+
pa.collaborator_id = $2
|
|
7539
|
+
OR t.project_id IN (
|
|
7540
|
+
SELECT pa2.project_id FROM operations_project_assignment pa2
|
|
7541
|
+
WHERE pa2.collaborator_id = $2 AND pa2.deleted_at IS NULL
|
|
7542
|
+
)
|
|
7543
|
+
)
|
|
7164
7544
|
LIMIT 1`,
|
|
7165
7545
|
taskId,
|
|
7166
7546
|
collaboratorId
|
|
@@ -7168,8 +7548,14 @@ export class OperationsService {
|
|
|
7168
7548
|
id: number;
|
|
7169
7549
|
name: string;
|
|
7170
7550
|
description: string | null;
|
|
7551
|
+
priority: string;
|
|
7171
7552
|
status: string;
|
|
7172
|
-
|
|
7553
|
+
dueDate: string | null;
|
|
7554
|
+
estimateHours: number | null;
|
|
7555
|
+
position: number;
|
|
7556
|
+
tags: string | null;
|
|
7557
|
+
assigneeCollaboratorId: number | null;
|
|
7558
|
+
projectAssignmentId: number | null;
|
|
7173
7559
|
projectId: number;
|
|
7174
7560
|
projectName: string;
|
|
7175
7561
|
projectCode: string | null;
|
|
@@ -7184,12 +7570,127 @@ export class OperationsService {
|
|
|
7184
7570
|
return task[0];
|
|
7185
7571
|
}
|
|
7186
7572
|
|
|
7187
|
-
private async
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7573
|
+
private async getTaskRecordForActor(
|
|
7574
|
+
client: any,
|
|
7575
|
+
actor: ActorContext,
|
|
7576
|
+
taskId: number
|
|
7577
|
+
) {
|
|
7578
|
+
if (actor.collaboratorId && !actor.isDirector && !actor.isSupervisor) {
|
|
7579
|
+
return this.getOwnedTaskRecord(client, actor.collaboratorId, taskId);
|
|
7580
|
+
}
|
|
7581
|
+
|
|
7582
|
+
const task = await this.getProjectBoardTask(taskId);
|
|
7583
|
+
if (!task?.projectId) {
|
|
7584
|
+
throw new NotFoundException('Task not found.');
|
|
7585
|
+
}
|
|
7586
|
+
|
|
7587
|
+
return task;
|
|
7588
|
+
}
|
|
7589
|
+
|
|
7590
|
+
async listProjectBoardTasks(userId: number, projectId: number) {
|
|
7591
|
+
const actor = await this.getActorContext(userId);
|
|
7592
|
+
this.ensureCollaborator(actor);
|
|
7593
|
+
await this.assertProjectAccess(actor, projectId);
|
|
7594
|
+
|
|
7595
|
+
const rows = await this.queryRows<{
|
|
7596
|
+
id: number;
|
|
7597
|
+
name: string;
|
|
7598
|
+
description: string | null;
|
|
7599
|
+
priority: string;
|
|
7600
|
+
status: string;
|
|
7601
|
+
dueDate: string | null;
|
|
7602
|
+
estimateHours: number | null;
|
|
7603
|
+
position: number;
|
|
7604
|
+
tags: string | null;
|
|
7605
|
+
assigneeCollaboratorId: number | null;
|
|
7606
|
+
assigneeName: string | null;
|
|
7607
|
+
assigneeUserPhotoId: number | null;
|
|
7608
|
+
assigneePersonAvatarId: number | null;
|
|
7609
|
+
projectAssignmentId: number | null;
|
|
7610
|
+
createdAt: string;
|
|
7611
|
+
}>(
|
|
7612
|
+
`SELECT t.id,
|
|
7613
|
+
t.name,
|
|
7614
|
+
t.description,
|
|
7615
|
+
t.priority,
|
|
7616
|
+
t.status,
|
|
7617
|
+
t.due_date AS "dueDate",
|
|
7618
|
+
t.estimate_hours AS "estimateHours",
|
|
7619
|
+
t.position,
|
|
7620
|
+
t.tags,
|
|
7621
|
+
t.assignee_collaborator_id AS "assigneeCollaboratorId",
|
|
7622
|
+
ac.display_name AS "assigneeName",
|
|
7623
|
+
au.photo_id AS "assigneeUserPhotoId",
|
|
7624
|
+
ap.avatar_id AS "assigneePersonAvatarId",
|
|
7625
|
+
t.project_assignment_id AS "projectAssignmentId",
|
|
7626
|
+
t.created_at AS "createdAt"
|
|
7627
|
+
FROM operations_task t
|
|
7628
|
+
LEFT JOIN operations_collaborator ac
|
|
7629
|
+
ON ac.id = t.assignee_collaborator_id AND ac.deleted_at IS NULL
|
|
7630
|
+
LEFT JOIN "user" au
|
|
7631
|
+
ON au.id = ac.user_id
|
|
7632
|
+
LEFT JOIN person ap
|
|
7633
|
+
ON ap.id = ac.person_id
|
|
7634
|
+
WHERE COALESCE(t.project_id, (
|
|
7635
|
+
SELECT pa.project_id FROM operations_project_assignment pa
|
|
7636
|
+
WHERE pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
|
|
7637
|
+
LIMIT 1
|
|
7638
|
+
)) = $1
|
|
7639
|
+
AND t.deleted_at IS NULL
|
|
7640
|
+
ORDER BY t.status ASC, t.position ASC, t.id ASC`,
|
|
7641
|
+
[projectId]
|
|
7642
|
+
);
|
|
7643
|
+
|
|
7644
|
+
return rows;
|
|
7645
|
+
}
|
|
7646
|
+
|
|
7647
|
+
private async getProjectBoardTask(taskId: number) {
|
|
7648
|
+
const rows = await this.queryRows<{
|
|
7649
|
+
id: number;
|
|
7650
|
+
name: string;
|
|
7651
|
+
description: string | null;
|
|
7652
|
+
priority: string;
|
|
7653
|
+
status: string;
|
|
7654
|
+
dueDate: string | null;
|
|
7655
|
+
estimateHours: number | null;
|
|
7656
|
+
position: number;
|
|
7657
|
+
tags: string | null;
|
|
7658
|
+
assigneeCollaboratorId: number | null;
|
|
7659
|
+
assigneeName: string | null;
|
|
7660
|
+
assigneeUserPhotoId: number | null;
|
|
7661
|
+
assigneePersonAvatarId: number | null;
|
|
7662
|
+
projectAssignmentId: number | null;
|
|
7663
|
+
projectId: number | null;
|
|
7664
|
+
createdAt: string;
|
|
7665
|
+
}>(
|
|
7666
|
+
`SELECT t.id,
|
|
7667
|
+
t.name,
|
|
7668
|
+
t.description,
|
|
7669
|
+
t.priority,
|
|
7670
|
+
t.status,
|
|
7671
|
+
t.due_date AS "dueDate",
|
|
7672
|
+
t.estimate_hours AS "estimateHours",
|
|
7673
|
+
t.position,
|
|
7674
|
+
t.tags,
|
|
7675
|
+
t.assignee_collaborator_id AS "assigneeCollaboratorId",
|
|
7676
|
+
ac.display_name AS "assigneeName",
|
|
7677
|
+
au.photo_id AS "assigneeUserPhotoId",
|
|
7678
|
+
ap.avatar_id AS "assigneePersonAvatarId",
|
|
7679
|
+
t.project_assignment_id AS "projectAssignmentId",
|
|
7680
|
+
COALESCE(t.project_id, pa.project_id) AS "projectId",
|
|
7681
|
+
t.created_at AS "createdAt"
|
|
7682
|
+
FROM operations_task t
|
|
7683
|
+
LEFT JOIN operations_collaborator ac
|
|
7684
|
+
ON ac.id = t.assignee_collaborator_id AND ac.deleted_at IS NULL
|
|
7685
|
+
LEFT JOIN "user" au ON au.id = ac.user_id
|
|
7686
|
+
LEFT JOIN person ap ON ap.id = ac.person_id
|
|
7687
|
+
LEFT JOIN operations_project_assignment pa
|
|
7688
|
+
ON pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
|
|
7689
|
+
WHERE t.id = $1`,
|
|
7690
|
+
[taskId]
|
|
7691
|
+
);
|
|
7692
|
+
|
|
7693
|
+
return rows[0] ?? null;
|
|
7193
7694
|
}
|
|
7194
7695
|
|
|
7195
7696
|
private async getOrCreateTimesheetForWorkDate(
|
|
@@ -7377,23 +7878,26 @@ export class OperationsService {
|
|
|
7377
7878
|
const assignmentIds = entries
|
|
7378
7879
|
.map((entry) => entry.projectAssignmentId)
|
|
7379
7880
|
.filter((value): value is number => typeof value === 'number');
|
|
7881
|
+
const assignmentMap = new Map<
|
|
7882
|
+
number,
|
|
7883
|
+
{ id: number; collaboratorId: number; projectId: number }
|
|
7884
|
+
>();
|
|
7380
7885
|
|
|
7381
7886
|
if (assignmentIds.length) {
|
|
7382
7887
|
const assignments = (await client.$queryRawUnsafe(
|
|
7383
|
-
`SELECT id,
|
|
7888
|
+
`SELECT id,
|
|
7889
|
+
collaborator_id AS "collaboratorId",
|
|
7890
|
+
project_id AS "projectId"
|
|
7384
7891
|
FROM operations_project_assignment
|
|
7385
7892
|
WHERE id = ANY($1::int[])
|
|
7386
7893
|
AND deleted_at IS NULL`,
|
|
7387
7894
|
assignmentIds
|
|
7388
|
-
)) as { id: number; collaboratorId: number }[];
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
assignment.collaboratorId,
|
|
7393
|
-
])
|
|
7394
|
-
);
|
|
7895
|
+
)) as { id: number; collaboratorId: number; projectId: number }[];
|
|
7896
|
+
assignments.forEach((assignment) => {
|
|
7897
|
+
assignmentMap.set(assignment.id, assignment);
|
|
7898
|
+
});
|
|
7395
7899
|
for (const assignmentId of assignmentIds) {
|
|
7396
|
-
if (assignmentMap.get(assignmentId) !== collaboratorId) {
|
|
7900
|
+
if (assignmentMap.get(assignmentId)?.collaboratorId !== collaboratorId) {
|
|
7397
7901
|
throw new ForbiddenException(
|
|
7398
7902
|
'Timesheet entries must use assignments owned by the target collaborator.'
|
|
7399
7903
|
);
|
|
@@ -7407,14 +7911,17 @@ export class OperationsService {
|
|
|
7407
7911
|
const resolvedTask = entry.taskId
|
|
7408
7912
|
? await this.getOwnedTaskRecord(client, collaboratorId, entry.taskId)
|
|
7409
7913
|
: null;
|
|
7914
|
+
const selectedAssignment = entry.projectAssignmentId
|
|
7915
|
+
? assignmentMap.get(entry.projectAssignmentId) ?? null
|
|
7916
|
+
: null;
|
|
7410
7917
|
|
|
7411
7918
|
if (
|
|
7412
7919
|
resolvedTask &&
|
|
7413
|
-
|
|
7414
|
-
resolvedTask.
|
|
7920
|
+
selectedAssignment &&
|
|
7921
|
+
resolvedTask.projectId !== selectedAssignment.projectId
|
|
7415
7922
|
) {
|
|
7416
7923
|
throw new BadRequestException(
|
|
7417
|
-
'The selected task does not belong to the chosen project
|
|
7924
|
+
'The selected task does not belong to the chosen project.'
|
|
7418
7925
|
);
|
|
7419
7926
|
}
|
|
7420
7927
|
|
|
@@ -7488,6 +7995,37 @@ export class OperationsService {
|
|
|
7488
7995
|
);
|
|
7489
7996
|
}
|
|
7490
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
|
+
|
|
7491
8029
|
private async upsertApproval(
|
|
7492
8030
|
client: any,
|
|
7493
8031
|
input: {
|
|
@@ -8055,6 +8593,146 @@ export class OperationsService {
|
|
|
8055
8593
|
);
|
|
8056
8594
|
}
|
|
8057
8595
|
|
|
8596
|
+
private async syncHiringContractDraft(
|
|
8597
|
+
client: any,
|
|
8598
|
+
updatedByUserId: number,
|
|
8599
|
+
collaboratorId: number,
|
|
8600
|
+
data: Partial<CollaboratorPayload>
|
|
8601
|
+
) {
|
|
8602
|
+
const collaborator = await client.$queryRawUnsafe(
|
|
8603
|
+
`SELECT c.id,
|
|
8604
|
+
c.code,
|
|
8605
|
+
COALESCE(NULLIF(c.display_name, ''), person_record.name) AS "displayName",
|
|
8606
|
+
collaborator_type.slug AS "collaboratorTypeSlug",
|
|
8607
|
+
c.supervisor_collaborator_id AS "supervisorCollaboratorId",
|
|
8608
|
+
c.joined_at AS "joinedAt",
|
|
8609
|
+
c.weekly_capacity_hours AS "weeklyCapacityHours"
|
|
8610
|
+
FROM operations_collaborator c
|
|
8611
|
+
LEFT JOIN person person_record
|
|
8612
|
+
ON person_record.id = c.person_id
|
|
8613
|
+
LEFT JOIN operations_collaborator_type collaborator_type
|
|
8614
|
+
ON collaborator_type.id = c.collaborator_type_id
|
|
8615
|
+
AND collaborator_type.deleted_at IS NULL
|
|
8616
|
+
WHERE c.id = $1
|
|
8617
|
+
AND c.deleted_at IS NULL
|
|
8618
|
+
LIMIT 1`,
|
|
8619
|
+
collaboratorId
|
|
8620
|
+
) as Array<{
|
|
8621
|
+
id: number;
|
|
8622
|
+
code: string | null;
|
|
8623
|
+
displayName: string | null;
|
|
8624
|
+
collaboratorTypeSlug: string | null;
|
|
8625
|
+
supervisorCollaboratorId: number | null;
|
|
8626
|
+
joinedAt: string | null;
|
|
8627
|
+
weeklyCapacityHours: number | null;
|
|
8628
|
+
}>;
|
|
8629
|
+
|
|
8630
|
+
const currentCollaborator = collaborator[0] ?? null;
|
|
8631
|
+
|
|
8632
|
+
if (!currentCollaborator) {
|
|
8633
|
+
throw new NotFoundException('Collaborator not found.');
|
|
8634
|
+
}
|
|
8635
|
+
|
|
8636
|
+
const hiringContracts = (await client.$queryRawUnsafe(
|
|
8637
|
+
`SELECT id,
|
|
8638
|
+
budget_amount AS "budgetAmount",
|
|
8639
|
+
description
|
|
8640
|
+
FROM operations_contract
|
|
8641
|
+
WHERE related_collaborator_id = $1
|
|
8642
|
+
AND origin_type = 'employee_hiring'
|
|
8643
|
+
AND deleted_at IS NULL
|
|
8644
|
+
ORDER BY created_at DESC
|
|
8645
|
+
LIMIT 1`,
|
|
8646
|
+
collaboratorId
|
|
8647
|
+
)) as Array<{
|
|
8648
|
+
id: number;
|
|
8649
|
+
budgetAmount: number | null;
|
|
8650
|
+
description: string | null;
|
|
8651
|
+
}>;
|
|
8652
|
+
|
|
8653
|
+
const hiringContract = hiringContracts[0] ?? null;
|
|
8654
|
+
const collaboratorCode =
|
|
8655
|
+
this.normalizeOptionalText(currentCollaborator.code) ??
|
|
8656
|
+
`COL-${collaboratorId}`;
|
|
8657
|
+
const displayName =
|
|
8658
|
+
this.normalizeOptionalText(currentCollaborator.displayName) ??
|
|
8659
|
+
`Collaborator ${collaboratorId}`;
|
|
8660
|
+
const collaboratorTypeSlug =
|
|
8661
|
+
this.normalizeOptionalText(currentCollaborator.collaboratorTypeSlug) ??
|
|
8662
|
+
'other';
|
|
8663
|
+
const startDate =
|
|
8664
|
+
data.joinedAt !== undefined
|
|
8665
|
+
? data.joinedAt ?? null
|
|
8666
|
+
: currentCollaborator.joinedAt ?? null;
|
|
8667
|
+
const weeklyCapacityHours =
|
|
8668
|
+
data.weeklyCapacityHours !== undefined
|
|
8669
|
+
? data.weeklyCapacityHours ?? null
|
|
8670
|
+
: currentCollaborator.weeklyCapacityHours ?? null;
|
|
8671
|
+
const compensationAmount =
|
|
8672
|
+
data.compensationAmount !== undefined
|
|
8673
|
+
? data.compensationAmount ?? null
|
|
8674
|
+
: hiringContract?.budgetAmount ?? null;
|
|
8675
|
+
const description =
|
|
8676
|
+
data.contractDescription !== undefined
|
|
8677
|
+
? this.normalizeOptionalText(data.contractDescription)
|
|
8678
|
+
: hiringContract?.description ?? null;
|
|
8679
|
+
const supervisorCollaboratorId =
|
|
8680
|
+
data.supervisorCollaboratorId !== undefined
|
|
8681
|
+
? data.supervisorCollaboratorId ?? null
|
|
8682
|
+
: currentCollaborator.supervisorCollaboratorId ?? null;
|
|
8683
|
+
|
|
8684
|
+
if (!hiringContract) {
|
|
8685
|
+
if (data.autoGenerateContractDraft === false) {
|
|
8686
|
+
return;
|
|
8687
|
+
}
|
|
8688
|
+
|
|
8689
|
+
await this.createHiringContractDraft(client, updatedByUserId, {
|
|
8690
|
+
collaboratorId,
|
|
8691
|
+
collaboratorCode,
|
|
8692
|
+
displayName,
|
|
8693
|
+
collaboratorType: collaboratorTypeSlug,
|
|
8694
|
+
supervisorCollaboratorId,
|
|
8695
|
+
startDate,
|
|
8696
|
+
weeklyCapacityHours,
|
|
8697
|
+
compensationAmount,
|
|
8698
|
+
description,
|
|
8699
|
+
});
|
|
8700
|
+
return;
|
|
8701
|
+
}
|
|
8702
|
+
|
|
8703
|
+
await client.$executeRawUnsafe(
|
|
8704
|
+
`UPDATE operations_contract
|
|
8705
|
+
SET code = $1,
|
|
8706
|
+
name = $2,
|
|
8707
|
+
contract_category = $3::operations_contract_contract_category_70d553ea09_enum,
|
|
8708
|
+
contract_type = $4::operations_contract_contract_type_48331e2ebf_enum,
|
|
8709
|
+
client_name = $5,
|
|
8710
|
+
billing_model = $6::operations_contract_billing_model_409dc7fea2_enum,
|
|
8711
|
+
account_manager_collaborator_id = $7,
|
|
8712
|
+
start_date = $8::date,
|
|
8713
|
+
effective_date = $8::date,
|
|
8714
|
+
budget_amount = $9,
|
|
8715
|
+
monthly_hour_cap = $10,
|
|
8716
|
+
description = $11,
|
|
8717
|
+
updated_by_user_id = $12,
|
|
8718
|
+
updated_at = NOW()
|
|
8719
|
+
WHERE id = $13`,
|
|
8720
|
+
`HIR-${collaboratorCode}`,
|
|
8721
|
+
this.buildHiringContractName(displayName, collaboratorTypeSlug),
|
|
8722
|
+
this.mapContractCategoryForCollaboratorType(collaboratorTypeSlug),
|
|
8723
|
+
this.mapContractTypeForCollaboratorType(collaboratorTypeSlug),
|
|
8724
|
+
displayName,
|
|
8725
|
+
this.mapBillingModelForCollaboratorType(collaboratorTypeSlug),
|
|
8726
|
+
supervisorCollaboratorId,
|
|
8727
|
+
startDate ?? new Date().toISOString().slice(0, 10),
|
|
8728
|
+
compensationAmount,
|
|
8729
|
+
weeklyCapacityHours ? Math.round(Number(weeklyCapacityHours) * 4) : null,
|
|
8730
|
+
description,
|
|
8731
|
+
updatedByUserId,
|
|
8732
|
+
hiringContract.id
|
|
8733
|
+
);
|
|
8734
|
+
}
|
|
8735
|
+
|
|
8058
8736
|
private async createProjectContractDraft(
|
|
8059
8737
|
client: any,
|
|
8060
8738
|
createdByUserId: number,
|