@hed-hog/operations 0.0.303 → 0.0.304
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 +169 -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 +54 -0
- package/dist/controllers/operations-tasks.controller.d.ts.map +1 -0
- package/dist/controllers/operations-tasks.controller.js +79 -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 +8 -0
- package/dist/dto/create-task.dto.d.ts.map +1 -0
- package/dist/dto/create-task.dto.js +50 -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 +8 -0
- package/dist/dto/update-task.dto.d.ts.map +1 -0
- package/dist/dto/update-task.dto.js +51 -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 +373 -8
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +1598 -111
- 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 +183 -0
- package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +231 -0
- package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +134 -49
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +772 -93
- package/hedhog/frontend/app/_components/department-select-with-create.tsx.ejs +38 -16
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +875 -632
- 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 +142 -39
- package/hedhog/frontend/app/_lib/utils/format.ts.ejs +33 -2
- 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 +109 -68
- 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/page.tsx.ejs +5 -1
- 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 +243 -51
- package/hedhog/frontend/messages/pt.json +458 -268
- 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_assignment.yaml +9 -0
- package/hedhog/table/operations_project_role.yaml +39 -0
- package/hedhog/table/operations_task.yaml +30 -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 +52 -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 +35 -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 +36 -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 +4641 -2163
- 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 = ['active', 'completed', 'archived'];
|
|
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,9 @@ 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
|
+
}
|
|
564
837
|
});
|
|
565
838
|
return this.getCollaboratorByIdForUser(userId, collaboratorId);
|
|
566
839
|
}
|
|
@@ -590,6 +863,67 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
590
863
|
ORDER BY CASE WHEN d.deleted_at IS NULL THEN 0 ELSE 1 END ASC,
|
|
591
864
|
d.name ASC`);
|
|
592
865
|
}
|
|
866
|
+
async listJobTitles(userId) {
|
|
867
|
+
const actor = await this.getActorContext(userId);
|
|
868
|
+
this.ensureCollaborator(actor);
|
|
869
|
+
return this.queryRows(`SELECT jt.id,
|
|
870
|
+
jt.slug,
|
|
871
|
+
jt.code,
|
|
872
|
+
jt.name,
|
|
873
|
+
jt.description,
|
|
874
|
+
CASE WHEN jt.deleted_at IS NULL THEN 'active' ELSE 'inactive' END AS status,
|
|
875
|
+
COUNT(DISTINCT c.id)::int AS "collaboratorCount",
|
|
876
|
+
jt.created_at AS "createdAt",
|
|
877
|
+
jt.updated_at AS "updatedAt"
|
|
878
|
+
FROM operations_job_title jt
|
|
879
|
+
LEFT JOIN operations_collaborator c
|
|
880
|
+
ON c.deleted_at IS NULL
|
|
881
|
+
AND (
|
|
882
|
+
c.job_title_id = jt.id
|
|
883
|
+
OR (
|
|
884
|
+
c.job_title_id IS NULL
|
|
885
|
+
AND LOWER(COALESCE(c.title, '')) = LOWER(jt.name)
|
|
886
|
+
)
|
|
887
|
+
)
|
|
888
|
+
GROUP BY jt.id
|
|
889
|
+
ORDER BY CASE WHEN jt.deleted_at IS NULL THEN 0 ELSE 1 END ASC,
|
|
890
|
+
jt.name ASC`);
|
|
891
|
+
}
|
|
892
|
+
async createJobTitle(userId, data) {
|
|
893
|
+
const actor = await this.getActorContext(userId);
|
|
894
|
+
this.ensureDirector(actor);
|
|
895
|
+
const name = this.normalizeOptionalText(data.name);
|
|
896
|
+
if (!name) {
|
|
897
|
+
throw new common_1.BadRequestException('Job title name is required.');
|
|
898
|
+
}
|
|
899
|
+
return this.prisma.$transaction(async (tx) => {
|
|
900
|
+
var _a, _b, _c, _d;
|
|
901
|
+
await this.assertJobTitleNameAvailable(tx, name);
|
|
902
|
+
const normalizedCode = (_b = (_a = this.normalizeOptionalText(data.code)) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : null;
|
|
903
|
+
if (normalizedCode) {
|
|
904
|
+
await this.assertJobTitleCodeAvailable(tx, normalizedCode);
|
|
905
|
+
}
|
|
906
|
+
const created = (await tx.$queryRawUnsafe(`INSERT INTO operations_job_title (
|
|
907
|
+
slug,
|
|
908
|
+
code,
|
|
909
|
+
name,
|
|
910
|
+
description,
|
|
911
|
+
deleted_at,
|
|
912
|
+
created_at,
|
|
913
|
+
updated_at
|
|
914
|
+
) VALUES (
|
|
915
|
+
$1, $2, $3, $4,
|
|
916
|
+
CASE WHEN $5 = 'inactive' THEN NOW() ELSE NULL END,
|
|
917
|
+
NOW(), NOW()
|
|
918
|
+
)
|
|
919
|
+
RETURNING id`, await this.generateUniqueJobTitleSlug(tx, name), normalizedCode, name, this.normalizeOptionalText(data.description), (_c = data.status) !== null && _c !== void 0 ? _c : 'active'));
|
|
920
|
+
const createdJobTitleId = (_d = created[0]) === null || _d === void 0 ? void 0 : _d.id;
|
|
921
|
+
if (!createdJobTitleId) {
|
|
922
|
+
throw new common_1.BadRequestException('Unable to create the job title.');
|
|
923
|
+
}
|
|
924
|
+
return this.getJobTitleById(tx, createdJobTitleId, true);
|
|
925
|
+
});
|
|
926
|
+
}
|
|
593
927
|
async createDepartment(userId, data) {
|
|
594
928
|
const actor = await this.getActorContext(userId);
|
|
595
929
|
this.ensureDirector(actor);
|
|
@@ -705,6 +1039,436 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
705
1039
|
GROUP BY p.id, c.id, m.id
|
|
706
1040
|
ORDER BY p.name ASC`, [...assignmentParams, ...filter.params]);
|
|
707
1041
|
}
|
|
1042
|
+
async listProjectOptions(userId, paginationParams) {
|
|
1043
|
+
var _a, _b;
|
|
1044
|
+
const actor = await this.getActorContext(userId);
|
|
1045
|
+
this.ensureCollaborator(actor);
|
|
1046
|
+
if (!actor.collaboratorId) {
|
|
1047
|
+
throw new common_1.BadRequestException('Collaborator context is required.');
|
|
1048
|
+
}
|
|
1049
|
+
const pagination = this.normalizePaginationParams(paginationParams, {
|
|
1050
|
+
defaultSortField: 'name',
|
|
1051
|
+
defaultSortOrder: 'asc',
|
|
1052
|
+
allowedSortFields: ['name', 'code', 'clientName', 'startDate', 'endDate'],
|
|
1053
|
+
});
|
|
1054
|
+
const params = [actor.collaboratorId];
|
|
1055
|
+
const filters = [
|
|
1056
|
+
'p.deleted_at IS NULL',
|
|
1057
|
+
'pa.deleted_at IS NULL',
|
|
1058
|
+
`pa.collaborator_id = $1`,
|
|
1059
|
+
`pa.status IN ('planned', 'active')`,
|
|
1060
|
+
];
|
|
1061
|
+
if (pagination.search) {
|
|
1062
|
+
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
1063
|
+
filters.push(`(
|
|
1064
|
+
COALESCE(p.name, '') ILIKE ${searchPlaceholder}
|
|
1065
|
+
OR COALESCE(p.code, '') ILIKE ${searchPlaceholder}
|
|
1066
|
+
OR COALESCE(p.client_name, '') ILIKE ${searchPlaceholder}
|
|
1067
|
+
OR COALESCE(pa.role_label, '') ILIKE ${searchPlaceholder}
|
|
1068
|
+
)`);
|
|
1069
|
+
}
|
|
1070
|
+
const whereClause = filters.join(' AND ');
|
|
1071
|
+
const totalRow = await this.querySingle(`SELECT COUNT(DISTINCT p.id)::text AS total
|
|
1072
|
+
FROM operations_project_assignment pa
|
|
1073
|
+
JOIN operations_project p
|
|
1074
|
+
ON p.id = pa.project_id
|
|
1075
|
+
WHERE ${whereClause}`, params);
|
|
1076
|
+
const sortColumn = (_a = {
|
|
1077
|
+
name: 'p.name',
|
|
1078
|
+
code: 'p.code',
|
|
1079
|
+
clientName: 'p.client_name',
|
|
1080
|
+
startDate: 'p.start_date',
|
|
1081
|
+
endDate: 'p.end_date',
|
|
1082
|
+
}[pagination.sortField]) !== null && _a !== void 0 ? _a : 'p.name';
|
|
1083
|
+
const queryParams = [...params];
|
|
1084
|
+
const limitPlaceholder = this.param(queryParams, pagination.pageSize);
|
|
1085
|
+
const offsetPlaceholder = this.param(queryParams, pagination.offset);
|
|
1086
|
+
const rows = await this.queryRows(`SELECT p.id,
|
|
1087
|
+
p.code,
|
|
1088
|
+
p.name,
|
|
1089
|
+
p.client_name AS "clientName",
|
|
1090
|
+
MAX(pa.id)::int AS "projectAssignmentId",
|
|
1091
|
+
MAX(pa.role_label) AS "roleLabel",
|
|
1092
|
+
p.status,
|
|
1093
|
+
p.start_date AS "startDate",
|
|
1094
|
+
p.end_date AS "endDate"
|
|
1095
|
+
FROM operations_project_assignment pa
|
|
1096
|
+
JOIN operations_project p
|
|
1097
|
+
ON p.id = pa.project_id
|
|
1098
|
+
WHERE ${whereClause}
|
|
1099
|
+
GROUP BY p.id
|
|
1100
|
+
ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, p.id ASC
|
|
1101
|
+
LIMIT ${limitPlaceholder}
|
|
1102
|
+
OFFSET ${offsetPlaceholder}`, queryParams);
|
|
1103
|
+
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);
|
|
1104
|
+
}
|
|
1105
|
+
async listTasks(userId, paginationParams) {
|
|
1106
|
+
var _a, _b;
|
|
1107
|
+
const actor = await this.getActorContext(userId);
|
|
1108
|
+
this.ensureCollaborator(actor);
|
|
1109
|
+
if (!actor.collaboratorId) {
|
|
1110
|
+
throw new common_1.BadRequestException('Collaborator context is required.');
|
|
1111
|
+
}
|
|
1112
|
+
const pagination = this.normalizePaginationParams(paginationParams, {
|
|
1113
|
+
defaultSortField: 'name',
|
|
1114
|
+
defaultSortOrder: 'asc',
|
|
1115
|
+
allowedSortFields: ['name', 'projectName', 'status', 'createdAt'],
|
|
1116
|
+
});
|
|
1117
|
+
const params = [actor.collaboratorId];
|
|
1118
|
+
const filters = [
|
|
1119
|
+
't.deleted_at IS NULL',
|
|
1120
|
+
'pa.deleted_at IS NULL',
|
|
1121
|
+
'p.deleted_at IS NULL',
|
|
1122
|
+
`pa.collaborator_id = $1`,
|
|
1123
|
+
`pa.status IN ('planned', 'active')`,
|
|
1124
|
+
];
|
|
1125
|
+
if (pagination.search) {
|
|
1126
|
+
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
1127
|
+
filters.push(`(
|
|
1128
|
+
COALESCE(t.name, '') ILIKE ${searchPlaceholder}
|
|
1129
|
+
OR COALESCE(t.description, '') ILIKE ${searchPlaceholder}
|
|
1130
|
+
OR COALESCE(p.name, '') ILIKE ${searchPlaceholder}
|
|
1131
|
+
OR COALESCE(p.code, '') ILIKE ${searchPlaceholder}
|
|
1132
|
+
)`);
|
|
1133
|
+
}
|
|
1134
|
+
if (paginationParams.projectAssignmentId) {
|
|
1135
|
+
filters.push(`pa.id = ${this.param(params, paginationParams.projectAssignmentId)}`);
|
|
1136
|
+
}
|
|
1137
|
+
if (paginationParams.projectId) {
|
|
1138
|
+
filters.push(`pa.project_id = ${this.param(params, paginationParams.projectId)}`);
|
|
1139
|
+
}
|
|
1140
|
+
if (paginationParams.status) {
|
|
1141
|
+
filters.push(`t.status = ${this.param(params, paginationParams.status)}`);
|
|
1142
|
+
}
|
|
1143
|
+
const whereClause = filters.join(' AND ');
|
|
1144
|
+
const totalRow = await this.querySingle(`SELECT COUNT(*)::text AS total
|
|
1145
|
+
FROM operations_task t
|
|
1146
|
+
JOIN operations_project_assignment pa
|
|
1147
|
+
ON pa.id = t.project_assignment_id
|
|
1148
|
+
JOIN operations_project p
|
|
1149
|
+
ON p.id = pa.project_id
|
|
1150
|
+
WHERE ${whereClause}`, params);
|
|
1151
|
+
const sortColumn = (_a = {
|
|
1152
|
+
name: 't.name',
|
|
1153
|
+
projectName: 'p.name',
|
|
1154
|
+
status: 't.status',
|
|
1155
|
+
createdAt: 't.created_at',
|
|
1156
|
+
}[pagination.sortField]) !== null && _a !== void 0 ? _a : 't.name';
|
|
1157
|
+
const queryParams = [...params];
|
|
1158
|
+
const limitPlaceholder = this.param(queryParams, pagination.pageSize);
|
|
1159
|
+
const offsetPlaceholder = this.param(queryParams, pagination.offset);
|
|
1160
|
+
const rows = await this.queryRows(`SELECT t.id,
|
|
1161
|
+
t.name,
|
|
1162
|
+
t.description,
|
|
1163
|
+
t.status,
|
|
1164
|
+
pa.project_id AS "projectId",
|
|
1165
|
+
pa.id AS "projectAssignmentId",
|
|
1166
|
+
p.name AS "projectName",
|
|
1167
|
+
p.code AS "projectCode",
|
|
1168
|
+
t.created_at AS "createdAt"
|
|
1169
|
+
FROM operations_task t
|
|
1170
|
+
JOIN operations_project_assignment pa
|
|
1171
|
+
ON pa.id = t.project_assignment_id
|
|
1172
|
+
JOIN operations_project p
|
|
1173
|
+
ON p.id = pa.project_id
|
|
1174
|
+
WHERE ${whereClause}
|
|
1175
|
+
ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, t.id ASC
|
|
1176
|
+
LIMIT ${limitPlaceholder}
|
|
1177
|
+
OFFSET ${offsetPlaceholder}`, queryParams);
|
|
1178
|
+
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);
|
|
1179
|
+
}
|
|
1180
|
+
async createTask(userId, data) {
|
|
1181
|
+
var _a, _b, _c, _d;
|
|
1182
|
+
const actor = await this.getActorContext(userId);
|
|
1183
|
+
this.ensureCollaborator(actor);
|
|
1184
|
+
this.requireFields(data, ['name']);
|
|
1185
|
+
if (!actor.collaboratorId) {
|
|
1186
|
+
throw new common_1.BadRequestException('Collaborator context is required.');
|
|
1187
|
+
}
|
|
1188
|
+
const assignment = await this.resolveOwnedProjectAssignment(this.prisma, actor.collaboratorId, {
|
|
1189
|
+
projectId: (_a = data.projectId) !== null && _a !== void 0 ? _a : null,
|
|
1190
|
+
projectAssignmentId: (_b = data.projectAssignmentId) !== null && _b !== void 0 ? _b : null,
|
|
1191
|
+
});
|
|
1192
|
+
await this.assertProjectAccess(actor, assignment.projectId);
|
|
1193
|
+
const name = this.normalizeOptionalText(data.name);
|
|
1194
|
+
if (!name) {
|
|
1195
|
+
throw new common_1.BadRequestException('Field "name" is required.');
|
|
1196
|
+
}
|
|
1197
|
+
const created = await this.querySingle(`INSERT INTO operations_task (
|
|
1198
|
+
project_assignment_id,
|
|
1199
|
+
name,
|
|
1200
|
+
description,
|
|
1201
|
+
status,
|
|
1202
|
+
created_at,
|
|
1203
|
+
updated_at
|
|
1204
|
+
) VALUES (
|
|
1205
|
+
$1,
|
|
1206
|
+
$2,
|
|
1207
|
+
$3,
|
|
1208
|
+
$4,
|
|
1209
|
+
NOW(),
|
|
1210
|
+
NOW()
|
|
1211
|
+
)
|
|
1212
|
+
RETURNING id`, [
|
|
1213
|
+
assignment.id,
|
|
1214
|
+
name,
|
|
1215
|
+
this.normalizeOptionalText(data.description),
|
|
1216
|
+
(_c = data.status) !== null && _c !== void 0 ? _c : 'active',
|
|
1217
|
+
]);
|
|
1218
|
+
return this.getTaskOptionById(actor.collaboratorId, (_d = created === null || created === void 0 ? void 0 : created.id) !== null && _d !== void 0 ? _d : 0);
|
|
1219
|
+
}
|
|
1220
|
+
async updateTask(userId, taskId, data) {
|
|
1221
|
+
const actor = await this.getActorContext(userId);
|
|
1222
|
+
this.ensureCollaborator(actor);
|
|
1223
|
+
if (!actor.collaboratorId) {
|
|
1224
|
+
throw new common_1.BadRequestException('Collaborator context is required.');
|
|
1225
|
+
}
|
|
1226
|
+
const current = await this.getOwnedTaskRecord(this.prisma, actor.collaboratorId, taskId);
|
|
1227
|
+
await this.assertProjectAccess(actor, current.projectId);
|
|
1228
|
+
const nextName = data.name !== undefined
|
|
1229
|
+
? this.normalizeOptionalText(data.name)
|
|
1230
|
+
: current.name;
|
|
1231
|
+
if (!nextName) {
|
|
1232
|
+
throw new common_1.BadRequestException('Field "name" is required.');
|
|
1233
|
+
}
|
|
1234
|
+
await this.prisma.$transaction(async (tx) => {
|
|
1235
|
+
var _a, _b, _c, _d;
|
|
1236
|
+
const nextAssignment = data.projectId !== undefined || data.projectAssignmentId !== undefined
|
|
1237
|
+
? await this.resolveOwnedProjectAssignment(tx, actor.collaboratorId, {
|
|
1238
|
+
projectId: (_a = data.projectId) !== null && _a !== void 0 ? _a : null,
|
|
1239
|
+
projectAssignmentId: (_b = data.projectAssignmentId) !== null && _b !== void 0 ? _b : null,
|
|
1240
|
+
})
|
|
1241
|
+
: {
|
|
1242
|
+
id: current.projectAssignmentId,
|
|
1243
|
+
projectId: current.projectId,
|
|
1244
|
+
};
|
|
1245
|
+
await this.assertProjectAccess(actor, nextAssignment.projectId);
|
|
1246
|
+
await tx.$executeRawUnsafe(`UPDATE operations_task
|
|
1247
|
+
SET project_assignment_id = $1,
|
|
1248
|
+
name = $2,
|
|
1249
|
+
description = $3,
|
|
1250
|
+
status = $4,
|
|
1251
|
+
updated_at = NOW()
|
|
1252
|
+
WHERE id = $5
|
|
1253
|
+
AND deleted_at IS NULL`, nextAssignment.id, nextName, data.description !== undefined
|
|
1254
|
+
? this.normalizeOptionalText(data.description)
|
|
1255
|
+
: ((_c = current.description) !== null && _c !== void 0 ? _c : null), (_d = data.status) !== null && _d !== void 0 ? _d : current.status, taskId);
|
|
1256
|
+
});
|
|
1257
|
+
return this.getTaskOptionById(actor.collaboratorId, taskId);
|
|
1258
|
+
}
|
|
1259
|
+
async removeTask(userId, taskId) {
|
|
1260
|
+
const actor = await this.getActorContext(userId);
|
|
1261
|
+
this.ensureCollaborator(actor);
|
|
1262
|
+
if (!actor.collaboratorId) {
|
|
1263
|
+
throw new common_1.BadRequestException('Collaborator context is required.');
|
|
1264
|
+
}
|
|
1265
|
+
const current = await this.getOwnedTaskRecord(this.prisma, actor.collaboratorId, taskId);
|
|
1266
|
+
await this.assertProjectAccess(actor, current.projectId);
|
|
1267
|
+
await this.prisma.$transaction(async (tx) => {
|
|
1268
|
+
await tx.$executeRawUnsafe(`UPDATE operations_task
|
|
1269
|
+
SET deleted_at = COALESCE(deleted_at, NOW()),
|
|
1270
|
+
status = 'archived',
|
|
1271
|
+
updated_at = NOW()
|
|
1272
|
+
WHERE id = $1
|
|
1273
|
+
AND deleted_at IS NULL`, taskId);
|
|
1274
|
+
});
|
|
1275
|
+
return { success: true };
|
|
1276
|
+
}
|
|
1277
|
+
async listTimesheetEntries(userId, paginationParams) {
|
|
1278
|
+
var _a, _b;
|
|
1279
|
+
const actor = await this.getActorContext(userId);
|
|
1280
|
+
this.ensureCollaborator(actor);
|
|
1281
|
+
if (!actor.collaboratorId) {
|
|
1282
|
+
throw new common_1.BadRequestException('Collaborator context is required.');
|
|
1283
|
+
}
|
|
1284
|
+
const pagination = this.normalizePaginationParams(paginationParams, {
|
|
1285
|
+
defaultSortField: 'workDate',
|
|
1286
|
+
defaultSortOrder: 'desc',
|
|
1287
|
+
allowedSortFields: [
|
|
1288
|
+
'workDate',
|
|
1289
|
+
'createdAt',
|
|
1290
|
+
'durationMinutes',
|
|
1291
|
+
'projectName',
|
|
1292
|
+
'taskName',
|
|
1293
|
+
'status',
|
|
1294
|
+
],
|
|
1295
|
+
});
|
|
1296
|
+
const params = [actor.collaboratorId];
|
|
1297
|
+
const filters = [
|
|
1298
|
+
'e.deleted_at IS NULL',
|
|
1299
|
+
't.deleted_at IS NULL',
|
|
1300
|
+
`t.collaborator_id = $1`,
|
|
1301
|
+
];
|
|
1302
|
+
if (pagination.search) {
|
|
1303
|
+
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
1304
|
+
filters.push(`(
|
|
1305
|
+
COALESCE(p.name, '') ILIKE ${searchPlaceholder}
|
|
1306
|
+
OR COALESCE(p.code, '') ILIKE ${searchPlaceholder}
|
|
1307
|
+
OR COALESCE(task_record.name, '') ILIKE ${searchPlaceholder}
|
|
1308
|
+
OR COALESCE(e.activity_label, '') ILIKE ${searchPlaceholder}
|
|
1309
|
+
OR COALESCE(e.description, '') ILIKE ${searchPlaceholder}
|
|
1310
|
+
)`);
|
|
1311
|
+
}
|
|
1312
|
+
if (paginationParams.projectAssignmentId) {
|
|
1313
|
+
filters.push(`e.project_assignment_id = ${this.param(params, paginationParams.projectAssignmentId)}`);
|
|
1314
|
+
}
|
|
1315
|
+
if (paginationParams.projectId) {
|
|
1316
|
+
filters.push(`pa.project_id = ${this.param(params, paginationParams.projectId)}`);
|
|
1317
|
+
}
|
|
1318
|
+
if (paginationParams.taskId) {
|
|
1319
|
+
filters.push(`e.task_id = ${this.param(params, paginationParams.taskId)}`);
|
|
1320
|
+
}
|
|
1321
|
+
if (paginationParams.status) {
|
|
1322
|
+
filters.push(`t.status = ${this.param(params, paginationParams.status, 'operations_timesheet_status_128a8ecd30_enum')}`);
|
|
1323
|
+
}
|
|
1324
|
+
if (paginationParams.fromDate) {
|
|
1325
|
+
filters.push(`e.work_date >= ${this.param(params, paginationParams.fromDate, 'date')}`);
|
|
1326
|
+
}
|
|
1327
|
+
if (paginationParams.toDate) {
|
|
1328
|
+
filters.push(`e.work_date <= ${this.param(params, paginationParams.toDate, 'date')}`);
|
|
1329
|
+
}
|
|
1330
|
+
const whereClause = filters.join(' AND ');
|
|
1331
|
+
const totalRow = await this.querySingle(`SELECT COUNT(*)::text AS total
|
|
1332
|
+
FROM operations_timesheet_entry e
|
|
1333
|
+
JOIN operations_timesheet t
|
|
1334
|
+
ON t.id = e.timesheet_id
|
|
1335
|
+
LEFT JOIN operations_project_assignment pa
|
|
1336
|
+
ON pa.id = e.project_assignment_id
|
|
1337
|
+
LEFT JOIN operations_project p
|
|
1338
|
+
ON p.id = pa.project_id
|
|
1339
|
+
LEFT JOIN operations_task task_record
|
|
1340
|
+
ON task_record.id = e.task_id
|
|
1341
|
+
AND task_record.deleted_at IS NULL
|
|
1342
|
+
WHERE ${whereClause}`, params);
|
|
1343
|
+
const sortColumn = (_a = {
|
|
1344
|
+
workDate: 'e.work_date',
|
|
1345
|
+
createdAt: 'e.created_at',
|
|
1346
|
+
durationMinutes: 'COALESCE(NULLIF(e.duration_minutes, 0), ROUND(COALESCE(e.hours, 0)::numeric * 60))',
|
|
1347
|
+
projectName: 'p.name',
|
|
1348
|
+
taskName: 'COALESCE(task_record.name, e.activity_label)',
|
|
1349
|
+
status: 't.status',
|
|
1350
|
+
}[pagination.sortField]) !== null && _a !== void 0 ? _a : 'e.work_date';
|
|
1351
|
+
const queryParams = [...params];
|
|
1352
|
+
const limitPlaceholder = this.param(queryParams, pagination.pageSize);
|
|
1353
|
+
const offsetPlaceholder = this.param(queryParams, pagination.offset);
|
|
1354
|
+
const rows = await this.queryRows(`SELECT e.id,
|
|
1355
|
+
e.timesheet_id AS "timesheetId",
|
|
1356
|
+
t.collaborator_id AS "collaboratorId",
|
|
1357
|
+
pa.project_id AS "projectId",
|
|
1358
|
+
e.project_assignment_id AS "projectAssignmentId",
|
|
1359
|
+
p.code AS "projectCode",
|
|
1360
|
+
p.name AS "projectName",
|
|
1361
|
+
e.task_id AS "taskId",
|
|
1362
|
+
COALESCE(task_record.name, e.activity_label) AS "taskName",
|
|
1363
|
+
e.activity_label AS "activityLabel",
|
|
1364
|
+
e.work_date AS "workDate",
|
|
1365
|
+
COALESCE(NULLIF(e.duration_minutes, 0), ROUND(COALESCE(e.hours, 0)::numeric * 60))::int AS "durationMinutes",
|
|
1366
|
+
COALESCE(
|
|
1367
|
+
e.hours,
|
|
1368
|
+
ROUND((COALESCE(NULLIF(e.duration_minutes, 0), 0)::numeric / 60), 2)
|
|
1369
|
+
) AS hours,
|
|
1370
|
+
e.description,
|
|
1371
|
+
t.status,
|
|
1372
|
+
t.week_start_date AS "weekStartDate",
|
|
1373
|
+
t.week_end_date AS "weekEndDate",
|
|
1374
|
+
e.created_at AS "createdAt"
|
|
1375
|
+
FROM operations_timesheet_entry e
|
|
1376
|
+
JOIN operations_timesheet t
|
|
1377
|
+
ON t.id = e.timesheet_id
|
|
1378
|
+
LEFT JOIN operations_project_assignment pa
|
|
1379
|
+
ON pa.id = e.project_assignment_id
|
|
1380
|
+
LEFT JOIN operations_project p
|
|
1381
|
+
ON p.id = pa.project_id
|
|
1382
|
+
LEFT JOIN operations_task task_record
|
|
1383
|
+
ON task_record.id = e.task_id
|
|
1384
|
+
AND task_record.deleted_at IS NULL
|
|
1385
|
+
WHERE ${whereClause}
|
|
1386
|
+
ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, e.id DESC
|
|
1387
|
+
LIMIT ${limitPlaceholder}
|
|
1388
|
+
OFFSET ${offsetPlaceholder}`, queryParams);
|
|
1389
|
+
return this.buildPaginationResult(rows.map((row) => (Object.assign(Object.assign({}, row), { label: [row.projectCode, row.projectName, row.taskName]
|
|
1390
|
+
.filter(Boolean)
|
|
1391
|
+
.join(' • ') }))), Number((_b = totalRow === null || totalRow === void 0 ? void 0 : totalRow.total) !== null && _b !== void 0 ? _b : 0), pagination.page, pagination.pageSize);
|
|
1392
|
+
}
|
|
1393
|
+
async createTimesheetEntry(userId, data) {
|
|
1394
|
+
var _a;
|
|
1395
|
+
const actor = await this.getActorContext(userId);
|
|
1396
|
+
this.ensureCollaborator(actor);
|
|
1397
|
+
this.requireFields(data, ['workDate', 'duration']);
|
|
1398
|
+
if (!actor.collaboratorId) {
|
|
1399
|
+
throw new common_1.BadRequestException('Collaborator context is required.');
|
|
1400
|
+
}
|
|
1401
|
+
const durationMinutes = this.normalizeDurationMinutes(data.duration, data.unit);
|
|
1402
|
+
const taskLabel = (_a = this.normalizeOptionalText(data.taskName)) !== null && _a !== void 0 ? _a : this.normalizeOptionalText(data.activityLabel);
|
|
1403
|
+
const createdEntryId = await this.prisma.$transaction(async (tx) => {
|
|
1404
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
1405
|
+
const assignment = await this.resolveOwnedProjectAssignment(tx, actor.collaboratorId, {
|
|
1406
|
+
projectId: (_a = data.projectId) !== null && _a !== void 0 ? _a : null,
|
|
1407
|
+
projectAssignmentId: (_b = data.projectAssignmentId) !== null && _b !== void 0 ? _b : null,
|
|
1408
|
+
});
|
|
1409
|
+
const resolvedTask = data.taskId
|
|
1410
|
+
? await this.getOwnedTaskRecord(tx, actor.collaboratorId, data.taskId)
|
|
1411
|
+
: null;
|
|
1412
|
+
if (resolvedTask && resolvedTask.projectAssignmentId !== assignment.id) {
|
|
1413
|
+
throw new common_1.BadRequestException('The selected task does not belong to the chosen project assignment.');
|
|
1414
|
+
}
|
|
1415
|
+
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;
|
|
1416
|
+
if (!activityLabel) {
|
|
1417
|
+
throw new common_1.BadRequestException('A task is required for the timesheet entry.');
|
|
1418
|
+
}
|
|
1419
|
+
const timesheetId = await this.getOrCreateTimesheetForWorkDate(tx, actor.collaboratorId, data.workDate);
|
|
1420
|
+
const created = (await tx.$queryRawUnsafe(`INSERT INTO operations_timesheet_entry (
|
|
1421
|
+
timesheet_id,
|
|
1422
|
+
project_assignment_id,
|
|
1423
|
+
task_id,
|
|
1424
|
+
activity_label,
|
|
1425
|
+
work_date,
|
|
1426
|
+
duration_minutes,
|
|
1427
|
+
hours,
|
|
1428
|
+
description,
|
|
1429
|
+
created_at,
|
|
1430
|
+
updated_at
|
|
1431
|
+
) VALUES (
|
|
1432
|
+
$1,
|
|
1433
|
+
$2,
|
|
1434
|
+
$3,
|
|
1435
|
+
$4,
|
|
1436
|
+
$5::date,
|
|
1437
|
+
$6,
|
|
1438
|
+
$7,
|
|
1439
|
+
$8,
|
|
1440
|
+
NOW(),
|
|
1441
|
+
NOW()
|
|
1442
|
+
)
|
|
1443
|
+
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)));
|
|
1444
|
+
await this.refreshTimesheetTotal(tx, timesheetId);
|
|
1445
|
+
return (_j = (_h = created[0]) === null || _h === void 0 ? void 0 : _h.id) !== null && _j !== void 0 ? _j : 0;
|
|
1446
|
+
});
|
|
1447
|
+
return this.getTimesheetEntryByIdForActor(actor, createdEntryId);
|
|
1448
|
+
}
|
|
1449
|
+
async removeTimesheetEntry(userId, entryId) {
|
|
1450
|
+
const actor = await this.getActorContext(userId);
|
|
1451
|
+
this.ensureCollaborator(actor);
|
|
1452
|
+
if (!actor.collaboratorId && !actor.isDirector) {
|
|
1453
|
+
throw new common_1.BadRequestException('Collaborator context is required.');
|
|
1454
|
+
}
|
|
1455
|
+
const entry = await this.getTimesheetEntryByIdForActor(actor, entryId);
|
|
1456
|
+
if (!actor.isDirector && entry.collaboratorId !== actor.collaboratorId) {
|
|
1457
|
+
throw new common_1.ForbiddenException('Only the entry owner can delete this timesheet entry.');
|
|
1458
|
+
}
|
|
1459
|
+
if (!['draft', 'rejected'].includes(entry.status)) {
|
|
1460
|
+
throw new common_1.BadRequestException('Only draft or rejected timesheet entries can be deleted.');
|
|
1461
|
+
}
|
|
1462
|
+
await this.prisma.$transaction(async (tx) => {
|
|
1463
|
+
await tx.$executeRawUnsafe(`UPDATE operations_timesheet_entry
|
|
1464
|
+
SET deleted_at = NOW(),
|
|
1465
|
+
updated_at = NOW()
|
|
1466
|
+
WHERE id = $1
|
|
1467
|
+
AND deleted_at IS NULL`, entryId);
|
|
1468
|
+
await this.refreshTimesheetTotal(tx, entry.timesheetId);
|
|
1469
|
+
});
|
|
1470
|
+
return { success: true };
|
|
1471
|
+
}
|
|
708
1472
|
async getProjectById(userId, projectId) {
|
|
709
1473
|
const actor = await this.getActorContext(userId);
|
|
710
1474
|
await this.assertProjectAccess(actor, projectId);
|
|
@@ -2101,13 +2865,22 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2101
2865
|
pa.project_id AS "projectId",
|
|
2102
2866
|
p.name AS "projectName",
|
|
2103
2867
|
pa.role_label AS "roleLabel",
|
|
2104
|
-
e.
|
|
2868
|
+
e.task_id AS "taskId",
|
|
2869
|
+
task_record.name AS "taskName",
|
|
2870
|
+
COALESCE(task_record.name, e.activity_label) AS "activityLabel",
|
|
2105
2871
|
e.work_date AS "workDate",
|
|
2106
|
-
e.hours,
|
|
2872
|
+
COALESCE(NULLIF(e.duration_minutes, 0), ROUND(COALESCE(e.hours, 0)::numeric * 60))::int AS "durationMinutes",
|
|
2873
|
+
COALESCE(
|
|
2874
|
+
e.hours,
|
|
2875
|
+
ROUND((COALESCE(NULLIF(e.duration_minutes, 0), 0)::numeric / 60), 2)
|
|
2876
|
+
) AS hours,
|
|
2107
2877
|
e.description
|
|
2108
2878
|
FROM operations_timesheet_entry e
|
|
2109
2879
|
LEFT JOIN operations_project_assignment pa ON pa.id = e.project_assignment_id
|
|
2110
2880
|
LEFT JOIN operations_project p ON p.id = pa.project_id
|
|
2881
|
+
LEFT JOIN operations_task task_record
|
|
2882
|
+
ON task_record.id = e.task_id
|
|
2883
|
+
AND task_record.deleted_at IS NULL
|
|
2111
2884
|
WHERE e.deleted_at IS NULL
|
|
2112
2885
|
AND e.timesheet_id = ANY($1::int[])
|
|
2113
2886
|
ORDER BY e.work_date ASC, e.id ASC`, [headers.map((item) => item.id)]);
|
|
@@ -2192,6 +2965,18 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2192
2965
|
}
|
|
2193
2966
|
const collaborator = await this.getCollaboratorById(current.collaboratorId);
|
|
2194
2967
|
const approverId = (_b = (_a = current.approverCollaboratorId) !== null && _a !== void 0 ? _a : collaborator.supervisorId) !== null && _b !== void 0 ? _b : null;
|
|
2968
|
+
if (!approverId) {
|
|
2969
|
+
throw new common_1.BadRequestException('An approver is required before submitting a timesheet.');
|
|
2970
|
+
}
|
|
2971
|
+
const activeEntries = await this.querySingle(`SELECT EXISTS (
|
|
2972
|
+
SELECT 1
|
|
2973
|
+
FROM operations_timesheet_entry
|
|
2974
|
+
WHERE timesheet_id = $1
|
|
2975
|
+
AND deleted_at IS NULL
|
|
2976
|
+
) AS exists`, [timesheetId]);
|
|
2977
|
+
if (!(activeEntries === null || activeEntries === void 0 ? void 0 : activeEntries.exists)) {
|
|
2978
|
+
throw new common_1.BadRequestException('Cannot submit a timesheet without active entries.');
|
|
2979
|
+
}
|
|
2195
2980
|
await this.prisma.$transaction(async (tx) => {
|
|
2196
2981
|
await tx.$executeRawUnsafe(`UPDATE operations_timesheet
|
|
2197
2982
|
SET status = 'submitted',
|
|
@@ -2606,6 +3391,9 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2606
3391
|
submitted_at = COALESCE(submitted_at, NOW()),
|
|
2607
3392
|
updated_at = NOW()
|
|
2608
3393
|
WHERE id = $3`, nextStatus, actor.collaboratorId, approval.targetId);
|
|
3394
|
+
if (nextStatus === 'approved') {
|
|
3395
|
+
await this.applyApprovedScheduleAdjustmentIfNeeded(tx, approval.targetId);
|
|
3396
|
+
}
|
|
2609
3397
|
}
|
|
2610
3398
|
await this.insertApprovalHistory(tx, approvalId, actor.collaboratorId, nextStatus === 'approved' ? 'approved' : 'rejected', (_b = data.note) !== null && _b !== void 0 ? _b : null);
|
|
2611
3399
|
});
|
|
@@ -2700,11 +3488,101 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2700
3488
|
if (!person) {
|
|
2701
3489
|
throw new common_1.NotFoundException('Person not found.');
|
|
2702
3490
|
}
|
|
2703
|
-
return person;
|
|
3491
|
+
return person;
|
|
3492
|
+
}
|
|
3493
|
+
async getDepartmentById(client, departmentId, includeInactive = false) {
|
|
3494
|
+
var _a;
|
|
3495
|
+
const departments = (await client.$queryRawUnsafe(`SELECT id,
|
|
3496
|
+
slug,
|
|
3497
|
+
code,
|
|
3498
|
+
name,
|
|
3499
|
+
description,
|
|
3500
|
+
created_at AS "createdAt",
|
|
3501
|
+
updated_at AS "updatedAt",
|
|
3502
|
+
deleted_at AS "deletedAt",
|
|
3503
|
+
CASE WHEN deleted_at IS NULL THEN 'active' ELSE 'inactive' END AS status
|
|
3504
|
+
FROM operations_department
|
|
3505
|
+
WHERE id = $1
|
|
3506
|
+
AND ($2::boolean OR deleted_at IS NULL)`, departmentId, includeInactive));
|
|
3507
|
+
const department = (_a = departments[0]) !== null && _a !== void 0 ? _a : null;
|
|
3508
|
+
if (!department) {
|
|
3509
|
+
throw new common_1.NotFoundException('Department not found.');
|
|
3510
|
+
}
|
|
3511
|
+
return department;
|
|
3512
|
+
}
|
|
3513
|
+
async assertDepartmentNameAvailable(client, name, excludeDepartmentId) {
|
|
3514
|
+
const existing = (await client.$queryRawUnsafe(`SELECT id
|
|
3515
|
+
FROM operations_department
|
|
3516
|
+
WHERE LOWER(name) = LOWER($1)
|
|
3517
|
+
AND ($2::int IS NULL OR id <> $2)
|
|
3518
|
+
LIMIT 1`, name, excludeDepartmentId !== null && excludeDepartmentId !== void 0 ? excludeDepartmentId : null));
|
|
3519
|
+
if (existing[0]) {
|
|
3520
|
+
throw new common_1.BadRequestException('A department with this name already exists.');
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
async assertDepartmentCodeAvailable(client, code, excludeDepartmentId) {
|
|
3524
|
+
const existing = (await client.$queryRawUnsafe(`SELECT id
|
|
3525
|
+
FROM operations_department
|
|
3526
|
+
WHERE UPPER(COALESCE(code, '')) = UPPER($1)
|
|
3527
|
+
AND ($2::int IS NULL OR id <> $2)
|
|
3528
|
+
LIMIT 1`, code, excludeDepartmentId !== null && excludeDepartmentId !== void 0 ? excludeDepartmentId : null));
|
|
3529
|
+
if (existing[0]) {
|
|
3530
|
+
throw new common_1.BadRequestException('A department with this code already exists.');
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
async resolveDepartmentReference(client, input) {
|
|
3534
|
+
var _a;
|
|
3535
|
+
if (input.departmentName !== undefined) {
|
|
3536
|
+
const normalizedDepartmentName = this.normalizeOptionalText(input.departmentName);
|
|
3537
|
+
if (!normalizedDepartmentName) {
|
|
3538
|
+
return null;
|
|
3539
|
+
}
|
|
3540
|
+
const existingByName = (await client.$queryRawUnsafe(`SELECT id, name
|
|
3541
|
+
FROM operations_department
|
|
3542
|
+
WHERE deleted_at IS NULL
|
|
3543
|
+
AND LOWER(name) = LOWER($1)
|
|
3544
|
+
ORDER BY id ASC
|
|
3545
|
+
LIMIT 1`, normalizedDepartmentName));
|
|
3546
|
+
if (existingByName[0]) {
|
|
3547
|
+
return existingByName[0];
|
|
3548
|
+
}
|
|
3549
|
+
const slug = await this.generateUniqueDepartmentSlug(client, normalizedDepartmentName);
|
|
3550
|
+
const created = (await client.$queryRawUnsafe(`INSERT INTO operations_department (
|
|
3551
|
+
slug,
|
|
3552
|
+
code,
|
|
3553
|
+
name,
|
|
3554
|
+
description,
|
|
3555
|
+
created_at,
|
|
3556
|
+
updated_at
|
|
3557
|
+
) VALUES ($1, NULL, $2, NULL, NOW(), NOW())
|
|
3558
|
+
RETURNING id, name`, slug, normalizedDepartmentName));
|
|
3559
|
+
return (_a = created[0]) !== null && _a !== void 0 ? _a : null;
|
|
3560
|
+
}
|
|
3561
|
+
if (typeof input.departmentId === 'number' &&
|
|
3562
|
+
Number.isInteger(input.departmentId) &&
|
|
3563
|
+
input.departmentId > 0) {
|
|
3564
|
+
return this.getDepartmentById(client, input.departmentId);
|
|
3565
|
+
}
|
|
3566
|
+
return null;
|
|
3567
|
+
}
|
|
3568
|
+
async generateUniqueDepartmentSlug(client, label, excludeDepartmentId) {
|
|
3569
|
+
const baseSlug = this.slugifyValue(label) || `department-${Date.now().toString(36)}`;
|
|
3570
|
+
for (let attempt = 0; attempt < 25; attempt += 1) {
|
|
3571
|
+
const candidate = attempt === 0 ? baseSlug : `${baseSlug}-${attempt + 1}`;
|
|
3572
|
+
const existing = (await client.$queryRawUnsafe(`SELECT id
|
|
3573
|
+
FROM operations_department
|
|
3574
|
+
WHERE slug = $1
|
|
3575
|
+
AND ($2::int IS NULL OR id <> $2)
|
|
3576
|
+
LIMIT 1`, candidate, excludeDepartmentId !== null && excludeDepartmentId !== void 0 ? excludeDepartmentId : null));
|
|
3577
|
+
if (!existing.length) {
|
|
3578
|
+
return candidate;
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
return `${baseSlug}-${Date.now().toString(36)}`;
|
|
2704
3582
|
}
|
|
2705
|
-
async
|
|
3583
|
+
async getJobTitleById(client, jobTitleId, includeInactive = false) {
|
|
2706
3584
|
var _a;
|
|
2707
|
-
const
|
|
3585
|
+
const jobTitles = (await client.$queryRawUnsafe(`SELECT id,
|
|
2708
3586
|
slug,
|
|
2709
3587
|
code,
|
|
2710
3588
|
name,
|
|
@@ -2713,53 +3591,53 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2713
3591
|
updated_at AS "updatedAt",
|
|
2714
3592
|
deleted_at AS "deletedAt",
|
|
2715
3593
|
CASE WHEN deleted_at IS NULL THEN 'active' ELSE 'inactive' END AS status
|
|
2716
|
-
FROM
|
|
3594
|
+
FROM operations_job_title
|
|
2717
3595
|
WHERE id = $1
|
|
2718
|
-
AND ($2::boolean OR deleted_at IS NULL)`,
|
|
2719
|
-
const
|
|
2720
|
-
if (!
|
|
2721
|
-
throw new common_1.NotFoundException('
|
|
3596
|
+
AND ($2::boolean OR deleted_at IS NULL)`, jobTitleId, includeInactive));
|
|
3597
|
+
const jobTitle = (_a = jobTitles[0]) !== null && _a !== void 0 ? _a : null;
|
|
3598
|
+
if (!jobTitle) {
|
|
3599
|
+
throw new common_1.NotFoundException('Job title not found.');
|
|
2722
3600
|
}
|
|
2723
|
-
return
|
|
3601
|
+
return jobTitle;
|
|
2724
3602
|
}
|
|
2725
|
-
async
|
|
3603
|
+
async assertJobTitleNameAvailable(client, name, excludeJobTitleId) {
|
|
2726
3604
|
const existing = (await client.$queryRawUnsafe(`SELECT id
|
|
2727
|
-
FROM
|
|
3605
|
+
FROM operations_job_title
|
|
2728
3606
|
WHERE LOWER(name) = LOWER($1)
|
|
2729
3607
|
AND ($2::int IS NULL OR id <> $2)
|
|
2730
|
-
LIMIT 1`, name,
|
|
3608
|
+
LIMIT 1`, name, excludeJobTitleId !== null && excludeJobTitleId !== void 0 ? excludeJobTitleId : null));
|
|
2731
3609
|
if (existing[0]) {
|
|
2732
|
-
throw new common_1.BadRequestException('A
|
|
3610
|
+
throw new common_1.BadRequestException('A job title with this name already exists.');
|
|
2733
3611
|
}
|
|
2734
3612
|
}
|
|
2735
|
-
async
|
|
3613
|
+
async assertJobTitleCodeAvailable(client, code, excludeJobTitleId) {
|
|
2736
3614
|
const existing = (await client.$queryRawUnsafe(`SELECT id
|
|
2737
|
-
FROM
|
|
3615
|
+
FROM operations_job_title
|
|
2738
3616
|
WHERE UPPER(COALESCE(code, '')) = UPPER($1)
|
|
2739
3617
|
AND ($2::int IS NULL OR id <> $2)
|
|
2740
|
-
LIMIT 1`, code,
|
|
3618
|
+
LIMIT 1`, code, excludeJobTitleId !== null && excludeJobTitleId !== void 0 ? excludeJobTitleId : null));
|
|
2741
3619
|
if (existing[0]) {
|
|
2742
|
-
throw new common_1.BadRequestException('A
|
|
3620
|
+
throw new common_1.BadRequestException('A job title with this code already exists.');
|
|
2743
3621
|
}
|
|
2744
3622
|
}
|
|
2745
|
-
async
|
|
3623
|
+
async resolveJobTitleReference(client, input) {
|
|
2746
3624
|
var _a;
|
|
2747
|
-
if (input.
|
|
2748
|
-
const
|
|
2749
|
-
if (!
|
|
3625
|
+
if (input.jobTitleName !== undefined) {
|
|
3626
|
+
const normalizedJobTitleName = this.normalizeOptionalText(input.jobTitleName);
|
|
3627
|
+
if (!normalizedJobTitleName) {
|
|
2750
3628
|
return null;
|
|
2751
3629
|
}
|
|
2752
3630
|
const existingByName = (await client.$queryRawUnsafe(`SELECT id, name
|
|
2753
|
-
FROM
|
|
3631
|
+
FROM operations_job_title
|
|
2754
3632
|
WHERE deleted_at IS NULL
|
|
2755
3633
|
AND LOWER(name) = LOWER($1)
|
|
2756
3634
|
ORDER BY id ASC
|
|
2757
|
-
LIMIT 1`,
|
|
3635
|
+
LIMIT 1`, normalizedJobTitleName));
|
|
2758
3636
|
if (existingByName[0]) {
|
|
2759
3637
|
return existingByName[0];
|
|
2760
3638
|
}
|
|
2761
|
-
const slug = await this.
|
|
2762
|
-
const created = (await client.$queryRawUnsafe(`INSERT INTO
|
|
3639
|
+
const slug = await this.generateUniqueJobTitleSlug(client, normalizedJobTitleName);
|
|
3640
|
+
const created = (await client.$queryRawUnsafe(`INSERT INTO operations_job_title (
|
|
2763
3641
|
slug,
|
|
2764
3642
|
code,
|
|
2765
3643
|
name,
|
|
@@ -2767,31 +3645,205 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2767
3645
|
created_at,
|
|
2768
3646
|
updated_at
|
|
2769
3647
|
) VALUES ($1, NULL, $2, NULL, NOW(), NOW())
|
|
2770
|
-
RETURNING id, name`, slug,
|
|
3648
|
+
RETURNING id, name`, slug, normalizedJobTitleName));
|
|
2771
3649
|
return (_a = created[0]) !== null && _a !== void 0 ? _a : null;
|
|
2772
3650
|
}
|
|
2773
|
-
if (typeof input.
|
|
2774
|
-
Number.isInteger(input.
|
|
2775
|
-
input.
|
|
2776
|
-
return this.
|
|
3651
|
+
if (typeof input.jobTitleId === 'number' &&
|
|
3652
|
+
Number.isInteger(input.jobTitleId) &&
|
|
3653
|
+
input.jobTitleId > 0) {
|
|
3654
|
+
return this.getJobTitleById(client, input.jobTitleId);
|
|
2777
3655
|
}
|
|
2778
3656
|
return null;
|
|
2779
3657
|
}
|
|
2780
|
-
async
|
|
2781
|
-
const baseSlug = this.slugifyValue(label) || `
|
|
3658
|
+
async generateUniqueJobTitleSlug(client, label, excludeJobTitleId) {
|
|
3659
|
+
const baseSlug = this.slugifyValue(label) || `job-title-${Date.now().toString(36)}`;
|
|
2782
3660
|
for (let attempt = 0; attempt < 25; attempt += 1) {
|
|
2783
3661
|
const candidate = attempt === 0 ? baseSlug : `${baseSlug}-${attempt + 1}`;
|
|
2784
3662
|
const existing = (await client.$queryRawUnsafe(`SELECT id
|
|
2785
|
-
FROM
|
|
3663
|
+
FROM operations_job_title
|
|
2786
3664
|
WHERE slug = $1
|
|
2787
3665
|
AND ($2::int IS NULL OR id <> $2)
|
|
2788
|
-
LIMIT 1`, candidate,
|
|
3666
|
+
LIMIT 1`, candidate, excludeJobTitleId !== null && excludeJobTitleId !== void 0 ? excludeJobTitleId : null));
|
|
3667
|
+
if (!existing.length) {
|
|
3668
|
+
return candidate;
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
return `${baseSlug}-${Date.now().toString(36)}`;
|
|
3672
|
+
}
|
|
3673
|
+
async getCollaboratorTypeById(client, collaboratorTypeId, includeInactive = false) {
|
|
3674
|
+
var _a;
|
|
3675
|
+
const collaboratorTypes = (await client.$queryRawUnsafe(`SELECT ct.id,
|
|
3676
|
+
ct.slug,
|
|
3677
|
+
ct.name,
|
|
3678
|
+
ct.description,
|
|
3679
|
+
ct.category,
|
|
3680
|
+
ct.is_active AS "isActive",
|
|
3681
|
+
ct.sort_order AS "sortOrder",
|
|
3682
|
+
CASE
|
|
3683
|
+
WHEN ct.deleted_at IS NULL AND ct.is_active THEN 'active'
|
|
3684
|
+
ELSE 'inactive'
|
|
3685
|
+
END AS status,
|
|
3686
|
+
COUNT(DISTINCT c.id)::int AS "collaboratorCount",
|
|
3687
|
+
ct.created_at AS "createdAt",
|
|
3688
|
+
ct.updated_at AS "updatedAt",
|
|
3689
|
+
ct.deleted_at AS "deletedAt"
|
|
3690
|
+
FROM operations_collaborator_type ct
|
|
3691
|
+
LEFT JOIN operations_collaborator c
|
|
3692
|
+
ON c.deleted_at IS NULL
|
|
3693
|
+
AND c.collaborator_type_id = ct.id
|
|
3694
|
+
WHERE ct.id = $1
|
|
3695
|
+
AND ($2::boolean OR ct.deleted_at IS NULL)
|
|
3696
|
+
GROUP BY ct.id
|
|
3697
|
+
LIMIT 1`, collaboratorTypeId, includeInactive));
|
|
3698
|
+
const collaboratorType = (_a = collaboratorTypes[0]) !== null && _a !== void 0 ? _a : null;
|
|
3699
|
+
if (!collaboratorType) {
|
|
3700
|
+
throw new common_1.NotFoundException('Collaborator type not found.');
|
|
3701
|
+
}
|
|
3702
|
+
return collaboratorType;
|
|
3703
|
+
}
|
|
3704
|
+
async assertCollaboratorTypeNameAvailable(client, name, excludeCollaboratorTypeId) {
|
|
3705
|
+
const existing = (await client.$queryRawUnsafe(`SELECT id
|
|
3706
|
+
FROM operations_collaborator_type
|
|
3707
|
+
WHERE LOWER(name) = LOWER($1)
|
|
3708
|
+
AND ($2::int IS NULL OR id <> $2)
|
|
3709
|
+
LIMIT 1`, name, excludeCollaboratorTypeId !== null && excludeCollaboratorTypeId !== void 0 ? excludeCollaboratorTypeId : null));
|
|
3710
|
+
if (existing[0]) {
|
|
3711
|
+
throw new common_1.BadRequestException('A collaborator type with this name already exists.');
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
async assertCollaboratorTypeSlugAvailable(client, slug, excludeCollaboratorTypeId) {
|
|
3715
|
+
const existing = (await client.$queryRawUnsafe(`SELECT id
|
|
3716
|
+
FROM operations_collaborator_type
|
|
3717
|
+
WHERE LOWER(slug) = LOWER($1)
|
|
3718
|
+
AND ($2::int IS NULL OR id <> $2)
|
|
3719
|
+
LIMIT 1`, slug, excludeCollaboratorTypeId !== null && excludeCollaboratorTypeId !== void 0 ? excludeCollaboratorTypeId : null));
|
|
3720
|
+
if (existing[0]) {
|
|
3721
|
+
throw new common_1.BadRequestException('A collaborator type with this slug already exists.');
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
async buildCollaboratorTypeSlug(client, name, slugInput, excludeCollaboratorTypeId) {
|
|
3725
|
+
var _a;
|
|
3726
|
+
const normalizedSlugInput = this.slugifyValue((_a = this.normalizeOptionalText(slugInput)) !== null && _a !== void 0 ? _a : '');
|
|
3727
|
+
if (normalizedSlugInput) {
|
|
3728
|
+
await this.assertCollaboratorTypeSlugAvailable(client, normalizedSlugInput, excludeCollaboratorTypeId);
|
|
3729
|
+
return normalizedSlugInput;
|
|
3730
|
+
}
|
|
3731
|
+
return this.generateUniqueCollaboratorTypeSlug(client, name, excludeCollaboratorTypeId);
|
|
3732
|
+
}
|
|
3733
|
+
async generateUniqueCollaboratorTypeSlug(client, label, excludeCollaboratorTypeId) {
|
|
3734
|
+
const baseSlug = this.slugifyValue(label) || `collaborator-type-${Date.now().toString(36)}`;
|
|
3735
|
+
for (let attempt = 0; attempt < 25; attempt += 1) {
|
|
3736
|
+
const candidate = attempt === 0 ? baseSlug : `${baseSlug}-${attempt + 1}`;
|
|
3737
|
+
const existing = (await client.$queryRawUnsafe(`SELECT id
|
|
3738
|
+
FROM operations_collaborator_type
|
|
3739
|
+
WHERE slug = $1
|
|
3740
|
+
AND ($2::int IS NULL OR id <> $2)
|
|
3741
|
+
LIMIT 1`, candidate, excludeCollaboratorTypeId !== null && excludeCollaboratorTypeId !== void 0 ? excludeCollaboratorTypeId : null));
|
|
3742
|
+
if (!existing.length) {
|
|
3743
|
+
return candidate;
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
return `${baseSlug}-${Date.now().toString(36)}`;
|
|
3747
|
+
}
|
|
3748
|
+
async getProjectRoleById(client, projectRoleId, includeInactive = false) {
|
|
3749
|
+
var _a;
|
|
3750
|
+
const localeId = await this.resolvePreferredLocaleId(client);
|
|
3751
|
+
const projectRoles = (await client.$queryRawUnsafe(`SELECT pr.id,
|
|
3752
|
+
pr.slug,
|
|
3753
|
+
pr.code,
|
|
3754
|
+
COALESCE(prl.name, pr.code, pr.slug) AS name,
|
|
3755
|
+
prl.description,
|
|
3756
|
+
pr.is_active AS "isActive",
|
|
3757
|
+
pr.sort_order AS "sortOrder",
|
|
3758
|
+
pr.created_at AS "createdAt",
|
|
3759
|
+
pr.updated_at AS "updatedAt",
|
|
3760
|
+
pr.deleted_at AS "deletedAt"
|
|
3761
|
+
FROM operations_project_role pr
|
|
3762
|
+
LEFT JOIN LATERAL (
|
|
3763
|
+
SELECT pr_locale.name,
|
|
3764
|
+
pr_locale.description
|
|
3765
|
+
FROM operations_project_role_locale pr_locale
|
|
3766
|
+
WHERE pr_locale.operations_project_role_id = pr.id
|
|
3767
|
+
ORDER BY CASE
|
|
3768
|
+
WHEN $3::int IS NOT NULL AND pr_locale.locale_id = $3 THEN 0
|
|
3769
|
+
ELSE 1
|
|
3770
|
+
END ASC,
|
|
3771
|
+
pr_locale.id ASC
|
|
3772
|
+
LIMIT 1
|
|
3773
|
+
) prl ON TRUE
|
|
3774
|
+
WHERE pr.id = $1
|
|
3775
|
+
AND ($2::boolean OR pr.deleted_at IS NULL)`, projectRoleId, includeInactive, localeId));
|
|
3776
|
+
const projectRole = (_a = projectRoles[0]) !== null && _a !== void 0 ? _a : null;
|
|
3777
|
+
if (!projectRole) {
|
|
3778
|
+
throw new common_1.NotFoundException('Project role not found.');
|
|
3779
|
+
}
|
|
3780
|
+
return projectRole;
|
|
3781
|
+
}
|
|
3782
|
+
async assertProjectRoleNameAvailable(client, name, excludeProjectRoleId) {
|
|
3783
|
+
const existing = (await client.$queryRawUnsafe(`SELECT pr.id
|
|
3784
|
+
FROM operations_project_role pr
|
|
3785
|
+
JOIN operations_project_role_locale prl
|
|
3786
|
+
ON prl.operations_project_role_id = pr.id
|
|
3787
|
+
WHERE LOWER(prl.name) = LOWER($1)
|
|
3788
|
+
AND pr.deleted_at IS NULL
|
|
3789
|
+
AND ($2::int IS NULL OR pr.id <> $2)
|
|
3790
|
+
LIMIT 1`, name, excludeProjectRoleId !== null && excludeProjectRoleId !== void 0 ? excludeProjectRoleId : null));
|
|
3791
|
+
if (existing[0]) {
|
|
3792
|
+
throw new common_1.BadRequestException('A project role with this name already exists.');
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
async assertProjectRoleCodeAvailable(client, code, excludeProjectRoleId) {
|
|
3796
|
+
const existing = (await client.$queryRawUnsafe(`SELECT id
|
|
3797
|
+
FROM operations_project_role
|
|
3798
|
+
WHERE UPPER(COALESCE(code, '')) = UPPER($1)
|
|
3799
|
+
AND ($2::int IS NULL OR id <> $2)
|
|
3800
|
+
LIMIT 1`, code, excludeProjectRoleId !== null && excludeProjectRoleId !== void 0 ? excludeProjectRoleId : null));
|
|
3801
|
+
if (existing[0]) {
|
|
3802
|
+
throw new common_1.BadRequestException('A project role with this code already exists.');
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
async generateUniqueProjectRoleSlug(client, label, excludeProjectRoleId) {
|
|
3806
|
+
const baseSlug = this.slugifyValue(label) || `project-role-${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_project_role
|
|
3811
|
+
WHERE slug = $1
|
|
3812
|
+
AND ($2::int IS NULL OR id <> $2)
|
|
3813
|
+
LIMIT 1`, candidate, excludeProjectRoleId !== null && excludeProjectRoleId !== void 0 ? excludeProjectRoleId : null));
|
|
2789
3814
|
if (!existing.length) {
|
|
2790
3815
|
return candidate;
|
|
2791
3816
|
}
|
|
2792
3817
|
}
|
|
2793
3818
|
return `${baseSlug}-${Date.now().toString(36)}`;
|
|
2794
3819
|
}
|
|
3820
|
+
async resolveCollaboratorTypeReference(client, input) {
|
|
3821
|
+
var _a, _b;
|
|
3822
|
+
if (typeof input.collaboratorTypeId === 'number' &&
|
|
3823
|
+
Number.isInteger(input.collaboratorTypeId) &&
|
|
3824
|
+
input.collaboratorTypeId > 0) {
|
|
3825
|
+
const collaboratorTypes = (await client.$queryRawUnsafe(`SELECT id, slug, name
|
|
3826
|
+
FROM operations_collaborator_type
|
|
3827
|
+
WHERE id = $1
|
|
3828
|
+
AND deleted_at IS NULL
|
|
3829
|
+
LIMIT 1`, input.collaboratorTypeId));
|
|
3830
|
+
return (_a = collaboratorTypes[0]) !== null && _a !== void 0 ? _a : null;
|
|
3831
|
+
}
|
|
3832
|
+
const normalizedLookup = this.normalizeOptionalText(input.collaboratorTypeSlug);
|
|
3833
|
+
if (!normalizedLookup) {
|
|
3834
|
+
return null;
|
|
3835
|
+
}
|
|
3836
|
+
const collaboratorTypes = (await client.$queryRawUnsafe(`SELECT id, slug, name
|
|
3837
|
+
FROM operations_collaborator_type
|
|
3838
|
+
WHERE deleted_at IS NULL
|
|
3839
|
+
AND (
|
|
3840
|
+
LOWER(slug) = LOWER($1)
|
|
3841
|
+
OR LOWER(name) = LOWER($1)
|
|
3842
|
+
)
|
|
3843
|
+
ORDER BY id ASC
|
|
3844
|
+
LIMIT 1`, normalizedLookup));
|
|
3845
|
+
return (_b = collaboratorTypes[0]) !== null && _b !== void 0 ? _b : null;
|
|
3846
|
+
}
|
|
2795
3847
|
async getContractTemplateRecord(client, templateId, includeInactive = false) {
|
|
2796
3848
|
const template = (await client.$queryRawUnsafe(`SELECT t.id,
|
|
2797
3849
|
t.slug,
|
|
@@ -2893,7 +3945,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2893
3945
|
c.contract_category AS "contractCategory",
|
|
2894
3946
|
m.display_name AS "managerName",
|
|
2895
3947
|
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",
|
|
3948
|
+
MAX(CASE WHEN pa.collaborator_id = $2 THEN COALESCE(project_role_locale.name, pa.role_label) END) AS "myRoleLabel",
|
|
2897
3949
|
COUNT(DISTINCT pa.id)::int AS "teamSize"
|
|
2898
3950
|
FROM operations_project p
|
|
2899
3951
|
LEFT JOIN operations_contract c ON c.id = p.contract_id
|
|
@@ -2901,6 +3953,16 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2901
3953
|
LEFT JOIN operations_project_assignment pa
|
|
2902
3954
|
ON pa.project_id = p.id
|
|
2903
3955
|
AND pa.deleted_at IS NULL
|
|
3956
|
+
LEFT JOIN operations_project_role project_role
|
|
3957
|
+
ON project_role.id = pa.project_role_id
|
|
3958
|
+
AND project_role.deleted_at IS NULL
|
|
3959
|
+
LEFT JOIN LATERAL (
|
|
3960
|
+
SELECT prl.name
|
|
3961
|
+
FROM operations_project_role_locale prl
|
|
3962
|
+
WHERE prl.operations_project_role_id = project_role.id
|
|
3963
|
+
ORDER BY prl.id ASC
|
|
3964
|
+
LIMIT 1
|
|
3965
|
+
) project_role_locale ON TRUE
|
|
2904
3966
|
WHERE p.id = $1
|
|
2905
3967
|
AND p.deleted_at IS NULL
|
|
2906
3968
|
GROUP BY p.id, c.id, m.id`, [projectId, actorCollaboratorId !== null && actorCollaboratorId !== void 0 ? actorCollaboratorId : null]);
|
|
@@ -2911,7 +3973,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2911
3973
|
this.queryRows(`SELECT pa.id,
|
|
2912
3974
|
pa.collaborator_id AS "collaboratorId",
|
|
2913
3975
|
c.display_name AS "collaboratorName",
|
|
2914
|
-
pa.
|
|
3976
|
+
pa.project_role_id AS "projectRoleId",
|
|
3977
|
+
COALESCE(project_role_locale.name, pa.role_label) AS "roleLabel",
|
|
2915
3978
|
pa.allocation_percent AS "allocationPercent",
|
|
2916
3979
|
pa.weekly_hours AS "weeklyHours",
|
|
2917
3980
|
pa.is_billable AS "isBillable",
|
|
@@ -2920,6 +3983,16 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2920
3983
|
pa.status
|
|
2921
3984
|
FROM operations_project_assignment pa
|
|
2922
3985
|
JOIN operations_collaborator c ON c.id = pa.collaborator_id
|
|
3986
|
+
LEFT JOIN operations_project_role project_role
|
|
3987
|
+
ON project_role.id = pa.project_role_id
|
|
3988
|
+
AND project_role.deleted_at IS NULL
|
|
3989
|
+
LEFT JOIN LATERAL (
|
|
3990
|
+
SELECT prl.name
|
|
3991
|
+
FROM operations_project_role_locale prl
|
|
3992
|
+
WHERE prl.operations_project_role_id = project_role.id
|
|
3993
|
+
ORDER BY prl.id ASC
|
|
3994
|
+
LIMIT 1
|
|
3995
|
+
) project_role_locale ON TRUE
|
|
2923
3996
|
WHERE pa.project_id = $1
|
|
2924
3997
|
AND pa.deleted_at IS NULL
|
|
2925
3998
|
ORDER BY c.display_name ASC`, [projectId]),
|
|
@@ -2982,11 +4055,14 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2982
4055
|
person_record.name AS "personName",
|
|
2983
4056
|
person_record.avatar_id AS "personAvatarId",
|
|
2984
4057
|
c.code,
|
|
2985
|
-
c.
|
|
4058
|
+
c.collaborator_type_id AS "collaboratorTypeId",
|
|
4059
|
+
collaborator_type.slug AS "collaboratorTypeSlug",
|
|
4060
|
+
collaborator_type.name AS "collaboratorType",
|
|
2986
4061
|
COALESCE(NULLIF(c.display_name, ''), person_record.name) AS "displayName",
|
|
2987
4062
|
c.department_id AS "departmentId",
|
|
2988
4063
|
COALESCE(NULLIF(department_record.name, ''), NULLIF(c.department, '')) AS "department",
|
|
2989
|
-
c.
|
|
4064
|
+
c.job_title_id AS "jobTitleId",
|
|
4065
|
+
COALESCE(NULLIF(job_title_record.name, ''), NULLIF(c.title, '')) AS "title",
|
|
2990
4066
|
c.level_label AS "levelLabel",
|
|
2991
4067
|
c.weekly_capacity_hours AS "weeklyCapacityHours",
|
|
2992
4068
|
c.status,
|
|
@@ -2997,13 +4073,20 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2997
4073
|
s.display_name AS "supervisorName",
|
|
2998
4074
|
hiring_contract.id AS "contractId",
|
|
2999
4075
|
hiring_contract.status AS "contractStatus",
|
|
4076
|
+
hiring_contract.budget_amount AS "compensationAmount",
|
|
3000
4077
|
COUNT(DISTINCT pa.id)::int AS "activeAssignments"
|
|
3001
4078
|
FROM operations_collaborator c
|
|
3002
4079
|
LEFT JOIN person person_record
|
|
3003
4080
|
ON person_record.id = c.person_id
|
|
4081
|
+
LEFT JOIN operations_collaborator_type collaborator_type
|
|
4082
|
+
ON collaborator_type.id = c.collaborator_type_id
|
|
4083
|
+
AND collaborator_type.deleted_at IS NULL
|
|
3004
4084
|
LEFT JOIN operations_department department_record
|
|
3005
4085
|
ON department_record.id = c.department_id
|
|
3006
4086
|
AND department_record.deleted_at IS NULL
|
|
4087
|
+
LEFT JOIN operations_job_title job_title_record
|
|
4088
|
+
ON job_title_record.id = c.job_title_id
|
|
4089
|
+
AND job_title_record.deleted_at IS NULL
|
|
3007
4090
|
LEFT JOIN operations_collaborator s
|
|
3008
4091
|
ON s.id = c.supervisor_collaborator_id
|
|
3009
4092
|
LEFT JOIN operations_project_assignment pa
|
|
@@ -3011,7 +4094,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3011
4094
|
AND pa.deleted_at IS NULL
|
|
3012
4095
|
AND pa.status IN ('planned', 'active')
|
|
3013
4096
|
LEFT JOIN LATERAL (
|
|
3014
|
-
SELECT oc.id, oc.status
|
|
4097
|
+
SELECT oc.id, oc.status, oc.budget_amount
|
|
3015
4098
|
FROM operations_contract oc
|
|
3016
4099
|
WHERE oc.related_collaborator_id = c.id
|
|
3017
4100
|
AND oc.deleted_at IS NULL
|
|
@@ -3021,22 +4104,33 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3021
4104
|
) hiring_contract ON TRUE
|
|
3022
4105
|
WHERE c.id = $1
|
|
3023
4106
|
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]);
|
|
4107
|
+
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
4108
|
if (!collaborator) {
|
|
3026
4109
|
throw new common_1.NotFoundException('Collaborator not found.');
|
|
3027
4110
|
}
|
|
3028
|
-
const [assignedProjects, relatedContracts, weeklySchedule, timesheetSummary, timeOffSummary, scheduleAdjustmentRequests] = await Promise.all([
|
|
4111
|
+
const [assignedProjects, relatedContracts, weeklySchedule, equityParticipation, timesheetSummary, timeOffSummary, scheduleAdjustmentRequests] = await Promise.all([
|
|
3029
4112
|
this.queryRows(`SELECT p.id,
|
|
3030
4113
|
p.code,
|
|
3031
4114
|
p.name,
|
|
3032
4115
|
p.status,
|
|
3033
|
-
pa.
|
|
4116
|
+
pa.project_role_id AS "projectRoleId",
|
|
4117
|
+
COALESCE(project_role_locale.name, pa.role_label) AS "roleLabel",
|
|
3034
4118
|
pa.allocation_percent AS "allocationPercent",
|
|
3035
4119
|
pa.weekly_hours AS "weeklyHours",
|
|
3036
4120
|
pa.start_date AS "startDate",
|
|
3037
4121
|
pa.end_date AS "endDate"
|
|
3038
4122
|
FROM operations_project_assignment pa
|
|
3039
4123
|
JOIN operations_project p ON p.id = pa.project_id
|
|
4124
|
+
LEFT JOIN operations_project_role project_role
|
|
4125
|
+
ON project_role.id = pa.project_role_id
|
|
4126
|
+
AND project_role.deleted_at IS NULL
|
|
4127
|
+
LEFT JOIN LATERAL (
|
|
4128
|
+
SELECT prl.name
|
|
4129
|
+
FROM operations_project_role_locale prl
|
|
4130
|
+
WHERE prl.operations_project_role_id = project_role.id
|
|
4131
|
+
ORDER BY prl.id ASC
|
|
4132
|
+
LIMIT 1
|
|
4133
|
+
) project_role_locale ON TRUE
|
|
3040
4134
|
WHERE pa.collaborator_id = $1
|
|
3041
4135
|
AND pa.deleted_at IS NULL
|
|
3042
4136
|
AND p.deleted_at IS NULL
|
|
@@ -3068,6 +4162,17 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3068
4162
|
WHERE collaborator_id = $1
|
|
3069
4163
|
AND deleted_at IS NULL
|
|
3070
4164
|
ORDER BY id ASC`, [collaboratorId]),
|
|
4165
|
+
this.querySingle(`SELECT participation_type AS "participationType",
|
|
4166
|
+
percentage,
|
|
4167
|
+
voting_power AS "votingPower",
|
|
4168
|
+
start_date AS "startDate",
|
|
4169
|
+
end_date AS "endDate",
|
|
4170
|
+
notes
|
|
4171
|
+
FROM operations_collaborator_equity_participation
|
|
4172
|
+
WHERE collaborator_id = $1
|
|
4173
|
+
AND deleted_at IS NULL
|
|
4174
|
+
ORDER BY updated_at DESC, id DESC
|
|
4175
|
+
LIMIT 1`, [collaboratorId]),
|
|
3071
4176
|
this.querySingle(`SELECT COUNT(*)::text AS "totalTimesheets",
|
|
3072
4177
|
COUNT(*) FILTER (WHERE status = 'submitted')::text AS "pendingTimesheets",
|
|
3073
4178
|
COALESCE(SUM(total_hours), 0)::text AS "totalHours"
|
|
@@ -3093,7 +4198,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3093
4198
|
]);
|
|
3094
4199
|
return Object.assign(Object.assign({}, collaborator), { assignedProjects,
|
|
3095
4200
|
relatedContracts,
|
|
3096
|
-
weeklySchedule,
|
|
4201
|
+
weeklySchedule,
|
|
4202
|
+
equityParticipation, timesheetSummary: {
|
|
3097
4203
|
totalTimesheets: Number((_a = timesheetSummary === null || timesheetSummary === void 0 ? void 0 : timesheetSummary.totalTimesheets) !== null && _a !== void 0 ? _a : 0),
|
|
3098
4204
|
pendingTimesheets: Number((_b = timesheetSummary === null || timesheetSummary === void 0 ? void 0 : timesheetSummary.pendingTimesheets) !== null && _b !== void 0 ? _b : 0),
|
|
3099
4205
|
totalHours: Number((_c = timesheetSummary === null || timesheetSummary === void 0 ? void 0 : timesheetSummary.totalHours) !== null && _c !== void 0 ? _c : 0),
|
|
@@ -3119,6 +4225,156 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3119
4225
|
AND status IN ('planned', 'active')
|
|
3120
4226
|
AND collaborator_id = ANY($1::int[])`, [collaboratorIds])).map((row) => row.projectId);
|
|
3121
4227
|
}
|
|
4228
|
+
async resolveOwnedProjectAssignment(client, collaboratorId, input) {
|
|
4229
|
+
if (!input.projectId && !input.projectAssignmentId) {
|
|
4230
|
+
throw new common_1.BadRequestException('Either projectId or projectAssignmentId is required.');
|
|
4231
|
+
}
|
|
4232
|
+
const params = [collaboratorId];
|
|
4233
|
+
const filters = [
|
|
4234
|
+
'pa.deleted_at IS NULL',
|
|
4235
|
+
'p.deleted_at IS NULL',
|
|
4236
|
+
`pa.collaborator_id = $1`,
|
|
4237
|
+
`pa.status IN ('planned', 'active')`,
|
|
4238
|
+
];
|
|
4239
|
+
if (input.projectAssignmentId) {
|
|
4240
|
+
filters.push(`pa.id = ${this.param(params, input.projectAssignmentId)}`);
|
|
4241
|
+
}
|
|
4242
|
+
if (input.projectId) {
|
|
4243
|
+
filters.push(`pa.project_id = ${this.param(params, input.projectId)}`);
|
|
4244
|
+
}
|
|
4245
|
+
const assignment = (await client.$queryRawUnsafe(`SELECT pa.id,
|
|
4246
|
+
pa.project_id AS "projectId",
|
|
4247
|
+
p.name AS "projectName",
|
|
4248
|
+
p.code AS "projectCode",
|
|
4249
|
+
pa.role_label AS "roleLabel"
|
|
4250
|
+
FROM operations_project_assignment pa
|
|
4251
|
+
JOIN operations_project p
|
|
4252
|
+
ON p.id = pa.project_id
|
|
4253
|
+
WHERE ${filters.join(' AND ')}
|
|
4254
|
+
ORDER BY CASE WHEN pa.status = 'active' THEN 0 ELSE 1 END,
|
|
4255
|
+
pa.start_date DESC NULLS LAST,
|
|
4256
|
+
pa.id DESC
|
|
4257
|
+
LIMIT 1`, ...params));
|
|
4258
|
+
if (!assignment[0]) {
|
|
4259
|
+
throw new common_1.ForbiddenException('The selected project is not assigned to the authenticated collaborator.');
|
|
4260
|
+
}
|
|
4261
|
+
return assignment[0];
|
|
4262
|
+
}
|
|
4263
|
+
async getOwnedTaskRecord(client, collaboratorId, taskId) {
|
|
4264
|
+
const task = (await client.$queryRawUnsafe(`SELECT t.id,
|
|
4265
|
+
t.name,
|
|
4266
|
+
t.description,
|
|
4267
|
+
t.status,
|
|
4268
|
+
t.project_assignment_id AS "projectAssignmentId",
|
|
4269
|
+
pa.project_id AS "projectId",
|
|
4270
|
+
p.name AS "projectName",
|
|
4271
|
+
p.code AS "projectCode"
|
|
4272
|
+
FROM operations_task t
|
|
4273
|
+
JOIN operations_project_assignment pa
|
|
4274
|
+
ON pa.id = t.project_assignment_id
|
|
4275
|
+
AND pa.deleted_at IS NULL
|
|
4276
|
+
JOIN operations_project p
|
|
4277
|
+
ON p.id = pa.project_id
|
|
4278
|
+
AND p.deleted_at IS NULL
|
|
4279
|
+
WHERE t.id = $1
|
|
4280
|
+
AND t.deleted_at IS NULL
|
|
4281
|
+
AND pa.collaborator_id = $2
|
|
4282
|
+
LIMIT 1`, taskId, collaboratorId));
|
|
4283
|
+
if (!task[0]) {
|
|
4284
|
+
throw new common_1.ForbiddenException('The selected task is not assigned to the authenticated collaborator.');
|
|
4285
|
+
}
|
|
4286
|
+
return task[0];
|
|
4287
|
+
}
|
|
4288
|
+
async getTaskOptionById(collaboratorId, taskId) {
|
|
4289
|
+
const task = await this.getOwnedTaskRecord(this.prisma, collaboratorId, taskId);
|
|
4290
|
+
return Object.assign(Object.assign({}, task), { label: [task.name, task.projectName].filter(Boolean).join(' • ') });
|
|
4291
|
+
}
|
|
4292
|
+
async getOrCreateTimesheetForWorkDate(client, collaboratorId, workDate) {
|
|
4293
|
+
var _a, _b, _c;
|
|
4294
|
+
const normalizedWorkDate = this.normalizeDateOnly(workDate);
|
|
4295
|
+
const { weekStartDate, weekEndDate } = this.getWorkWeekRange(normalizedWorkDate);
|
|
4296
|
+
const existing = (await client.$queryRawUnsafe(`SELECT id, status
|
|
4297
|
+
FROM operations_timesheet
|
|
4298
|
+
WHERE collaborator_id = $1
|
|
4299
|
+
AND week_start_date = $2::date
|
|
4300
|
+
AND week_end_date = $3::date
|
|
4301
|
+
AND deleted_at IS NULL
|
|
4302
|
+
ORDER BY id DESC
|
|
4303
|
+
LIMIT 1`, collaboratorId, weekStartDate, weekEndDate));
|
|
4304
|
+
if (existing[0]) {
|
|
4305
|
+
if (!['draft', 'rejected'].includes(existing[0].status)) {
|
|
4306
|
+
throw new common_1.BadRequestException('The timesheet for this week has already been submitted or approved.');
|
|
4307
|
+
}
|
|
4308
|
+
return existing[0].id;
|
|
4309
|
+
}
|
|
4310
|
+
const collaborator = await this.getCollaboratorById(collaboratorId);
|
|
4311
|
+
const created = (await client.$queryRawUnsafe(`INSERT INTO operations_timesheet (
|
|
4312
|
+
collaborator_id,
|
|
4313
|
+
approver_collaborator_id,
|
|
4314
|
+
week_start_date,
|
|
4315
|
+
week_end_date,
|
|
4316
|
+
notes,
|
|
4317
|
+
status,
|
|
4318
|
+
created_at,
|
|
4319
|
+
updated_at
|
|
4320
|
+
) VALUES (
|
|
4321
|
+
$1,
|
|
4322
|
+
$2,
|
|
4323
|
+
$3::date,
|
|
4324
|
+
$4::date,
|
|
4325
|
+
NULL,
|
|
4326
|
+
'draft',
|
|
4327
|
+
NOW(),
|
|
4328
|
+
NOW()
|
|
4329
|
+
)
|
|
4330
|
+
RETURNING id`, collaboratorId, (_a = collaborator.supervisorId) !== null && _a !== void 0 ? _a : null, weekStartDate, weekEndDate));
|
|
4331
|
+
return (_c = (_b = created[0]) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : 0;
|
|
4332
|
+
}
|
|
4333
|
+
async getTimesheetEntryByIdForActor(actor, entryId) {
|
|
4334
|
+
const filter = this.buildIdFilter(actor.visibleCollaboratorIds, 't.collaborator_id', actor.isDirector);
|
|
4335
|
+
const params = [...filter.params, entryId];
|
|
4336
|
+
const entry = await this.querySingle(`SELECT e.id,
|
|
4337
|
+
e.timesheet_id AS "timesheetId",
|
|
4338
|
+
t.collaborator_id AS "collaboratorId",
|
|
4339
|
+
pa.project_id AS "projectId",
|
|
4340
|
+
e.project_assignment_id AS "projectAssignmentId",
|
|
4341
|
+
p.code AS "projectCode",
|
|
4342
|
+
p.name AS "projectName",
|
|
4343
|
+
e.task_id AS "taskId",
|
|
4344
|
+
COALESCE(task_record.name, e.activity_label) AS "taskName",
|
|
4345
|
+
e.activity_label AS "activityLabel",
|
|
4346
|
+
e.work_date AS "workDate",
|
|
4347
|
+
COALESCE(NULLIF(e.duration_minutes, 0), ROUND(COALESCE(e.hours, 0)::numeric * 60))::int AS "durationMinutes",
|
|
4348
|
+
COALESCE(
|
|
4349
|
+
e.hours,
|
|
4350
|
+
ROUND((COALESCE(NULLIF(e.duration_minutes, 0), 0)::numeric / 60), 2)
|
|
4351
|
+
) AS hours,
|
|
4352
|
+
e.description,
|
|
4353
|
+
t.status,
|
|
4354
|
+
t.week_start_date AS "weekStartDate",
|
|
4355
|
+
t.week_end_date AS "weekEndDate",
|
|
4356
|
+
e.created_at AS "createdAt"
|
|
4357
|
+
FROM operations_timesheet_entry e
|
|
4358
|
+
JOIN operations_timesheet t
|
|
4359
|
+
ON t.id = e.timesheet_id
|
|
4360
|
+
LEFT JOIN operations_project_assignment pa
|
|
4361
|
+
ON pa.id = e.project_assignment_id
|
|
4362
|
+
LEFT JOIN operations_project p
|
|
4363
|
+
ON p.id = pa.project_id
|
|
4364
|
+
LEFT JOIN operations_task task_record
|
|
4365
|
+
ON task_record.id = e.task_id
|
|
4366
|
+
AND task_record.deleted_at IS NULL
|
|
4367
|
+
WHERE e.deleted_at IS NULL
|
|
4368
|
+
AND ${filter.clause}
|
|
4369
|
+
AND e.id = $${params.length}
|
|
4370
|
+
LIMIT 1`, params);
|
|
4371
|
+
if (!entry) {
|
|
4372
|
+
throw new common_1.NotFoundException('Timesheet entry not found.');
|
|
4373
|
+
}
|
|
4374
|
+
return Object.assign(Object.assign({}, entry), { label: [entry.projectCode, entry.projectName, entry.taskName]
|
|
4375
|
+
.filter(Boolean)
|
|
4376
|
+
.join(' • ') });
|
|
4377
|
+
}
|
|
3122
4378
|
async getTimesheetById(timesheetId) {
|
|
3123
4379
|
const timesheet = await this.querySingle(`SELECT id,
|
|
3124
4380
|
collaborator_id AS "collaboratorId",
|
|
@@ -3133,7 +4389,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3133
4389
|
return timesheet;
|
|
3134
4390
|
}
|
|
3135
4391
|
async replaceTimesheetEntries(client, timesheetId, entries, collaboratorId) {
|
|
3136
|
-
var _a, _b, _c;
|
|
4392
|
+
var _a, _b, _c, _d, _e, _f;
|
|
3137
4393
|
await client.$executeRawUnsafe(`UPDATE operations_timesheet_entry
|
|
3138
4394
|
SET deleted_at = NOW()
|
|
3139
4395
|
WHERE timesheet_id = $1
|
|
@@ -3159,22 +4415,54 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3159
4415
|
}
|
|
3160
4416
|
}
|
|
3161
4417
|
for (const entry of entries) {
|
|
4418
|
+
const durationMinutes = this.resolveEntryDurationMinutes(entry);
|
|
4419
|
+
const hours = Number((durationMinutes / 60).toFixed(2));
|
|
4420
|
+
const resolvedTask = entry.taskId
|
|
4421
|
+
? await this.getOwnedTaskRecord(client, collaboratorId, entry.taskId)
|
|
4422
|
+
: null;
|
|
4423
|
+
if (resolvedTask &&
|
|
4424
|
+
entry.projectAssignmentId &&
|
|
4425
|
+
resolvedTask.projectAssignmentId !== entry.projectAssignmentId) {
|
|
4426
|
+
throw new common_1.BadRequestException('The selected task does not belong to the chosen project assignment.');
|
|
4427
|
+
}
|
|
4428
|
+
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;
|
|
4429
|
+
if (entry.taskId && !resolvedAssignmentId) {
|
|
4430
|
+
throw new common_1.BadRequestException('The selected task must belong to a project assignment.');
|
|
4431
|
+
}
|
|
4432
|
+
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);
|
|
4433
|
+
if (!entry.workDate) {
|
|
4434
|
+
throw new common_1.BadRequestException('Timesheet entry workDate is required.');
|
|
4435
|
+
}
|
|
3162
4436
|
await client.$executeRawUnsafe(`INSERT INTO operations_timesheet_entry (
|
|
3163
4437
|
timesheet_id,
|
|
3164
4438
|
project_assignment_id,
|
|
4439
|
+
task_id,
|
|
3165
4440
|
activity_label,
|
|
3166
4441
|
work_date,
|
|
4442
|
+
duration_minutes,
|
|
3167
4443
|
hours,
|
|
3168
4444
|
description,
|
|
3169
4445
|
created_at,
|
|
3170
4446
|
updated_at
|
|
3171
|
-
) VALUES ($1, $2, $3, $4
|
|
4447
|
+
) 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
4448
|
}
|
|
3173
4449
|
}
|
|
3174
4450
|
async refreshTimesheetTotal(client, timesheetId) {
|
|
3175
4451
|
await client.$executeRawUnsafe(`UPDATE operations_timesheet
|
|
3176
4452
|
SET total_hours = (
|
|
3177
|
-
SELECT COALESCE(
|
|
4453
|
+
SELECT COALESCE(
|
|
4454
|
+
SUM(
|
|
4455
|
+
COALESCE(
|
|
4456
|
+
CASE
|
|
4457
|
+
WHEN duration_minutes IS NOT NULL AND duration_minutes > 0
|
|
4458
|
+
THEN duration_minutes::numeric / 60
|
|
4459
|
+
ELSE hours
|
|
4460
|
+
END,
|
|
4461
|
+
0
|
|
4462
|
+
)
|
|
4463
|
+
),
|
|
4464
|
+
0
|
|
4465
|
+
)
|
|
3178
4466
|
FROM operations_timesheet_entry
|
|
3179
4467
|
WHERE timesheet_id = $1
|
|
3180
4468
|
AND deleted_at IS NULL
|
|
@@ -3222,6 +4510,30 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3222
4510
|
}
|
|
3223
4511
|
await this.insertApprovalHistory(client, approvalId, input.requesterCollaboratorId, 'submitted', null);
|
|
3224
4512
|
}
|
|
4513
|
+
async applyApprovedScheduleAdjustmentIfNeeded(client, requestId) {
|
|
4514
|
+
const request = (await client.$queryRawUnsafe(`SELECT collaborator_id AS "collaboratorId",
|
|
4515
|
+
request_scope AS "requestScope"
|
|
4516
|
+
FROM operations_schedule_adjustment_request
|
|
4517
|
+
WHERE id = $1
|
|
4518
|
+
AND deleted_at IS NULL
|
|
4519
|
+
LIMIT 1`, requestId));
|
|
4520
|
+
const currentRequest = request[0];
|
|
4521
|
+
if (!currentRequest || currentRequest.requestScope !== 'permanent') {
|
|
4522
|
+
return;
|
|
4523
|
+
}
|
|
4524
|
+
const days = (await client.$queryRawUnsafe(`SELECT weekday,
|
|
4525
|
+
is_working_day AS "isWorkingDay",
|
|
4526
|
+
start_time AS "startTime",
|
|
4527
|
+
end_time AS "endTime",
|
|
4528
|
+
break_minutes AS "breakMinutes"
|
|
4529
|
+
FROM operations_schedule_adjustment_day
|
|
4530
|
+
WHERE schedule_adjustment_request_id = $1
|
|
4531
|
+
ORDER BY id ASC`, requestId));
|
|
4532
|
+
if (!days.length) {
|
|
4533
|
+
return;
|
|
4534
|
+
}
|
|
4535
|
+
await this.replaceCollaboratorScheduleDays(client, currentRequest.collaboratorId, days);
|
|
4536
|
+
}
|
|
3225
4537
|
async insertApprovalHistory(client, approvalId, actorCollaboratorId, action, note) {
|
|
3226
4538
|
await client.$executeRawUnsafe(`INSERT INTO operations_approval_history (
|
|
3227
4539
|
approval_id,
|
|
@@ -3238,19 +4550,10 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3238
4550
|
)`, approvalId, actorCollaboratorId, action, note);
|
|
3239
4551
|
}
|
|
3240
4552
|
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
|
-
}
|
|
4553
|
+
this.accessService.ensureProjectAccess(actor, projectId);
|
|
3246
4554
|
}
|
|
3247
4555
|
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
|
-
}
|
|
4556
|
+
this.accessService.ensureCollaboratorAccess(actor, collaboratorId);
|
|
3254
4557
|
}
|
|
3255
4558
|
async listSingleTimesheet(actor, timesheetId) {
|
|
3256
4559
|
const timesheets = await this.listTimesheets(actor.userId);
|
|
@@ -3261,19 +4564,13 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3261
4564
|
return timesheet;
|
|
3262
4565
|
}
|
|
3263
4566
|
ensureDirector(actor) {
|
|
3264
|
-
|
|
3265
|
-
throw new common_1.ForbiddenException('Director access is required.');
|
|
3266
|
-
}
|
|
4567
|
+
this.accessService.ensureDirector(actor);
|
|
3267
4568
|
}
|
|
3268
4569
|
ensureSupervisor(actor) {
|
|
3269
|
-
|
|
3270
|
-
throw new common_1.ForbiddenException('Supervisor access is required.');
|
|
3271
|
-
}
|
|
4570
|
+
this.accessService.ensureSupervisor(actor);
|
|
3272
4571
|
}
|
|
3273
4572
|
ensureCollaborator(actor) {
|
|
3274
|
-
|
|
3275
|
-
throw new common_1.ForbiddenException('Operations collaborator access is required.');
|
|
3276
|
-
}
|
|
4573
|
+
this.accessService.ensureCollaborator(actor);
|
|
3277
4574
|
}
|
|
3278
4575
|
defaultWeeklySchedule() {
|
|
3279
4576
|
return [
|
|
@@ -3352,16 +4649,22 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3352
4649
|
}
|
|
3353
4650
|
}
|
|
3354
4651
|
async replaceProjectAssignments(client, projectId, teamAssignments) {
|
|
3355
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
4652
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
3356
4653
|
await client.$executeRawUnsafe(`UPDATE operations_project_assignment
|
|
3357
4654
|
SET deleted_at = NOW(),
|
|
3358
4655
|
updated_at = NOW()
|
|
3359
4656
|
WHERE project_id = $1
|
|
3360
4657
|
AND deleted_at IS NULL`, projectId);
|
|
3361
4658
|
for (const assignment of teamAssignments !== null && teamAssignments !== void 0 ? teamAssignments : []) {
|
|
4659
|
+
const projectRole = typeof assignment.projectRoleId === 'number' &&
|
|
4660
|
+
Number.isInteger(assignment.projectRoleId) &&
|
|
4661
|
+
assignment.projectRoleId > 0
|
|
4662
|
+
? await this.getProjectRoleById(client, assignment.projectRoleId)
|
|
4663
|
+
: null;
|
|
3362
4664
|
await client.$executeRawUnsafe(`INSERT INTO operations_project_assignment (
|
|
3363
4665
|
project_id,
|
|
3364
4666
|
collaborator_id,
|
|
4667
|
+
project_role_id,
|
|
3365
4668
|
role_label,
|
|
3366
4669
|
allocation_percent,
|
|
3367
4670
|
weekly_hours,
|
|
@@ -3372,30 +4675,98 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3372
4675
|
created_at,
|
|
3373
4676
|
updated_at
|
|
3374
4677
|
) VALUES (
|
|
3375
|
-
$1, $2, $3, $4, $5, $6, $7::date, $
|
|
3376
|
-
$
|
|
4678
|
+
$1, $2, $3, $4, $5, $6, $7, $8::date, $9::date,
|
|
4679
|
+
$10::operations_project_assignment_status_155b459bbf_enum,
|
|
3377
4680
|
NOW(), NOW()
|
|
3378
|
-
)`, projectId, assignment.collaboratorId, (_a =
|
|
4681
|
+
)`, 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');
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
async replaceCollaboratorEquityParticipation(client, collaboratorId, equityParticipation) {
|
|
4685
|
+
var _a, _b, _c, _d, _e;
|
|
4686
|
+
if (equityParticipation === undefined) {
|
|
4687
|
+
return;
|
|
4688
|
+
}
|
|
4689
|
+
await client.$executeRawUnsafe(`UPDATE operations_collaborator_equity_participation
|
|
4690
|
+
SET deleted_at = COALESCE(deleted_at, NOW()),
|
|
4691
|
+
updated_at = NOW()
|
|
4692
|
+
WHERE collaborator_id = $1
|
|
4693
|
+
AND deleted_at IS NULL`, collaboratorId);
|
|
4694
|
+
if (equityParticipation === null) {
|
|
4695
|
+
return;
|
|
4696
|
+
}
|
|
4697
|
+
const participationType = (_a = equityParticipation.participationType) !== null && _a !== void 0 ? _a : 'other';
|
|
4698
|
+
await client.$executeRawUnsafe(`INSERT INTO operations_collaborator_equity_participation (
|
|
4699
|
+
collaborator_id,
|
|
4700
|
+
participation_type,
|
|
4701
|
+
percentage,
|
|
4702
|
+
voting_power,
|
|
4703
|
+
start_date,
|
|
4704
|
+
end_date,
|
|
4705
|
+
notes,
|
|
4706
|
+
deleted_at,
|
|
4707
|
+
created_at,
|
|
4708
|
+
updated_at
|
|
4709
|
+
) VALUES (
|
|
4710
|
+
$1,
|
|
4711
|
+
$2::operations_collaborator_equity_participation_pa_98792c1f1e_enum,
|
|
4712
|
+
$3,
|
|
4713
|
+
$4,
|
|
4714
|
+
$5::date,
|
|
4715
|
+
$6::date,
|
|
4716
|
+
$7,
|
|
4717
|
+
NULL,
|
|
4718
|
+
NOW(),
|
|
4719
|
+
NOW()
|
|
4720
|
+
)`, 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));
|
|
4721
|
+
}
|
|
4722
|
+
normalizeCollaboratorTypeKey(collaboratorType) {
|
|
4723
|
+
const normalized = String(collaboratorType !== null && collaboratorType !== void 0 ? collaboratorType : '')
|
|
4724
|
+
.trim()
|
|
4725
|
+
.toLowerCase()
|
|
4726
|
+
.normalize('NFD')
|
|
4727
|
+
.replace(/[\u0300-\u036f]/g, '');
|
|
4728
|
+
switch (normalized) {
|
|
4729
|
+
case 'estagiario':
|
|
4730
|
+
return 'intern';
|
|
4731
|
+
case 'outro':
|
|
4732
|
+
return 'other';
|
|
4733
|
+
default:
|
|
4734
|
+
return normalized;
|
|
3379
4735
|
}
|
|
3380
4736
|
}
|
|
3381
4737
|
mapContractCategoryForCollaboratorType(collaboratorType) {
|
|
3382
|
-
switch (collaboratorType) {
|
|
4738
|
+
switch (this.normalizeCollaboratorTypeKey(collaboratorType)) {
|
|
3383
4739
|
case 'clt':
|
|
4740
|
+
case 'intern':
|
|
3384
4741
|
return 'employee';
|
|
3385
4742
|
case 'pj':
|
|
3386
4743
|
case 'freelancer':
|
|
3387
4744
|
return 'contractor';
|
|
4745
|
+
case 'socio':
|
|
4746
|
+
case 'acionista':
|
|
4747
|
+
case 'parceiro':
|
|
4748
|
+
return 'partner';
|
|
4749
|
+
case 'administrador':
|
|
4750
|
+
case 'diretor':
|
|
4751
|
+
case 'conselheiro':
|
|
4752
|
+
return 'internal';
|
|
3388
4753
|
default:
|
|
3389
4754
|
return 'other';
|
|
3390
4755
|
}
|
|
3391
4756
|
}
|
|
3392
4757
|
mapBillingModelForCollaboratorType(collaboratorType) {
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
4758
|
+
switch (this.normalizeCollaboratorTypeKey(collaboratorType)) {
|
|
4759
|
+
case 'freelancer':
|
|
4760
|
+
return 'time_and_material';
|
|
4761
|
+
case 'pj':
|
|
4762
|
+
case 'parceiro':
|
|
4763
|
+
return 'fixed_price';
|
|
4764
|
+
default:
|
|
4765
|
+
return 'monthly_retainer';
|
|
4766
|
+
}
|
|
3396
4767
|
}
|
|
3397
4768
|
mapContractTypeForCollaboratorType(collaboratorType) {
|
|
3398
|
-
switch (collaboratorType) {
|
|
4769
|
+
switch (this.normalizeCollaboratorTypeKey(collaboratorType)) {
|
|
3399
4770
|
case 'clt':
|
|
3400
4771
|
return 'clt';
|
|
3401
4772
|
case 'pj':
|
|
@@ -3404,16 +4775,39 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3404
4775
|
return 'freelancer_agreement';
|
|
3405
4776
|
case 'intern':
|
|
3406
4777
|
return 'fixed_term';
|
|
4778
|
+
case 'socio':
|
|
4779
|
+
case 'acionista':
|
|
4780
|
+
case 'administrador':
|
|
4781
|
+
case 'diretor':
|
|
4782
|
+
case 'conselheiro':
|
|
4783
|
+
case 'parceiro':
|
|
4784
|
+
return 'service_agreement';
|
|
3407
4785
|
default:
|
|
3408
4786
|
return 'other';
|
|
3409
4787
|
}
|
|
3410
4788
|
}
|
|
4789
|
+
buildHiringContractName(displayName, collaboratorType) {
|
|
4790
|
+
switch (this.normalizeCollaboratorTypeKey(collaboratorType)) {
|
|
4791
|
+
case 'clt':
|
|
4792
|
+
return `${displayName} Employment Contract`;
|
|
4793
|
+
case 'intern':
|
|
4794
|
+
return `${displayName} Internship Agreement`;
|
|
4795
|
+
case 'socio':
|
|
4796
|
+
case 'acionista':
|
|
4797
|
+
case 'parceiro':
|
|
4798
|
+
return `${displayName} Partnership Agreement`;
|
|
4799
|
+
case 'administrador':
|
|
4800
|
+
case 'diretor':
|
|
4801
|
+
case 'conselheiro':
|
|
4802
|
+
return `${displayName} Executive Engagement Agreement`;
|
|
4803
|
+
default:
|
|
4804
|
+
return `${displayName} Service Contract`;
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
3411
4807
|
async createHiringContractDraft(client, createdByUserId, input) {
|
|
3412
4808
|
var _a, _b, _c;
|
|
3413
4809
|
const contractCode = `HIR-${input.collaboratorCode}`;
|
|
3414
|
-
const contractName = input.collaboratorType
|
|
3415
|
-
? `${input.displayName} Employment Contract`
|
|
3416
|
-
: `${input.displayName} Service Contract`;
|
|
4810
|
+
const contractName = this.buildHiringContractName(input.displayName, input.collaboratorType);
|
|
3417
4811
|
await client.$executeRawUnsafe(`INSERT INTO operations_contract (
|
|
3418
4812
|
code,
|
|
3419
4813
|
name,
|
|
@@ -4479,6 +5873,97 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
4479
5873
|
? ''
|
|
4480
5874
|
: String(value).trim();
|
|
4481
5875
|
}
|
|
5876
|
+
normalizeDurationMinutes(duration, unit) {
|
|
5877
|
+
const numeric = Number(duration);
|
|
5878
|
+
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
5879
|
+
throw new common_1.BadRequestException('Duration must be greater than 0.');
|
|
5880
|
+
}
|
|
5881
|
+
const minutes = unit === 'hours' ? numeric * 60 : numeric;
|
|
5882
|
+
const rounded = Math.round(minutes);
|
|
5883
|
+
if (!Number.isFinite(rounded) || rounded <= 0) {
|
|
5884
|
+
throw new common_1.BadRequestException('Duration must be greater than 0.');
|
|
5885
|
+
}
|
|
5886
|
+
return rounded;
|
|
5887
|
+
}
|
|
5888
|
+
resolveEntryDurationMinutes(entry) {
|
|
5889
|
+
var _a;
|
|
5890
|
+
if (entry.durationMinutes !== undefined &&
|
|
5891
|
+
entry.durationMinutes !== null) {
|
|
5892
|
+
return this.normalizeDurationMinutes(entry.durationMinutes, 'minutes');
|
|
5893
|
+
}
|
|
5894
|
+
if (entry.duration !== undefined && entry.duration !== null) {
|
|
5895
|
+
return this.normalizeDurationMinutes(entry.duration, (_a = entry.unit) !== null && _a !== void 0 ? _a : 'minutes');
|
|
5896
|
+
}
|
|
5897
|
+
if (entry.hours !== undefined && entry.hours !== null) {
|
|
5898
|
+
return this.normalizeDurationMinutes(entry.hours, 'hours');
|
|
5899
|
+
}
|
|
5900
|
+
throw new common_1.BadRequestException('Timesheet entry duration is required.');
|
|
5901
|
+
}
|
|
5902
|
+
normalizeDateOnly(value) {
|
|
5903
|
+
const normalized = this.normalizeExtractionString(value);
|
|
5904
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized)) {
|
|
5905
|
+
throw new common_1.BadRequestException('Date values must use the YYYY-MM-DD format.');
|
|
5906
|
+
}
|
|
5907
|
+
return normalized;
|
|
5908
|
+
}
|
|
5909
|
+
parseDateOnly(value) {
|
|
5910
|
+
const normalized = this.normalizeDateOnly(value);
|
|
5911
|
+
const parsed = new Date(`${normalized}T00:00:00.000Z`);
|
|
5912
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
5913
|
+
throw new common_1.BadRequestException('Invalid date value provided.');
|
|
5914
|
+
}
|
|
5915
|
+
return parsed;
|
|
5916
|
+
}
|
|
5917
|
+
formatDateOnly(value) {
|
|
5918
|
+
return value.toISOString().slice(0, 10);
|
|
5919
|
+
}
|
|
5920
|
+
getWorkWeekRange(workDate) {
|
|
5921
|
+
const date = this.parseDateOnly(workDate);
|
|
5922
|
+
const day = date.getUTCDay();
|
|
5923
|
+
const diffToMonday = day === 0 ? -6 : 1 - day;
|
|
5924
|
+
const weekStart = new Date(date);
|
|
5925
|
+
weekStart.setUTCDate(weekStart.getUTCDate() + diffToMonday);
|
|
5926
|
+
const weekEnd = new Date(weekStart);
|
|
5927
|
+
weekEnd.setUTCDate(weekStart.getUTCDate() + 6);
|
|
5928
|
+
return {
|
|
5929
|
+
weekStartDate: this.formatDateOnly(weekStart),
|
|
5930
|
+
weekEndDate: this.formatDateOnly(weekEnd),
|
|
5931
|
+
};
|
|
5932
|
+
}
|
|
5933
|
+
normalizePaginationParams(input = {}, options) {
|
|
5934
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
5935
|
+
const page = Math.max(Number((_a = input.page) !== null && _a !== void 0 ? _a : 1) || 1, 1);
|
|
5936
|
+
const pageSize = Math.min(Math.max(Number((_b = input.pageSize) !== null && _b !== void 0 ? _b : 10) || 10, 1), 100);
|
|
5937
|
+
const search = (_c = this.normalizeOptionalText(input.search)) !== null && _c !== void 0 ? _c : '';
|
|
5938
|
+
const requestedSortField = this.normalizeOptionalText(input.sortField);
|
|
5939
|
+
const sortField = requestedSortField && options.allowedSortFields.includes(requestedSortField)
|
|
5940
|
+
? requestedSortField
|
|
5941
|
+
: ((_e = (_d = options.defaultSortField) !== null && _d !== void 0 ? _d : options.allowedSortFields[0]) !== null && _e !== void 0 ? _e : 'id');
|
|
5942
|
+
const sortOrder = String((_g = (_f = input.sortOrder) !== null && _f !== void 0 ? _f : options.defaultSortOrder) !== null && _g !== void 0 ? _g : 'asc')
|
|
5943
|
+
.toLowerCase() === 'desc'
|
|
5944
|
+
? 'desc'
|
|
5945
|
+
: 'asc';
|
|
5946
|
+
return {
|
|
5947
|
+
page,
|
|
5948
|
+
pageSize,
|
|
5949
|
+
search,
|
|
5950
|
+
sortField,
|
|
5951
|
+
sortOrder,
|
|
5952
|
+
offset: (page - 1) * pageSize,
|
|
5953
|
+
};
|
|
5954
|
+
}
|
|
5955
|
+
buildPaginationResult(data, total, page, pageSize) {
|
|
5956
|
+
const lastPage = Math.max(1, Math.ceil(total / Math.max(pageSize, 1)));
|
|
5957
|
+
return {
|
|
5958
|
+
total,
|
|
5959
|
+
lastPage,
|
|
5960
|
+
page,
|
|
5961
|
+
pageSize,
|
|
5962
|
+
prev: page > 1 ? page - 1 : null,
|
|
5963
|
+
next: page < lastPage ? page + 1 : null,
|
|
5964
|
+
data,
|
|
5965
|
+
};
|
|
5966
|
+
}
|
|
4482
5967
|
normalizeExtractionBoolean(value) {
|
|
4483
5968
|
if (typeof value === 'boolean') {
|
|
4484
5969
|
return value;
|
|
@@ -4621,6 +6106,8 @@ exports.OperationsService = OperationsService = OperationsService_1 = __decorate
|
|
|
4621
6106
|
core_1.AiService,
|
|
4622
6107
|
core_1.IntegrationDeveloperApiService,
|
|
4623
6108
|
core_1.FileService,
|
|
4624
|
-
core_1.SettingService
|
|
6109
|
+
core_1.SettingService,
|
|
6110
|
+
operations_access_service_1.OperationsAccessService,
|
|
6111
|
+
api_locale_1.LocaleService])
|
|
4625
6112
|
], OperationsService);
|
|
4626
6113
|
//# sourceMappingURL=operations.service.js.map
|