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