@hed-hog/operations 0.0.306 → 0.0.309
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controllers/operations-approvals.controller.d.ts +114 -1
- package/dist/controllers/operations-approvals.controller.d.ts.map +1 -1
- package/dist/controllers/operations-approvals.controller.js +16 -3
- package/dist/controllers/operations-approvals.controller.js.map +1 -1
- package/dist/controllers/operations-collaborators.controller.d.ts +16 -1
- package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
- package/dist/controllers/operations-collaborators.controller.js +16 -3
- package/dist/controllers/operations-collaborators.controller.js.map +1 -1
- package/dist/controllers/operations-contracts.controller.d.ts +14 -453
- package/dist/controllers/operations-contracts.controller.d.ts.map +1 -1
- package/dist/controllers/operations-contracts.controller.js +11 -112
- package/dist/controllers/operations-contracts.controller.js.map +1 -1
- package/dist/controllers/operations-org-structure.controller.d.ts +65 -2
- package/dist/controllers/operations-org-structure.controller.d.ts.map +1 -1
- package/dist/controllers/operations-org-structure.controller.js +18 -5
- package/dist/controllers/operations-org-structure.controller.js.map +1 -1
- package/dist/controllers/operations-projects.controller.d.ts +28 -4
- package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
- package/dist/controllers/operations-projects.controller.js +17 -5
- package/dist/controllers/operations-projects.controller.js.map +1 -1
- package/dist/controllers/operations-timesheets.controller.d.ts +31 -4
- package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
- package/dist/controllers/operations-timesheets.controller.js +16 -11
- package/dist/controllers/operations-timesheets.controller.js.map +1 -1
- package/dist/dto/list-approvals.dto.d.ts +6 -0
- package/dist/dto/list-approvals.dto.d.ts.map +1 -0
- package/dist/dto/list-approvals.dto.js +28 -0
- package/dist/dto/list-approvals.dto.js.map +1 -0
- package/dist/dto/list-collaborator-types.dto.d.ts +3 -1
- package/dist/dto/list-collaborator-types.dto.d.ts.map +1 -1
- package/dist/dto/list-collaborator-types.dto.js +7 -1
- package/dist/dto/list-collaborator-types.dto.js.map +1 -1
- package/dist/dto/list-collaborators.dto.d.ts +1 -0
- package/dist/dto/list-collaborators.dto.d.ts.map +1 -1
- package/dist/dto/list-collaborators.dto.js +5 -0
- package/dist/dto/list-collaborators.dto.js.map +1 -1
- package/dist/dto/list-contracts.dto.d.ts +8 -0
- package/dist/dto/list-contracts.dto.d.ts.map +1 -0
- package/dist/dto/list-contracts.dto.js +38 -0
- package/dist/dto/list-contracts.dto.js.map +1 -0
- package/dist/dto/list-departments.dto.d.ts +5 -0
- package/dist/dto/list-departments.dto.d.ts.map +1 -0
- package/dist/dto/list-departments.dto.js +23 -0
- package/dist/dto/list-departments.dto.js.map +1 -0
- package/dist/dto/list-projects.dto.d.ts +5 -0
- package/dist/dto/list-projects.dto.d.ts.map +1 -0
- package/dist/dto/list-projects.dto.js +23 -0
- package/dist/dto/list-projects.dto.js.map +1 -0
- package/dist/dto/list-schedule-adjustments.dto.d.ts +5 -0
- package/dist/dto/list-schedule-adjustments.dto.d.ts.map +1 -0
- package/dist/dto/list-schedule-adjustments.dto.js +23 -0
- package/dist/dto/list-schedule-adjustments.dto.js.map +1 -0
- package/dist/dto/list-time-off-requests.dto.d.ts +5 -0
- package/dist/dto/list-time-off-requests.dto.d.ts.map +1 -0
- package/dist/dto/list-time-off-requests.dto.js +23 -0
- package/dist/dto/list-time-off-requests.dto.js.map +1 -0
- package/dist/dto/list-timesheets.dto.d.ts +5 -0
- package/dist/dto/list-timesheets.dto.d.ts.map +1 -0
- package/dist/dto/list-timesheets.dto.js +23 -0
- package/dist/dto/list-timesheets.dto.js.map +1 -0
- package/dist/dto/reorder-collaborator-types.dto.d.ts +4 -0
- package/dist/dto/reorder-collaborator-types.dto.d.ts.map +1 -0
- package/dist/dto/reorder-collaborator-types.dto.js +25 -0
- package/dist/dto/reorder-collaborator-types.dto.js.map +1 -0
- package/dist/operations.service.d.ts +340 -271
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +1007 -1043
- package/dist/operations.service.js.map +1 -1
- package/dist/operations.service.spec.js +0 -22
- package/dist/operations.service.spec.js.map +1 -1
- package/hedhog/data/menu.yaml +0 -36
- package/hedhog/data/route.yaml +42 -73
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +8 -1
- package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +15 -10
- package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +108 -213
- package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +251 -2039
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +167 -60
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +70 -301
- package/hedhog/frontend/app/_components/system-user-select-with-create.tsx.ejs +102 -51
- package/hedhog/frontend/app/_lib/types.ts.ejs +19 -24
- package/hedhog/frontend/app/_lib/utils/format.ts.ejs +14 -9
- package/hedhog/frontend/app/approvals/page.tsx.ejs +842 -150
- package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +445 -153
- package/hedhog/frontend/app/collaborators/page.tsx.ejs +118 -49
- package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/contracts/page.tsx.ejs +215 -617
- package/hedhog/frontend/app/departments/page.tsx.ejs +257 -113
- package/hedhog/frontend/app/projects/page.tsx.ejs +90 -51
- package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +412 -147
- package/hedhog/frontend/app/time-off/page.tsx.ejs +400 -123
- package/hedhog/frontend/app/timesheets/page.tsx.ejs +460 -365
- package/hedhog/frontend/messages/en.json +143 -14
- package/hedhog/frontend/messages/pt.json +192 -54
- package/hedhog/table/operations_contract.yaml +0 -9
- package/package.json +5 -5
- package/src/controllers/operations-approvals.controller.ts +9 -3
- package/src/controllers/operations-collaborators.controller.ts +15 -2
- package/src/controllers/operations-contracts.controller.ts +8 -92
- package/src/controllers/operations-org-structure.controller.ts +17 -4
- package/src/controllers/operations-projects.controller.ts +10 -4
- package/src/controllers/operations-timesheets.controller.ts +17 -8
- package/src/dto/list-approvals.dto.ts +12 -0
- package/src/dto/list-collaborator-types.dto.ts +7 -2
- package/src/dto/list-collaborators.dto.ts +4 -0
- package/src/dto/list-contracts.dto.ts +20 -0
- package/src/dto/list-departments.dto.ts +8 -0
- package/src/dto/list-projects.dto.ts +8 -0
- package/src/dto/list-schedule-adjustments.dto.ts +8 -0
- package/src/dto/list-time-off-requests.dto.ts +8 -0
- package/src/dto/list-timesheets.dto.ts +8 -0
- package/src/dto/reorder-collaborator-types.dto.ts +10 -0
- package/src/operations.service.spec.ts +0 -30
- package/src/operations.service.ts +1557 -1806
- package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +0 -631
- package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +0 -526
- package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +0 -247
- package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +0 -3520
- package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +0 -380
- package/hedhog/frontend/app/team/page.tsx.ejs +0 -352
- package/hedhog/table/operations_contract_financial_term.yaml +0 -40
- package/hedhog/table/operations_contract_revision.yaml +0 -38
- package/hedhog/table/operations_contract_signature.yaml +0 -38
- package/hedhog/table/operations_contract_template.yaml +0 -58
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import { LocaleService } from '@hed-hog/api-locale';
|
|
2
2
|
import { PrismaService } from '@hed-hog/api-prisma';
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
AiService,
|
|
5
|
+
FileService,
|
|
6
|
+
IntegrationDeveloperApiService,
|
|
7
|
+
SettingService,
|
|
8
|
+
getLocaleFromContext,
|
|
9
9
|
} from '@hed-hog/core';
|
|
10
10
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
BadRequestException,
|
|
12
|
+
ForbiddenException,
|
|
13
|
+
Inject,
|
|
14
|
+
Injectable,
|
|
15
|
+
InternalServerErrorException,
|
|
16
|
+
Logger,
|
|
17
|
+
NotFoundException,
|
|
18
|
+
forwardRef,
|
|
19
19
|
} from '@nestjs/common';
|
|
20
20
|
import { OperationsAccessService } from './services/shared/operations-access.service';
|
|
21
21
|
|
|
@@ -102,11 +102,7 @@ const PARTY_ROLE_VALUES = [
|
|
|
102
102
|
'other',
|
|
103
103
|
] as const;
|
|
104
104
|
const PARTY_TYPE_VALUES = ['individual', 'company', 'internal_team', 'other'] as const;
|
|
105
|
-
|
|
106
|
-
const FINANCIAL_TERM_TYPE_VALUES = ['value', 'payment', 'revenue', 'fine', 'other'] as const;
|
|
107
|
-
const RECURRENCE_VALUES = ['one_time', 'monthly', 'quarterly', 'yearly', 'other'] as const;
|
|
108
|
-
const REVISION_TYPE_VALUES = ['amendment', 'renewal', 'revision', 'addendum', 'other'] as const;
|
|
109
|
-
const REVISION_STATUS_VALUES = ['draft', 'active', 'completed', 'cancelled'] as const;
|
|
105
|
+
|
|
110
106
|
const TASK_STATUS_VALUES = ['todo', 'doing', 'review', 'done'] as const;
|
|
111
107
|
|
|
112
108
|
type ApprovalAction = 'approve' | 'reject';
|
|
@@ -243,7 +239,6 @@ type ContractPayload = {
|
|
|
243
239
|
isActive?: boolean;
|
|
244
240
|
accountManagerCollaboratorId?: number | null;
|
|
245
241
|
relatedCollaboratorId?: number | null;
|
|
246
|
-
contractTemplateId?: number | null;
|
|
247
242
|
originType?: 'manual' | 'employee_hiring' | 'client_project' | 'crm_proposal';
|
|
248
243
|
originId?: number | null;
|
|
249
244
|
startDate?: string | null;
|
|
@@ -282,28 +277,6 @@ type ContractPayload = {
|
|
|
282
277
|
phone?: string | null;
|
|
283
278
|
isPrimary?: boolean;
|
|
284
279
|
}>;
|
|
285
|
-
signatures?: Array<{
|
|
286
|
-
signerName: string;
|
|
287
|
-
signerRole?: string | null;
|
|
288
|
-
signerEmail?: string | null;
|
|
289
|
-
status?: 'pending' | 'signed' | 'rejected';
|
|
290
|
-
signedAt?: string | null;
|
|
291
|
-
}>;
|
|
292
|
-
financialTerms?: Array<{
|
|
293
|
-
termType?: 'value' | 'payment' | 'revenue' | 'fine' | 'other';
|
|
294
|
-
label: string;
|
|
295
|
-
amount: number;
|
|
296
|
-
recurrence?: 'one_time' | 'monthly' | 'quarterly' | 'yearly' | 'other';
|
|
297
|
-
dueDay?: number | null;
|
|
298
|
-
notes?: string | null;
|
|
299
|
-
}>;
|
|
300
|
-
revisions?: Array<{
|
|
301
|
-
revisionType?: 'amendment' | 'renewal' | 'revision' | 'addendum' | 'other';
|
|
302
|
-
title: string;
|
|
303
|
-
effectiveDate?: string | null;
|
|
304
|
-
status?: 'draft' | 'active' | 'completed' | 'cancelled';
|
|
305
|
-
summary?: string | null;
|
|
306
|
-
}>;
|
|
307
280
|
replaceUploadedPdfDocument?: {
|
|
308
281
|
fileId?: number | null;
|
|
309
282
|
fileName: string;
|
|
@@ -315,15 +288,6 @@ type ContractPayload = {
|
|
|
315
288
|
} | null;
|
|
316
289
|
};
|
|
317
290
|
|
|
318
|
-
type ContractDraftPayload = {
|
|
319
|
-
creationMode?: 'blank' | 'template' | 'upload' | 'duplicate' | null;
|
|
320
|
-
templateId?: number | null;
|
|
321
|
-
duplicateFromId?: number | null;
|
|
322
|
-
sourceFileId?: number | null;
|
|
323
|
-
sourceFileName?: string | null;
|
|
324
|
-
sourceMimeType?: string | null;
|
|
325
|
-
};
|
|
326
|
-
|
|
327
291
|
type ProposalApprovedEventPayload = {
|
|
328
292
|
proposalId?: number;
|
|
329
293
|
proposalRevisionId?: number | null;
|
|
@@ -383,53 +347,14 @@ type ProposalApprovedEventPayload = {
|
|
|
383
347
|
items?: Array<{
|
|
384
348
|
name?: string | null;
|
|
385
349
|
description?: string | null;
|
|
386
|
-
termType?:
|
|
387
|
-
recurrence?:
|
|
350
|
+
termType?: 'value' | 'payment' | 'revenue' | 'fine' | 'other';
|
|
351
|
+
recurrence?: 'one_time' | 'monthly' | 'quarterly' | 'yearly' | 'other';
|
|
388
352
|
dueDay?: number | null;
|
|
389
353
|
amount?: number | null;
|
|
390
354
|
totalAmountCents?: number | null;
|
|
391
355
|
}>;
|
|
392
356
|
};
|
|
393
357
|
|
|
394
|
-
type ContractTemplatePayload = {
|
|
395
|
-
code?: string | null;
|
|
396
|
-
name?: string | null;
|
|
397
|
-
description?: string | null;
|
|
398
|
-
contractCategory?:
|
|
399
|
-
| 'employee'
|
|
400
|
-
| 'contractor'
|
|
401
|
-
| 'client'
|
|
402
|
-
| 'supplier'
|
|
403
|
-
| 'vendor'
|
|
404
|
-
| 'partner'
|
|
405
|
-
| 'internal'
|
|
406
|
-
| 'other';
|
|
407
|
-
contractType?:
|
|
408
|
-
| 'clt'
|
|
409
|
-
| 'pj'
|
|
410
|
-
| 'freelancer_agreement'
|
|
411
|
-
| 'service_agreement'
|
|
412
|
-
| 'fixed_term'
|
|
413
|
-
| 'recurring_service'
|
|
414
|
-
| 'nda'
|
|
415
|
-
| 'amendment'
|
|
416
|
-
| 'addendum'
|
|
417
|
-
| 'other';
|
|
418
|
-
billingModel?:
|
|
419
|
-
| 'time_and_material'
|
|
420
|
-
| 'monthly_retainer'
|
|
421
|
-
| 'fixed_price';
|
|
422
|
-
signatureStatus?:
|
|
423
|
-
| 'not_started'
|
|
424
|
-
| 'pending'
|
|
425
|
-
| 'partially_signed'
|
|
426
|
-
| 'signed'
|
|
427
|
-
| 'expired';
|
|
428
|
-
isActive?: boolean;
|
|
429
|
-
status?: 'draft' | 'active' | 'inactive' | 'archived';
|
|
430
|
-
contentHtml?: string | null;
|
|
431
|
-
};
|
|
432
|
-
|
|
433
358
|
type ContractExtractDraftPayload = {
|
|
434
359
|
contractId?: number | null;
|
|
435
360
|
fileName?: string | null;
|
|
@@ -511,10 +436,6 @@ type ContractDetailRecord = {
|
|
|
511
436
|
billingModel: (typeof BILLING_MODEL_VALUES)[number];
|
|
512
437
|
accountManagerCollaboratorId: number | null;
|
|
513
438
|
relatedCollaboratorId: number | null;
|
|
514
|
-
contractTemplateId: number | null;
|
|
515
|
-
contractTemplateName: string | null;
|
|
516
|
-
contractTemplateSlug: string | null;
|
|
517
|
-
contractTemplateCode: string | null;
|
|
518
439
|
originType: (typeof ORIGIN_TYPE_VALUES)[number];
|
|
519
440
|
originId: number | null;
|
|
520
441
|
startDate: string | null;
|
|
@@ -537,16 +458,12 @@ type ContractDetailsRecord = ContractDetailRecord & {
|
|
|
537
458
|
projects: ContractProjectSummary[];
|
|
538
459
|
scheduleSummary: ContractScheduleDay[];
|
|
539
460
|
parties: NonNullable<ContractPayload['parties']>;
|
|
540
|
-
signatures: NonNullable<ContractPayload['signatures']>;
|
|
541
|
-
financialTerms: NonNullable<ContractPayload['financialTerms']>;
|
|
542
461
|
documents: ContractDocumentRecord[];
|
|
543
|
-
revisions: NonNullable<ContractPayload['revisions']>;
|
|
544
462
|
history: ContractHistoryRecord[];
|
|
545
463
|
};
|
|
546
464
|
|
|
547
465
|
type ProjectPayload = {
|
|
548
466
|
contractId?: number | null;
|
|
549
|
-
contractTemplateId?: number | null;
|
|
550
467
|
managerCollaboratorId?: number | null;
|
|
551
468
|
clientPersonId?: number | null;
|
|
552
469
|
code: string;
|
|
@@ -577,7 +494,6 @@ type ProjectPayload = {
|
|
|
577
494
|
contractCode?: string | null;
|
|
578
495
|
contractName?: string | null;
|
|
579
496
|
contractDescription?: string | null;
|
|
580
|
-
autoGenerateContractDraft?: boolean;
|
|
581
497
|
teamAssignments?: Array<{
|
|
582
498
|
collaboratorId: number;
|
|
583
499
|
projectRoleId?: number | null;
|
|
@@ -758,12 +674,114 @@ export class OperationsService {
|
|
|
758
674
|
|
|
759
675
|
async listCollaboratorTypes(
|
|
760
676
|
userId: number,
|
|
761
|
-
filters
|
|
677
|
+
filters: {
|
|
678
|
+
page?: number;
|
|
679
|
+
pageSize?: number;
|
|
680
|
+
search?: string;
|
|
681
|
+
sortField?: string;
|
|
682
|
+
sortOrder?: string;
|
|
683
|
+
active?: boolean;
|
|
684
|
+
status?: string;
|
|
685
|
+
} = {}
|
|
762
686
|
) {
|
|
763
687
|
const actor = await this.getActorContext(userId);
|
|
764
688
|
this.ensureCollaborator(actor);
|
|
765
689
|
|
|
766
|
-
|
|
690
|
+
const params: unknown[] = [];
|
|
691
|
+
const where: string[] = [];
|
|
692
|
+
|
|
693
|
+
if (filters.active === true || filters.status === 'active') {
|
|
694
|
+
where.push('(ct.deleted_at IS NULL AND ct.is_active = true)');
|
|
695
|
+
} else if (filters.status === 'inactive') {
|
|
696
|
+
where.push('(ct.deleted_at IS NOT NULL OR ct.is_active = false)');
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const pagination = this.shouldPaginate(filters)
|
|
700
|
+
? this.normalizePaginationParams(filters, {
|
|
701
|
+
defaultSortField: 'sortOrder',
|
|
702
|
+
defaultSortOrder: 'asc',
|
|
703
|
+
allowedSortFields: ['name', 'slug', 'category', 'sortOrder', 'createdAt'],
|
|
704
|
+
})
|
|
705
|
+
: null;
|
|
706
|
+
|
|
707
|
+
if (pagination?.search) {
|
|
708
|
+
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
709
|
+
where.push(`(
|
|
710
|
+
COALESCE(ct.name, '') ILIKE ${searchPlaceholder}
|
|
711
|
+
OR COALESCE(ct.slug, '') ILIKE ${searchPlaceholder}
|
|
712
|
+
OR COALESCE(ct.description, '') ILIKE ${searchPlaceholder}
|
|
713
|
+
OR COALESCE(ct.category, '') ILIKE ${searchPlaceholder}
|
|
714
|
+
)`);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const whereClause = where.length ? `WHERE ${where.join(' AND ')}` : '';
|
|
718
|
+
const baseQuery = `SELECT ct.id,
|
|
719
|
+
ct.slug,
|
|
720
|
+
ct.name,
|
|
721
|
+
ct.description,
|
|
722
|
+
ct.category,
|
|
723
|
+
ct.is_active AS "isActive",
|
|
724
|
+
ct.sort_order AS "sortOrder",
|
|
725
|
+
CASE
|
|
726
|
+
WHEN ct.deleted_at IS NULL AND ct.is_active THEN 'active'
|
|
727
|
+
ELSE 'inactive'
|
|
728
|
+
END AS status,
|
|
729
|
+
COUNT(DISTINCT c.id)::int AS "collaboratorCount",
|
|
730
|
+
ct.created_at AS "createdAt",
|
|
731
|
+
ct.updated_at AS "updatedAt"
|
|
732
|
+
FROM operations_collaborator_type ct
|
|
733
|
+
LEFT JOIN operations_collaborator c
|
|
734
|
+
ON c.deleted_at IS NULL
|
|
735
|
+
AND c.collaborator_type_id = ct.id
|
|
736
|
+
${whereClause}
|
|
737
|
+
GROUP BY ct.id`;
|
|
738
|
+
|
|
739
|
+
if (!pagination) {
|
|
740
|
+
return this.queryRows<{
|
|
741
|
+
id: number;
|
|
742
|
+
slug: string;
|
|
743
|
+
name: string;
|
|
744
|
+
description: string | null;
|
|
745
|
+
category: string | null;
|
|
746
|
+
isActive: boolean;
|
|
747
|
+
sortOrder: number;
|
|
748
|
+
status: 'active' | 'inactive';
|
|
749
|
+
collaboratorCount: number;
|
|
750
|
+
createdAt: string;
|
|
751
|
+
updatedAt: string;
|
|
752
|
+
}>(
|
|
753
|
+
`${baseQuery}
|
|
754
|
+
ORDER BY CASE
|
|
755
|
+
WHEN ct.deleted_at IS NULL AND ct.is_active THEN 0
|
|
756
|
+
ELSE 1
|
|
757
|
+
END ASC,
|
|
758
|
+
ct.sort_order ASC,
|
|
759
|
+
ct.name ASC`,
|
|
760
|
+
params
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const totalRow = await this.querySingle<{ total: string }>(
|
|
765
|
+
`SELECT COUNT(*)::text AS total
|
|
766
|
+
FROM operations_collaborator_type ct
|
|
767
|
+
${whereClause}`,
|
|
768
|
+
params
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
const sortColumn =
|
|
772
|
+
{
|
|
773
|
+
name: 'ct.name',
|
|
774
|
+
slug: 'ct.slug',
|
|
775
|
+
category: 'ct.category',
|
|
776
|
+
sortOrder: 'ct.sort_order',
|
|
777
|
+
createdAt: 'ct.created_at',
|
|
778
|
+
}[pagination.sortField] ?? 'ct.name';
|
|
779
|
+
|
|
780
|
+
const queryParams = [...params];
|
|
781
|
+
const limitPlaceholder = this.param(queryParams, pagination.pageSize);
|
|
782
|
+
const offsetPlaceholder = this.param(queryParams, pagination.offset);
|
|
783
|
+
|
|
784
|
+
const rows = await this.queryRows<{
|
|
767
785
|
id: number;
|
|
768
786
|
slug: string;
|
|
769
787
|
name: string;
|
|
@@ -776,33 +794,18 @@ export class OperationsService {
|
|
|
776
794
|
createdAt: string;
|
|
777
795
|
updatedAt: string;
|
|
778
796
|
}>(
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
ct.created_at AS "createdAt",
|
|
792
|
-
ct.updated_at AS "updatedAt"
|
|
793
|
-
FROM operations_collaborator_type ct
|
|
794
|
-
LEFT JOIN operations_collaborator c
|
|
795
|
-
ON c.deleted_at IS NULL
|
|
796
|
-
AND c.collaborator_type_id = ct.id
|
|
797
|
-
WHERE ($1::boolean = false OR (ct.deleted_at IS NULL AND ct.is_active = true))
|
|
798
|
-
GROUP BY ct.id
|
|
799
|
-
ORDER BY CASE
|
|
800
|
-
WHEN ct.deleted_at IS NULL AND ct.is_active THEN 0
|
|
801
|
-
ELSE 1
|
|
802
|
-
END ASC,
|
|
803
|
-
ct.sort_order ASC,
|
|
804
|
-
ct.name ASC`,
|
|
805
|
-
[Boolean(filters?.active)]
|
|
797
|
+
`${baseQuery}
|
|
798
|
+
ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, ct.id ASC
|
|
799
|
+
LIMIT ${limitPlaceholder}
|
|
800
|
+
OFFSET ${offsetPlaceholder}`,
|
|
801
|
+
queryParams
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
return this.buildPaginationResult(
|
|
805
|
+
rows,
|
|
806
|
+
Number(totalRow?.total ?? 0),
|
|
807
|
+
pagination.page,
|
|
808
|
+
pagination.pageSize
|
|
806
809
|
);
|
|
807
810
|
}
|
|
808
811
|
|
|
@@ -825,6 +828,10 @@ export class OperationsService {
|
|
|
825
828
|
);
|
|
826
829
|
const nextIsActive =
|
|
827
830
|
data.isActive ?? (data.status ? data.status === 'active' : true);
|
|
831
|
+
const nextSortOrder =
|
|
832
|
+
data.sortOrder !== undefined && Number.isFinite(Number(data.sortOrder))
|
|
833
|
+
? Number(data.sortOrder)
|
|
834
|
+
: await this.getNextCollaboratorTypeSortOrder(tx as any);
|
|
828
835
|
|
|
829
836
|
const created = (await (tx as any).$queryRawUnsafe(
|
|
830
837
|
`INSERT INTO operations_collaborator_type (
|
|
@@ -848,7 +855,7 @@ export class OperationsService {
|
|
|
848
855
|
this.normalizeOptionalText(data.description),
|
|
849
856
|
this.normalizeOptionalText(data.category),
|
|
850
857
|
nextIsActive,
|
|
851
|
-
|
|
858
|
+
nextSortOrder
|
|
852
859
|
)) as { id: number }[];
|
|
853
860
|
|
|
854
861
|
const createdCollaboratorTypeId = created[0]?.id;
|
|
@@ -940,6 +947,55 @@ export class OperationsService {
|
|
|
940
947
|
});
|
|
941
948
|
}
|
|
942
949
|
|
|
950
|
+
async reorderCollaboratorTypes(userId: number, ids: number[]) {
|
|
951
|
+
const actor = await this.getActorContext(userId);
|
|
952
|
+
this.ensureDirector(actor);
|
|
953
|
+
|
|
954
|
+
const normalizedIds = [
|
|
955
|
+
...new Set(
|
|
956
|
+
ids
|
|
957
|
+
.map((value) => Number(value))
|
|
958
|
+
.filter((value) => Number.isInteger(value) && value > 0)
|
|
959
|
+
),
|
|
960
|
+
];
|
|
961
|
+
|
|
962
|
+
if (!normalizedIds.length) {
|
|
963
|
+
throw new BadRequestException('At least one collaborator type is required.');
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return this.prisma.$transaction(async (tx) => {
|
|
967
|
+
const existingRows = (await (tx as any).$queryRawUnsafe(
|
|
968
|
+
`SELECT id
|
|
969
|
+
FROM operations_collaborator_type
|
|
970
|
+
WHERE id = ANY($1::int[])`,
|
|
971
|
+
normalizedIds
|
|
972
|
+
)) as { id: number }[];
|
|
973
|
+
|
|
974
|
+
if (existingRows.length !== normalizedIds.length) {
|
|
975
|
+
throw new NotFoundException('One or more collaborator types were not found.');
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
await (tx as any).$executeRawUnsafe(
|
|
979
|
+
`UPDATE operations_collaborator_type AS ct
|
|
980
|
+
SET sort_order = updates.sort_order,
|
|
981
|
+
updated_at = NOW()
|
|
982
|
+
FROM (
|
|
983
|
+
SELECT UNNEST($1::int[]) AS id,
|
|
984
|
+
GENERATE_SERIES(1, array_length($1::int[], 1)) AS sort_order
|
|
985
|
+
) AS updates
|
|
986
|
+
WHERE ct.id = updates.id`,
|
|
987
|
+
normalizedIds
|
|
988
|
+
);
|
|
989
|
+
|
|
990
|
+
return this.listCollaboratorTypes(userId, {
|
|
991
|
+
page: 1,
|
|
992
|
+
pageSize: Math.max(normalizedIds.length, 1),
|
|
993
|
+
sortField: 'sortOrder',
|
|
994
|
+
sortOrder: 'asc',
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
943
999
|
async listProjectRoles(userId: number) {
|
|
944
1000
|
const actor = await this.getActorContext(userId);
|
|
945
1001
|
this.ensureCollaborator(actor);
|
|
@@ -1173,7 +1229,21 @@ export class OperationsService {
|
|
|
1173
1229
|
};
|
|
1174
1230
|
}
|
|
1175
1231
|
|
|
1176
|
-
async listCollaborators(
|
|
1232
|
+
async listCollaborators(
|
|
1233
|
+
userId: number,
|
|
1234
|
+
filters: {
|
|
1235
|
+
page?: number;
|
|
1236
|
+
pageSize?: number;
|
|
1237
|
+
search?: string;
|
|
1238
|
+
sortField?: string;
|
|
1239
|
+
sortOrder?: string;
|
|
1240
|
+
status?: string;
|
|
1241
|
+
collaboratorType?: string;
|
|
1242
|
+
collaboratorTypeId?: number;
|
|
1243
|
+
departmentId?: number;
|
|
1244
|
+
jobTitleId?: number;
|
|
1245
|
+
} = {}
|
|
1246
|
+
) {
|
|
1177
1247
|
const actor = await this.getActorContext(userId);
|
|
1178
1248
|
this.ensureCollaborator(actor);
|
|
1179
1249
|
const filter = this.buildIdFilter(
|
|
@@ -1181,9 +1251,55 @@ export class OperationsService {
|
|
|
1181
1251
|
'c.id',
|
|
1182
1252
|
actor.isDirector
|
|
1183
1253
|
);
|
|
1254
|
+
const pagination = this.shouldPaginate(filters)
|
|
1255
|
+
? this.normalizePaginationParams(filters, {
|
|
1256
|
+
defaultSortField: 'displayName',
|
|
1257
|
+
defaultSortOrder: 'asc',
|
|
1258
|
+
allowedSortFields: ['displayName', 'code', 'joinedAt', 'status'],
|
|
1259
|
+
})
|
|
1260
|
+
: null;
|
|
1184
1261
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1262
|
+
const params: unknown[] = [...filter.params];
|
|
1263
|
+
const where = ['c.deleted_at IS NULL', filter.clause];
|
|
1264
|
+
|
|
1265
|
+
if (filters.status && filters.status !== 'all') {
|
|
1266
|
+
where.push(`c.status::text = ${this.param(params, filters.status)}`);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if (filters.collaboratorTypeId) {
|
|
1270
|
+
where.push(`c.collaborator_type_id = ${this.param(params, Number(filters.collaboratorTypeId))}`);
|
|
1271
|
+
} else if (filters.collaboratorType && filters.collaboratorType !== 'all') {
|
|
1272
|
+
const collaboratorTypePlaceholder = this.param(params, filters.collaboratorType);
|
|
1273
|
+
where.push(`(
|
|
1274
|
+
collaborator_type.slug = ${collaboratorTypePlaceholder}
|
|
1275
|
+
OR collaborator_type.name = ${collaboratorTypePlaceholder}
|
|
1276
|
+
)`);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
if (filters.departmentId) {
|
|
1280
|
+
where.push(`c.department_id = ${this.param(params, Number(filters.departmentId))}`);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if (filters.jobTitleId) {
|
|
1284
|
+
where.push(`c.job_title_id = ${this.param(params, Number(filters.jobTitleId))}`);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
if (pagination?.search) {
|
|
1288
|
+
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
1289
|
+
where.push(`(
|
|
1290
|
+
COALESCE(c.display_name, '') ILIKE ${searchPlaceholder}
|
|
1291
|
+
OR COALESCE(person_record.name, '') ILIKE ${searchPlaceholder}
|
|
1292
|
+
OR COALESCE(c.code, '') ILIKE ${searchPlaceholder}
|
|
1293
|
+
OR COALESCE(department_record.name, '') ILIKE ${searchPlaceholder}
|
|
1294
|
+
OR COALESCE(c.department, '') ILIKE ${searchPlaceholder}
|
|
1295
|
+
OR COALESCE(job_title_record.name, '') ILIKE ${searchPlaceholder}
|
|
1296
|
+
OR COALESCE(c.title, '') ILIKE ${searchPlaceholder}
|
|
1297
|
+
OR COALESCE(s.display_name, '') ILIKE ${searchPlaceholder}
|
|
1298
|
+
)`);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
const whereClause = where.join(' AND ');
|
|
1302
|
+
const baseQuery = `SELECT c.id,
|
|
1187
1303
|
c.user_id AS "userId",
|
|
1188
1304
|
c.person_id AS "personId",
|
|
1189
1305
|
c.code,
|
|
@@ -1236,12 +1352,180 @@ export class OperationsService {
|
|
|
1236
1352
|
oc.created_at DESC
|
|
1237
1353
|
LIMIT 1
|
|
1238
1354
|
) hiring_contract ON TRUE
|
|
1239
|
-
WHERE
|
|
1240
|
-
AND ${filter.clause}
|
|
1355
|
+
WHERE ${whereClause}
|
|
1241
1356
|
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
|
|
1242
|
-
|
|
1243
|
-
|
|
1357
|
+
`;
|
|
1358
|
+
|
|
1359
|
+
if (!pagination) {
|
|
1360
|
+
return this.queryRows(
|
|
1361
|
+
`${baseQuery}
|
|
1362
|
+
ORDER BY COALESCE(NULLIF(c.display_name, ''), person_record.name) ASC`,
|
|
1363
|
+
params
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
const totalRow = await this.querySingle<{ total: string }>(
|
|
1368
|
+
`SELECT COUNT(*)::text AS total
|
|
1369
|
+
FROM (
|
|
1370
|
+
SELECT c.id
|
|
1371
|
+
FROM operations_collaborator c
|
|
1372
|
+
LEFT JOIN person person_record
|
|
1373
|
+
ON person_record.id = c.person_id
|
|
1374
|
+
LEFT JOIN operations_collaborator_type collaborator_type
|
|
1375
|
+
ON collaborator_type.id = c.collaborator_type_id
|
|
1376
|
+
AND collaborator_type.deleted_at IS NULL
|
|
1377
|
+
LEFT JOIN operations_department department_record
|
|
1378
|
+
ON department_record.id = c.department_id
|
|
1379
|
+
AND department_record.deleted_at IS NULL
|
|
1380
|
+
LEFT JOIN operations_job_title job_title_record
|
|
1381
|
+
ON job_title_record.id = c.job_title_id
|
|
1382
|
+
AND job_title_record.deleted_at IS NULL
|
|
1383
|
+
LEFT JOIN operations_collaborator s
|
|
1384
|
+
ON s.id = c.supervisor_collaborator_id
|
|
1385
|
+
WHERE ${whereClause}
|
|
1386
|
+
GROUP BY c.id, person_record.id, collaborator_type.id, department_record.id, job_title_record.id, s.id
|
|
1387
|
+
) collaborator_rows`,
|
|
1388
|
+
params
|
|
1389
|
+
);
|
|
1390
|
+
|
|
1391
|
+
const sortColumn =
|
|
1392
|
+
{
|
|
1393
|
+
displayName: `COALESCE(NULLIF(c.display_name, ''), person_record.name)`,
|
|
1394
|
+
code: 'c.code',
|
|
1395
|
+
joinedAt: 'c.joined_at',
|
|
1396
|
+
status: 'c.status',
|
|
1397
|
+
}[pagination.sortField] ?? `COALESCE(NULLIF(c.display_name, ''), person_record.name)`;
|
|
1398
|
+
|
|
1399
|
+
const queryParams = [...params];
|
|
1400
|
+
const limitPlaceholder = this.param(queryParams, pagination.pageSize);
|
|
1401
|
+
const offsetPlaceholder = this.param(queryParams, pagination.offset);
|
|
1402
|
+
const rows = await this.queryRows(
|
|
1403
|
+
`${baseQuery}
|
|
1404
|
+
ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, c.id ASC
|
|
1405
|
+
LIMIT ${limitPlaceholder}
|
|
1406
|
+
OFFSET ${offsetPlaceholder}`,
|
|
1407
|
+
queryParams
|
|
1408
|
+
);
|
|
1409
|
+
|
|
1410
|
+
return this.buildPaginationResult(
|
|
1411
|
+
rows,
|
|
1412
|
+
Number(totalRow?.total ?? 0),
|
|
1413
|
+
pagination.page,
|
|
1414
|
+
pagination.pageSize
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
async getCollaboratorStats(
|
|
1419
|
+
userId: number,
|
|
1420
|
+
filters: {
|
|
1421
|
+
search?: string;
|
|
1422
|
+
status?: string;
|
|
1423
|
+
collaboratorType?: string;
|
|
1424
|
+
collaboratorTypeId?: number;
|
|
1425
|
+
departmentId?: number;
|
|
1426
|
+
jobTitleId?: number;
|
|
1427
|
+
} = {}
|
|
1428
|
+
) {
|
|
1429
|
+
const actor = await this.getActorContext(userId);
|
|
1430
|
+
this.ensureCollaborator(actor);
|
|
1431
|
+
const filter = this.buildIdFilter(
|
|
1432
|
+
actor.visibleCollaboratorIds,
|
|
1433
|
+
'c.id',
|
|
1434
|
+
actor.isDirector
|
|
1435
|
+
);
|
|
1436
|
+
|
|
1437
|
+
const params: unknown[] = [...filter.params];
|
|
1438
|
+
const where = ['c.deleted_at IS NULL', filter.clause];
|
|
1439
|
+
|
|
1440
|
+
if (filters.status && filters.status !== 'all') {
|
|
1441
|
+
where.push(`c.status::text = ${this.param(params, filters.status)}`);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
if (filters.collaboratorTypeId) {
|
|
1445
|
+
where.push(
|
|
1446
|
+
`c.collaborator_type_id = ${this.param(params, Number(filters.collaboratorTypeId))}`
|
|
1447
|
+
);
|
|
1448
|
+
} else if (filters.collaboratorType && filters.collaboratorType !== 'all') {
|
|
1449
|
+
const collaboratorTypePlaceholder = this.param(
|
|
1450
|
+
params,
|
|
1451
|
+
filters.collaboratorType
|
|
1452
|
+
);
|
|
1453
|
+
where.push(`(
|
|
1454
|
+
collaborator_type.slug = ${collaboratorTypePlaceholder}
|
|
1455
|
+
OR collaborator_type.name = ${collaboratorTypePlaceholder}
|
|
1456
|
+
)`);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
if (filters.departmentId) {
|
|
1460
|
+
where.push(
|
|
1461
|
+
`c.department_id = ${this.param(params, Number(filters.departmentId))}`
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
if (filters.jobTitleId) {
|
|
1466
|
+
where.push(
|
|
1467
|
+
`c.job_title_id = ${this.param(params, Number(filters.jobTitleId))}`
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
const normalizedSearch = String(filters.search ?? '').trim();
|
|
1472
|
+
if (normalizedSearch) {
|
|
1473
|
+
const searchPlaceholder = this.param(params, `%${normalizedSearch}%`);
|
|
1474
|
+
where.push(`(
|
|
1475
|
+
COALESCE(c.display_name, '') ILIKE ${searchPlaceholder}
|
|
1476
|
+
OR COALESCE(person_record.name, '') ILIKE ${searchPlaceholder}
|
|
1477
|
+
OR COALESCE(c.code, '') ILIKE ${searchPlaceholder}
|
|
1478
|
+
OR COALESCE(department_record.name, '') ILIKE ${searchPlaceholder}
|
|
1479
|
+
OR COALESCE(c.department, '') ILIKE ${searchPlaceholder}
|
|
1480
|
+
OR COALESCE(job_title_record.name, '') ILIKE ${searchPlaceholder}
|
|
1481
|
+
OR COALESCE(c.title, '') ILIKE ${searchPlaceholder}
|
|
1482
|
+
OR COALESCE(s.display_name, '') ILIKE ${searchPlaceholder}
|
|
1483
|
+
)`);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
const result = await this.querySingle<{
|
|
1487
|
+
total: number;
|
|
1488
|
+
active: number;
|
|
1489
|
+
onLeave: number;
|
|
1490
|
+
withContracts: number;
|
|
1491
|
+
}>(
|
|
1492
|
+
`SELECT COUNT(DISTINCT c.id)::int AS total,
|
|
1493
|
+
COUNT(DISTINCT c.id) FILTER (WHERE c.status = 'active')::int AS active,
|
|
1494
|
+
COUNT(DISTINCT c.id) FILTER (WHERE c.status = 'on_leave')::int AS "onLeave",
|
|
1495
|
+
COUNT(DISTINCT c.id) FILTER (WHERE hiring_contract.id IS NOT NULL)::int AS "withContracts"
|
|
1496
|
+
FROM operations_collaborator c
|
|
1497
|
+
LEFT JOIN person person_record
|
|
1498
|
+
ON person_record.id = c.person_id
|
|
1499
|
+
LEFT JOIN operations_collaborator_type collaborator_type
|
|
1500
|
+
ON collaborator_type.id = c.collaborator_type_id
|
|
1501
|
+
AND collaborator_type.deleted_at IS NULL
|
|
1502
|
+
LEFT JOIN operations_department department_record
|
|
1503
|
+
ON department_record.id = c.department_id
|
|
1504
|
+
AND department_record.deleted_at IS NULL
|
|
1505
|
+
LEFT JOIN operations_job_title job_title_record
|
|
1506
|
+
ON job_title_record.id = c.job_title_id
|
|
1507
|
+
AND job_title_record.deleted_at IS NULL
|
|
1508
|
+
LEFT JOIN operations_collaborator s
|
|
1509
|
+
ON s.id = c.supervisor_collaborator_id
|
|
1510
|
+
LEFT JOIN LATERAL (
|
|
1511
|
+
SELECT oc.id
|
|
1512
|
+
FROM operations_contract oc
|
|
1513
|
+
WHERE oc.related_collaborator_id = c.id
|
|
1514
|
+
AND oc.deleted_at IS NULL
|
|
1515
|
+
ORDER BY CASE WHEN oc.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
|
|
1516
|
+
oc.created_at DESC
|
|
1517
|
+
LIMIT 1
|
|
1518
|
+
) hiring_contract ON TRUE
|
|
1519
|
+
WHERE ${where.join(' AND ')}`,
|
|
1520
|
+
params
|
|
1244
1521
|
);
|
|
1522
|
+
|
|
1523
|
+
return {
|
|
1524
|
+
total: Number(result?.total ?? 0),
|
|
1525
|
+
active: Number(result?.active ?? 0),
|
|
1526
|
+
onLeave: Number(result?.onLeave ?? 0),
|
|
1527
|
+
withContracts: Number(result?.withContracts ?? 0),
|
|
1528
|
+
};
|
|
1245
1529
|
}
|
|
1246
1530
|
|
|
1247
1531
|
async getMyCollaborator(userId: number) {
|
|
@@ -1380,12 +1664,12 @@ export class OperationsService {
|
|
|
1380
1664
|
''
|
|
1381
1665
|
) AS "timesheetProjectNames",
|
|
1382
1666
|
tor.request_type AS "timeOffType",
|
|
1383
|
-
tor.start_date AS "timeOffStartDate",
|
|
1384
|
-
tor.end_date AS "timeOffEndDate",
|
|
1667
|
+
tor.start_date::text AS "timeOffStartDate",
|
|
1668
|
+
tor.end_date::text AS "timeOffEndDate",
|
|
1385
1669
|
tor.reason AS "timeOffReason",
|
|
1386
1670
|
sar.request_scope AS "scheduleRequestScope",
|
|
1387
|
-
sar.effective_start_date AS "scheduleStartDate",
|
|
1388
|
-
sar.effective_end_date AS "scheduleEndDate",
|
|
1671
|
+
sar.effective_start_date::text AS "scheduleStartDate",
|
|
1672
|
+
sar.effective_end_date::text AS "scheduleEndDate",
|
|
1389
1673
|
sar.reason AS "scheduleReason"
|
|
1390
1674
|
FROM operations_approval a
|
|
1391
1675
|
JOIN operations_collaborator requester
|
|
@@ -1420,8 +1704,8 @@ export class OperationsService {
|
|
|
1420
1704
|
tor.collaborator_id AS "collaboratorId",
|
|
1421
1705
|
c.display_name AS "collaboratorName",
|
|
1422
1706
|
tor.request_type AS "requestType",
|
|
1423
|
-
tor.start_date AS "startDate",
|
|
1424
|
-
tor.end_date AS "endDate",
|
|
1707
|
+
tor.start_date::text AS "startDate",
|
|
1708
|
+
tor.end_date::text AS "endDate",
|
|
1425
1709
|
tor.total_days AS "totalDays",
|
|
1426
1710
|
tor.status,
|
|
1427
1711
|
tor.reason,
|
|
@@ -1445,8 +1729,8 @@ export class OperationsService {
|
|
|
1445
1729
|
sar.collaborator_id AS "collaboratorId",
|
|
1446
1730
|
c.display_name AS "collaboratorName",
|
|
1447
1731
|
sar.request_scope AS "requestScope",
|
|
1448
|
-
sar.effective_start_date AS "effectiveStartDate",
|
|
1449
|
-
sar.effective_end_date AS "effectiveEndDate",
|
|
1732
|
+
sar.effective_start_date::text AS "effectiveStartDate",
|
|
1733
|
+
sar.effective_end_date::text AS "effectiveEndDate",
|
|
1450
1734
|
sar.status,
|
|
1451
1735
|
sar.reason,
|
|
1452
1736
|
sar.submitted_at AS "submittedAt",
|
|
@@ -1777,33 +2061,110 @@ export class OperationsService {
|
|
|
1777
2061
|
return this.getCollaboratorByIdForUser(userId, collaboratorId);
|
|
1778
2062
|
}
|
|
1779
2063
|
|
|
1780
|
-
async listDepartments(
|
|
1781
|
-
|
|
1782
|
-
|
|
2064
|
+
async listDepartments(
|
|
2065
|
+
userId: number,
|
|
2066
|
+
filters: {
|
|
2067
|
+
page?: number;
|
|
2068
|
+
pageSize?: number;
|
|
2069
|
+
search?: string;
|
|
2070
|
+
sortField?: string;
|
|
2071
|
+
sortOrder?: string;
|
|
2072
|
+
status?: string;
|
|
2073
|
+
} = {}
|
|
2074
|
+
) {
|
|
2075
|
+
const actor = await this.getActorContext(userId);
|
|
2076
|
+
this.ensureCollaborator(actor);
|
|
1783
2077
|
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
2078
|
+
const pagination = this.shouldPaginate(filters)
|
|
2079
|
+
? this.normalizePaginationParams(filters, {
|
|
2080
|
+
defaultSortField: 'name',
|
|
2081
|
+
defaultSortOrder: 'asc',
|
|
2082
|
+
allowedSortFields: ['name', 'code', 'createdAt', 'updatedAt'],
|
|
2083
|
+
})
|
|
2084
|
+
: null;
|
|
2085
|
+
|
|
2086
|
+
const params: unknown[] = [];
|
|
2087
|
+
const where: string[] = [];
|
|
2088
|
+
|
|
2089
|
+
if (filters.status === 'active') {
|
|
2090
|
+
where.push('d.deleted_at IS NULL');
|
|
2091
|
+
} else if (filters.status === 'inactive') {
|
|
2092
|
+
where.push('d.deleted_at IS NOT NULL');
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
if (pagination?.search) {
|
|
2096
|
+
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
2097
|
+
where.push(`(
|
|
2098
|
+
COALESCE(d.name, '') ILIKE ${searchPlaceholder}
|
|
2099
|
+
OR COALESCE(d.code, '') ILIKE ${searchPlaceholder}
|
|
2100
|
+
OR COALESCE(d.description, '') ILIKE ${searchPlaceholder}
|
|
2101
|
+
)`);
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
const whereClause = where.length ? `WHERE ${where.join(' AND ')}` : '';
|
|
2105
|
+
const baseQuery = `SELECT d.id,
|
|
2106
|
+
d.slug,
|
|
2107
|
+
d.code,
|
|
2108
|
+
d.name,
|
|
2109
|
+
d.description,
|
|
2110
|
+
CASE WHEN d.deleted_at IS NULL THEN 'active' ELSE 'inactive' END AS status,
|
|
2111
|
+
COUNT(DISTINCT c.id)::int AS "collaboratorCount",
|
|
2112
|
+
d.created_at AS "createdAt",
|
|
2113
|
+
d.updated_at AS "updatedAt"
|
|
2114
|
+
FROM operations_department d
|
|
2115
|
+
LEFT JOIN operations_collaborator c
|
|
2116
|
+
ON c.deleted_at IS NULL
|
|
2117
|
+
AND (
|
|
2118
|
+
c.department_id = d.id
|
|
2119
|
+
OR (
|
|
2120
|
+
c.department_id IS NULL
|
|
2121
|
+
AND LOWER(COALESCE(c.department, '')) = LOWER(d.name)
|
|
1803
2122
|
)
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
2123
|
+
)
|
|
2124
|
+
${whereClause}
|
|
2125
|
+
GROUP BY d.id`;
|
|
2126
|
+
|
|
2127
|
+
if (!pagination) {
|
|
2128
|
+
return this.queryRows(
|
|
2129
|
+
`${baseQuery}
|
|
2130
|
+
ORDER BY CASE WHEN d.deleted_at IS NULL THEN 0 ELSE 1 END ASC,
|
|
2131
|
+
d.name ASC`,
|
|
2132
|
+
params
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
const totalRow = await this.querySingle<{ total: string }>(
|
|
2137
|
+
`SELECT COUNT(*)::text AS total
|
|
2138
|
+
FROM operations_department d
|
|
2139
|
+
${whereClause}`,
|
|
2140
|
+
params
|
|
2141
|
+
);
|
|
2142
|
+
|
|
2143
|
+
const sortColumn =
|
|
2144
|
+
{
|
|
2145
|
+
name: 'd.name',
|
|
2146
|
+
code: 'd.code',
|
|
2147
|
+
createdAt: 'd.created_at',
|
|
2148
|
+
updatedAt: 'd.updated_at',
|
|
2149
|
+
}[pagination.sortField] ?? 'd.name';
|
|
2150
|
+
|
|
2151
|
+
const queryParams = [...params];
|
|
2152
|
+
const limitPlaceholder = this.param(queryParams, pagination.pageSize);
|
|
2153
|
+
const offsetPlaceholder = this.param(queryParams, pagination.offset);
|
|
2154
|
+
|
|
2155
|
+
const rows = await this.queryRows(
|
|
2156
|
+
`${baseQuery}
|
|
2157
|
+
ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, d.id ASC
|
|
2158
|
+
LIMIT ${limitPlaceholder}
|
|
2159
|
+
OFFSET ${offsetPlaceholder}`,
|
|
2160
|
+
queryParams
|
|
2161
|
+
);
|
|
2162
|
+
|
|
2163
|
+
return this.buildPaginationResult(
|
|
2164
|
+
rows,
|
|
2165
|
+
Number(totalRow?.total ?? 0),
|
|
2166
|
+
pagination.page,
|
|
2167
|
+
pagination.pageSize
|
|
1807
2168
|
);
|
|
1808
2169
|
}
|
|
1809
2170
|
|
|
@@ -2003,7 +2364,17 @@ export class OperationsService {
|
|
|
2003
2364
|
});
|
|
2004
2365
|
}
|
|
2005
2366
|
|
|
2006
|
-
async listProjects(
|
|
2367
|
+
async listProjects(
|
|
2368
|
+
userId: number,
|
|
2369
|
+
filters: {
|
|
2370
|
+
page?: number;
|
|
2371
|
+
pageSize?: number;
|
|
2372
|
+
search?: string;
|
|
2373
|
+
sortField?: string;
|
|
2374
|
+
sortOrder?: string;
|
|
2375
|
+
status?: string;
|
|
2376
|
+
} = {}
|
|
2377
|
+
) {
|
|
2007
2378
|
const actor = await this.getActorContext(userId);
|
|
2008
2379
|
const filter = this.buildIdFilter(actor.visibleProjectIds, 'p.id', actor.isDirector);
|
|
2009
2380
|
const assignmentParams: unknown[] = [];
|
|
@@ -2018,9 +2389,34 @@ export class OperationsService {
|
|
|
2018
2389
|
)} THEN pa.role_label END) AS "myRoleLabel",`
|
|
2019
2390
|
: `NULL::int AS "myAssignmentId",
|
|
2020
2391
|
NULL::varchar AS "myRoleLabel",`;
|
|
2392
|
+
const pagination = this.shouldPaginate(filters)
|
|
2393
|
+
? this.normalizePaginationParams(filters, {
|
|
2394
|
+
defaultSortField: 'name',
|
|
2395
|
+
defaultSortOrder: 'asc',
|
|
2396
|
+
allowedSortFields: ['name', 'code', 'clientName', 'startDate', 'endDate', 'status'],
|
|
2397
|
+
})
|
|
2398
|
+
: null;
|
|
2021
2399
|
|
|
2022
|
-
|
|
2023
|
-
|
|
2400
|
+
const params: unknown[] = [...assignmentParams, ...filter.params];
|
|
2401
|
+
const where = ['p.deleted_at IS NULL', filter.clause];
|
|
2402
|
+
|
|
2403
|
+
if (filters.status && filters.status !== 'all') {
|
|
2404
|
+
where.push(`p.status::text = ${this.param(params, filters.status)}`);
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
if (pagination?.search) {
|
|
2408
|
+
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
2409
|
+
where.push(`(
|
|
2410
|
+
COALESCE(p.name, '') ILIKE ${searchPlaceholder}
|
|
2411
|
+
OR COALESCE(p.code, '') ILIKE ${searchPlaceholder}
|
|
2412
|
+
OR COALESCE(p.client_name, '') ILIKE ${searchPlaceholder}
|
|
2413
|
+
OR COALESCE(c.name, '') ILIKE ${searchPlaceholder}
|
|
2414
|
+
OR COALESCE(m.display_name, '') ILIKE ${searchPlaceholder}
|
|
2415
|
+
)`);
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
const whereClause = where.join(' AND ');
|
|
2419
|
+
const baseQuery = `SELECT p.id,
|
|
2024
2420
|
p.contract_id AS "contractId",
|
|
2025
2421
|
p.manager_collaborator_id AS "managerCollaboratorId",
|
|
2026
2422
|
p.code,
|
|
@@ -2045,10 +2441,49 @@ export class OperationsService {
|
|
|
2045
2441
|
ON pa.project_id = p.id
|
|
2046
2442
|
AND pa.deleted_at IS NULL
|
|
2047
2443
|
AND pa.status IN ('planned', 'active')
|
|
2048
|
-
WHERE
|
|
2049
|
-
GROUP BY p.id, c.id, m.id
|
|
2050
|
-
|
|
2051
|
-
|
|
2444
|
+
WHERE ${whereClause}
|
|
2445
|
+
GROUP BY p.id, c.id, m.id`;
|
|
2446
|
+
|
|
2447
|
+
if (!pagination) {
|
|
2448
|
+
return this.queryRows(`${baseQuery} ORDER BY p.name ASC`, params);
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
const totalRow = await this.querySingle<{ total: string }>(
|
|
2452
|
+
`SELECT COUNT(*)::text AS total
|
|
2453
|
+
FROM operations_project p
|
|
2454
|
+
LEFT JOIN operations_contract c ON c.id = p.contract_id
|
|
2455
|
+
LEFT JOIN operations_collaborator m ON m.id = p.manager_collaborator_id
|
|
2456
|
+
WHERE ${whereClause}`,
|
|
2457
|
+
params
|
|
2458
|
+
);
|
|
2459
|
+
|
|
2460
|
+
const sortColumn =
|
|
2461
|
+
{
|
|
2462
|
+
name: 'p.name',
|
|
2463
|
+
code: 'p.code',
|
|
2464
|
+
clientName: 'p.client_name',
|
|
2465
|
+
startDate: 'p.start_date',
|
|
2466
|
+
endDate: 'p.end_date',
|
|
2467
|
+
status: 'p.status',
|
|
2468
|
+
}[pagination.sortField] ?? 'p.name';
|
|
2469
|
+
|
|
2470
|
+
const queryParams = [...params];
|
|
2471
|
+
const limitPlaceholder = this.param(queryParams, pagination.pageSize);
|
|
2472
|
+
const offsetPlaceholder = this.param(queryParams, pagination.offset);
|
|
2473
|
+
|
|
2474
|
+
const rows = await this.queryRows(
|
|
2475
|
+
`${baseQuery}
|
|
2476
|
+
ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, p.id ASC
|
|
2477
|
+
LIMIT ${limitPlaceholder}
|
|
2478
|
+
OFFSET ${offsetPlaceholder}`,
|
|
2479
|
+
queryParams
|
|
2480
|
+
);
|
|
2481
|
+
|
|
2482
|
+
return this.buildPaginationResult(
|
|
2483
|
+
rows,
|
|
2484
|
+
Number(totalRow?.total ?? 0),
|
|
2485
|
+
pagination.page,
|
|
2486
|
+
pagination.pageSize
|
|
2052
2487
|
);
|
|
2053
2488
|
}
|
|
2054
2489
|
|
|
@@ -2221,7 +2656,7 @@ export class OperationsService {
|
|
|
2221
2656
|
}
|
|
2222
2657
|
|
|
2223
2658
|
if (paginationParams.status) {
|
|
2224
|
-
filters.push(`t.status = ${this.param(params, paginationParams.status)}`);
|
|
2659
|
+
filters.push(`t.status::text = ${this.param(params, paginationParams.status)}`);
|
|
2225
2660
|
}
|
|
2226
2661
|
|
|
2227
2662
|
const whereClause = filters.join(' AND ');
|
|
@@ -2980,38 +3415,6 @@ export class OperationsService {
|
|
|
2980
3415
|
);
|
|
2981
3416
|
}
|
|
2982
3417
|
|
|
2983
|
-
if (!data.contractId && data.autoGenerateContractDraft !== false) {
|
|
2984
|
-
const contractId = await this.createProjectContractDraft(
|
|
2985
|
-
tx as any,
|
|
2986
|
-
actor.userId,
|
|
2987
|
-
{
|
|
2988
|
-
projectId,
|
|
2989
|
-
contractTemplateId: data.contractTemplateId ?? null,
|
|
2990
|
-
projectCode: data.code,
|
|
2991
|
-
projectName: data.name,
|
|
2992
|
-
clientName: data.clientName ?? data.name,
|
|
2993
|
-
managerCollaboratorId: data.managerCollaboratorId ?? null,
|
|
2994
|
-
startDate: data.startDate ?? null,
|
|
2995
|
-
endDate: data.endDate ?? null,
|
|
2996
|
-
budgetAmount: data.budgetAmount ?? null,
|
|
2997
|
-
monthlyHourCap: data.monthlyHourCap ?? null,
|
|
2998
|
-
billingModel: data.billingModel ?? 'time_and_material',
|
|
2999
|
-
contractCode: data.contractCode ?? null,
|
|
3000
|
-
contractName: data.contractName ?? null,
|
|
3001
|
-
description: data.contractDescription ?? data.summary ?? null,
|
|
3002
|
-
}
|
|
3003
|
-
);
|
|
3004
|
-
|
|
3005
|
-
await (tx as any).$executeRawUnsafe(
|
|
3006
|
-
`UPDATE operations_project
|
|
3007
|
-
SET contract_id = $1,
|
|
3008
|
-
updated_at = NOW()
|
|
3009
|
-
WHERE id = $2`,
|
|
3010
|
-
contractId,
|
|
3011
|
-
projectId
|
|
3012
|
-
);
|
|
3013
|
-
}
|
|
3014
|
-
|
|
3015
3418
|
return projectId;
|
|
3016
3419
|
});
|
|
3017
3420
|
|
|
@@ -3075,59 +3478,8 @@ export class OperationsService {
|
|
|
3075
3478
|
data.contractId !== undefined
|
|
3076
3479
|
? data.contractId
|
|
3077
3480
|
: (currentProject.relatedContract?.id ?? null);
|
|
3078
|
-
const shouldGenerateDraft =
|
|
3079
|
-
!nextContractId && data.autoGenerateContractDraft === true;
|
|
3080
|
-
|
|
3081
|
-
if (shouldGenerateDraft) {
|
|
3082
|
-
const contractId = await this.createProjectContractDraft(
|
|
3083
|
-
tx as any,
|
|
3084
|
-
actor.userId,
|
|
3085
|
-
{
|
|
3086
|
-
projectId,
|
|
3087
|
-
contractTemplateId: data.contractTemplateId ?? null,
|
|
3088
|
-
projectCode: data.code ?? currentProject.code,
|
|
3089
|
-
projectName: data.name ?? currentProject.name,
|
|
3090
|
-
clientName:
|
|
3091
|
-
data.clientName ??
|
|
3092
|
-
currentProject.clientName ??
|
|
3093
|
-
currentProject.name,
|
|
3094
|
-
managerCollaboratorId:
|
|
3095
|
-
data.managerCollaboratorId ??
|
|
3096
|
-
currentProject.managerCollaboratorId ??
|
|
3097
|
-
null,
|
|
3098
|
-
startDate: data.startDate ?? currentProject.startDate ?? null,
|
|
3099
|
-
endDate: data.endDate ?? currentProject.endDate ?? null,
|
|
3100
|
-
budgetAmount: data.budgetAmount ?? currentProject.budgetAmount ?? null,
|
|
3101
|
-
monthlyHourCap:
|
|
3102
|
-
data.monthlyHourCap ??
|
|
3103
|
-
currentProject.relatedContract?.monthlyHourCap ??
|
|
3104
|
-
null,
|
|
3105
|
-
billingModel:
|
|
3106
|
-
data.billingModel ??
|
|
3107
|
-
currentProject.relatedContract?.billingModel ??
|
|
3108
|
-
'time_and_material',
|
|
3109
|
-
contractCode:
|
|
3110
|
-
data.contractCode ?? currentProject.relatedContract?.code ?? null,
|
|
3111
|
-
contractName:
|
|
3112
|
-
data.contractName ?? currentProject.relatedContract?.name ?? null,
|
|
3113
|
-
description:
|
|
3114
|
-
data.contractDescription ??
|
|
3115
|
-
currentProject.relatedContract?.description ??
|
|
3116
|
-
data.summary ??
|
|
3117
|
-
currentProject.summary ??
|
|
3118
|
-
null,
|
|
3119
|
-
}
|
|
3120
|
-
);
|
|
3121
3481
|
|
|
3122
|
-
|
|
3123
|
-
`UPDATE operations_project
|
|
3124
|
-
SET contract_id = $1,
|
|
3125
|
-
updated_at = NOW()
|
|
3126
|
-
WHERE id = $2`,
|
|
3127
|
-
contractId,
|
|
3128
|
-
projectId
|
|
3129
|
-
);
|
|
3130
|
-
} else if (
|
|
3482
|
+
if (
|
|
3131
3483
|
nextContractId &&
|
|
3132
3484
|
(data.monthlyHourCap !== undefined || data.billingModel !== undefined)
|
|
3133
3485
|
) {
|
|
@@ -3166,214 +3518,177 @@ export class OperationsService {
|
|
|
3166
3518
|
return this.getProjectById(userId, projectId);
|
|
3167
3519
|
}
|
|
3168
3520
|
|
|
3169
|
-
async
|
|
3521
|
+
async listContracts(
|
|
3522
|
+
userId: number,
|
|
3523
|
+
filters: {
|
|
3524
|
+
page?: number;
|
|
3525
|
+
pageSize?: number;
|
|
3526
|
+
search?: string;
|
|
3527
|
+
sortField?: string;
|
|
3528
|
+
sortOrder?: string;
|
|
3529
|
+
status?: string;
|
|
3530
|
+
contractCategory?: string;
|
|
3531
|
+
originType?: string;
|
|
3532
|
+
isActive?: string;
|
|
3533
|
+
} = {}
|
|
3534
|
+
) {
|
|
3170
3535
|
const actor = await this.getActorContext(userId);
|
|
3171
|
-
|
|
3536
|
+
const params: unknown[] = [];
|
|
3537
|
+
const accessClause = actor.isDirector
|
|
3538
|
+
? 'c.deleted_at IS NULL'
|
|
3539
|
+
: `c.deleted_at IS NULL AND (
|
|
3540
|
+
c.related_collaborator_id = ANY(${this.param(params, actor.visibleCollaboratorIds)}::int[])
|
|
3541
|
+
OR EXISTS (
|
|
3542
|
+
SELECT 1
|
|
3543
|
+
FROM operations_project p_access
|
|
3544
|
+
WHERE p_access.contract_id = c.id
|
|
3545
|
+
AND p_access.deleted_at IS NULL
|
|
3546
|
+
AND p_access.id = ANY(${this.param(params, actor.visibleProjectIds)}::int[])
|
|
3547
|
+
)
|
|
3548
|
+
)`;
|
|
3549
|
+
const pagination = this.shouldPaginate(filters)
|
|
3550
|
+
? this.normalizePaginationParams(filters, {
|
|
3551
|
+
defaultSortField: 'name',
|
|
3552
|
+
defaultSortOrder: 'asc',
|
|
3553
|
+
allowedSortFields: ['name', 'code', 'clientName', 'startDate', 'endDate', 'status'],
|
|
3554
|
+
})
|
|
3555
|
+
: null;
|
|
3556
|
+
const where = [accessClause];
|
|
3172
3557
|
|
|
3173
|
-
|
|
3174
|
-
`
|
|
3175
|
-
|
|
3176
|
-
t.code,
|
|
3177
|
-
t.name,
|
|
3178
|
-
t.description,
|
|
3179
|
-
t.contract_category AS "contractCategory",
|
|
3180
|
-
t.contract_type AS "contractType",
|
|
3181
|
-
t.billing_model AS "billingModel",
|
|
3182
|
-
t.signature_status AS "signatureStatus",
|
|
3183
|
-
t.is_active AS "isActive",
|
|
3184
|
-
t.status,
|
|
3185
|
-
t.content_html AS "contentHtml",
|
|
3186
|
-
t.created_at AS "createdAt",
|
|
3187
|
-
t.updated_at AS "updatedAt",
|
|
3188
|
-
COUNT(DISTINCT c.id)::int AS "usageCount"
|
|
3189
|
-
FROM operations_contract_template t
|
|
3190
|
-
LEFT JOIN operations_contract c
|
|
3191
|
-
ON c.contract_template_id = t.id
|
|
3192
|
-
AND c.deleted_at IS NULL
|
|
3193
|
-
WHERE t.deleted_at IS NULL
|
|
3194
|
-
GROUP BY t.id
|
|
3195
|
-
ORDER BY CASE
|
|
3196
|
-
WHEN t.status = 'active' THEN 0
|
|
3197
|
-
WHEN t.status = 'draft' THEN 1
|
|
3198
|
-
WHEN t.status = 'inactive' THEN 2
|
|
3199
|
-
ELSE 3
|
|
3200
|
-
END,
|
|
3201
|
-
t.name ASC`
|
|
3202
|
-
);
|
|
3203
|
-
}
|
|
3558
|
+
if (filters.status && filters.status !== 'all') {
|
|
3559
|
+
where.push(`c.status::text = ${this.param(params, filters.status)}`);
|
|
3560
|
+
}
|
|
3204
3561
|
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3562
|
+
if (filters.contractCategory && filters.contractCategory !== 'all') {
|
|
3563
|
+
where.push(
|
|
3564
|
+
`c.contract_category = ${this.param(params, filters.contractCategory)}::text::operations_contract_contract_category_70d553ea09_enum`
|
|
3565
|
+
);
|
|
3566
|
+
}
|
|
3208
3567
|
|
|
3209
|
-
|
|
3210
|
-
|
|
3568
|
+
if (filters.originType && filters.originType !== 'all') {
|
|
3569
|
+
where.push(
|
|
3570
|
+
`c.origin_type = ${this.param(params, filters.originType)}::text::operations_contract_origin_type_07a7cc2b5d_enum`
|
|
3571
|
+
);
|
|
3572
|
+
}
|
|
3211
3573
|
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3574
|
+
if (filters.isActive === 'true' || filters.isActive === 'false') {
|
|
3575
|
+
where.push(`c.is_active = ${this.param(params, filters.isActive === 'true')}`);
|
|
3576
|
+
}
|
|
3215
3577
|
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3578
|
+
if (pagination?.search) {
|
|
3579
|
+
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
3580
|
+
where.push(`(
|
|
3581
|
+
COALESCE(c.name, '') ILIKE ${searchPlaceholder}
|
|
3582
|
+
OR COALESCE(c.code, '') ILIKE ${searchPlaceholder}
|
|
3583
|
+
OR COALESCE(c.client_name, '') ILIKE ${searchPlaceholder}
|
|
3584
|
+
OR COALESCE(m.display_name, '') ILIKE ${searchPlaceholder}
|
|
3585
|
+
OR COALESCE(linked.display_name, '') ILIKE ${searchPlaceholder}
|
|
3586
|
+
)`);
|
|
3219
3587
|
}
|
|
3220
3588
|
|
|
3221
|
-
|
|
3222
|
-
|
|
3589
|
+
const whereClause = where.join(' AND ');
|
|
3590
|
+
const baseQuery = `SELECT c.id,
|
|
3591
|
+
c.code,
|
|
3592
|
+
c.name,
|
|
3593
|
+
c.contract_category AS "contractCategory",
|
|
3594
|
+
c.contract_type AS "contractType",
|
|
3595
|
+
c.client_name AS "clientName",
|
|
3596
|
+
c.signature_status AS "signatureStatus",
|
|
3597
|
+
c.is_active AS "isActive",
|
|
3598
|
+
c.billing_model AS "billingModel",
|
|
3599
|
+
c.account_manager_collaborator_id AS "accountManagerCollaboratorId",
|
|
3600
|
+
c.related_collaborator_id AS "relatedCollaboratorId",
|
|
3601
|
+
c.origin_type AS "originType",
|
|
3602
|
+
c.origin_id AS "originId",
|
|
3603
|
+
c.start_date AS "startDate",
|
|
3604
|
+
c.end_date AS "endDate",
|
|
3605
|
+
c.signed_at AS "signedAt",
|
|
3606
|
+
c.effective_date AS "effectiveDate",
|
|
3607
|
+
c.budget_amount AS "budgetAmount",
|
|
3608
|
+
c.monthly_hour_cap AS "monthlyHourCap",
|
|
3609
|
+
c.status,
|
|
3610
|
+
c.creation_mode AS "creationMode",
|
|
3611
|
+
c.wizard_step AS "wizardStep",
|
|
3612
|
+
c.description,
|
|
3613
|
+
m.display_name AS "accountManagerName",
|
|
3614
|
+
linked.display_name AS "relatedCollaboratorName",
|
|
3615
|
+
MAX(COALESCE(primary_party.display_name, linked.display_name, c.client_name)) AS "mainRelatedPartyName",
|
|
3616
|
+
MAX(COALESCE(pdf_document.file_name, '')) AS "currentPdfFileName",
|
|
3617
|
+
COUNT(DISTINCT p.id)::int AS "projectCount"
|
|
3618
|
+
FROM operations_contract c
|
|
3619
|
+
LEFT JOIN operations_collaborator m ON m.id = c.account_manager_collaborator_id
|
|
3620
|
+
LEFT JOIN operations_collaborator linked ON linked.id = c.related_collaborator_id
|
|
3621
|
+
LEFT JOIN LATERAL (
|
|
3622
|
+
SELECT cp.display_name
|
|
3623
|
+
FROM operations_contract_party cp
|
|
3624
|
+
WHERE cp.contract_id = c.id
|
|
3625
|
+
AND cp.deleted_at IS NULL
|
|
3626
|
+
ORDER BY cp.is_primary DESC, cp.id ASC
|
|
3627
|
+
LIMIT 1
|
|
3628
|
+
) primary_party ON TRUE
|
|
3629
|
+
LEFT JOIN LATERAL (
|
|
3630
|
+
SELECT cd.file_name
|
|
3631
|
+
FROM operations_contract_document cd
|
|
3632
|
+
WHERE cd.contract_id = c.id
|
|
3633
|
+
AND cd.deleted_at IS NULL
|
|
3634
|
+
AND cd.is_current = true
|
|
3635
|
+
AND cd.document_type IN ('source_upload', 'generated_pdf')
|
|
3636
|
+
ORDER BY cd.id DESC
|
|
3637
|
+
LIMIT 1
|
|
3638
|
+
) pdf_document ON TRUE
|
|
3639
|
+
LEFT JOIN operations_project p
|
|
3640
|
+
ON p.contract_id = c.id
|
|
3641
|
+
AND p.deleted_at IS NULL
|
|
3642
|
+
WHERE ${whereClause}
|
|
3643
|
+
GROUP BY c.id, m.id, linked.id`;
|
|
3223
3644
|
|
|
3224
|
-
|
|
3225
|
-
|
|
3645
|
+
if (!pagination) {
|
|
3646
|
+
return this.queryRows(
|
|
3647
|
+
`${baseQuery}
|
|
3648
|
+
ORDER BY COALESCE(c.name, c.code, CONCAT('draft-', c.id)) ASC`,
|
|
3649
|
+
params
|
|
3650
|
+
);
|
|
3651
|
+
}
|
|
3226
3652
|
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3653
|
+
const totalRow = await this.querySingle<{ total: string }>(
|
|
3654
|
+
`SELECT COUNT(*)::text AS total
|
|
3655
|
+
FROM operations_contract c
|
|
3656
|
+
LEFT JOIN operations_collaborator m ON m.id = c.account_manager_collaborator_id
|
|
3657
|
+
LEFT JOIN operations_collaborator linked ON linked.id = c.related_collaborator_id
|
|
3658
|
+
WHERE ${whereClause}`,
|
|
3659
|
+
params
|
|
3660
|
+
);
|
|
3230
3661
|
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3662
|
+
const sortColumn =
|
|
3663
|
+
{
|
|
3664
|
+
name: `COALESCE(c.name, c.code, CONCAT('draft-', c.id))`,
|
|
3665
|
+
code: 'c.code',
|
|
3666
|
+
clientName: 'c.client_name',
|
|
3667
|
+
startDate: 'c.start_date',
|
|
3668
|
+
endDate: 'c.end_date',
|
|
3669
|
+
status: 'c.status',
|
|
3670
|
+
}[pagination.sortField] ?? `COALESCE(c.name, c.code, CONCAT('draft-', c.id))`;
|
|
3234
3671
|
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
is_active,
|
|
3246
|
-
status,
|
|
3247
|
-
content_html,
|
|
3248
|
-
created_at,
|
|
3249
|
-
updated_at
|
|
3250
|
-
) VALUES (
|
|
3251
|
-
$1, $2, $3, $4,
|
|
3252
|
-
$5::text::operations_contract_template_contract_category_49bb07a713_enum,
|
|
3253
|
-
$6::text::operations_contract_template_contract_type_3962dbda6a_enum,
|
|
3254
|
-
$7::text::operations_contract_template_billing_model_384a7c60e2_enum,
|
|
3255
|
-
$8::text::operations_contract_template_signature_status_56cb6d625b_enum,
|
|
3256
|
-
$9, $10::text::operations_contract_template_status_c9d2e90231_enum, $11,
|
|
3257
|
-
NOW(), NOW()
|
|
3258
|
-
)
|
|
3259
|
-
RETURNING id`,
|
|
3260
|
-
await this.generateUniqueContractTemplateSlug(tx as any, name),
|
|
3261
|
-
nextCode,
|
|
3262
|
-
name,
|
|
3263
|
-
this.normalizeOptionalText(data.description),
|
|
3264
|
-
data.contractCategory ?? 'client',
|
|
3265
|
-
data.contractType ?? 'service_agreement',
|
|
3266
|
-
data.billingModel ?? 'time_and_material',
|
|
3267
|
-
data.signatureStatus ?? 'not_started',
|
|
3268
|
-
isActive,
|
|
3269
|
-
nextStatus,
|
|
3270
|
-
this.normalizeOptionalText(data.contentHtml)
|
|
3271
|
-
)) as { id: number }[];
|
|
3272
|
-
|
|
3273
|
-
const templateId = created[0]?.id;
|
|
3274
|
-
if (!templateId) {
|
|
3275
|
-
throw new BadRequestException('Unable to create the contract template.');
|
|
3276
|
-
}
|
|
3277
|
-
|
|
3278
|
-
return this.getContractTemplateRecord(tx as any, templateId, true);
|
|
3279
|
-
});
|
|
3280
|
-
}
|
|
3281
|
-
|
|
3282
|
-
async updateContractTemplate(
|
|
3283
|
-
userId: number,
|
|
3284
|
-
templateId: number,
|
|
3285
|
-
data: Partial<ContractTemplatePayload>
|
|
3286
|
-
) {
|
|
3287
|
-
const actor = await this.getActorContext(userId);
|
|
3288
|
-
this.ensureDirector(actor);
|
|
3289
|
-
|
|
3290
|
-
return this.prisma.$transaction(async (tx) => {
|
|
3291
|
-
const current = await this.getContractTemplateRecord(
|
|
3292
|
-
tx as any,
|
|
3293
|
-
templateId,
|
|
3294
|
-
true
|
|
3295
|
-
);
|
|
3296
|
-
|
|
3297
|
-
const nextName =
|
|
3298
|
-
data.name !== undefined
|
|
3299
|
-
? this.normalizeOptionalText(data.name)
|
|
3300
|
-
: current.name;
|
|
3301
|
-
|
|
3302
|
-
if (!nextName) {
|
|
3303
|
-
throw new BadRequestException('Contract template name is required.');
|
|
3304
|
-
}
|
|
3305
|
-
|
|
3306
|
-
if (String(nextName).toLowerCase() !== String(current.name).toLowerCase()) {
|
|
3307
|
-
await this.assertContractTemplateNameAvailable(
|
|
3308
|
-
tx as any,
|
|
3309
|
-
nextName,
|
|
3310
|
-
templateId
|
|
3311
|
-
);
|
|
3312
|
-
}
|
|
3313
|
-
|
|
3314
|
-
const nextCode =
|
|
3315
|
-
data.code !== undefined
|
|
3316
|
-
? this.normalizeOptionalText(data.code)?.toUpperCase() ?? null
|
|
3317
|
-
: (current.code ?? null);
|
|
3318
|
-
|
|
3319
|
-
if (nextCode) {
|
|
3320
|
-
await this.assertContractTemplateCodeAvailable(
|
|
3321
|
-
tx as any,
|
|
3322
|
-
nextCode,
|
|
3323
|
-
templateId
|
|
3324
|
-
);
|
|
3325
|
-
}
|
|
3326
|
-
|
|
3327
|
-
const nextStatus = data.status ?? current.status ?? 'active';
|
|
3328
|
-
const nextIsActive =
|
|
3329
|
-
data.isActive ?? !['inactive', 'archived'].includes(nextStatus);
|
|
3330
|
-
const nextSlug =
|
|
3331
|
-
String(nextName).toLowerCase() !== String(current.name).toLowerCase()
|
|
3332
|
-
? await this.generateUniqueContractTemplateSlug(
|
|
3333
|
-
tx as any,
|
|
3334
|
-
nextName,
|
|
3335
|
-
templateId
|
|
3336
|
-
)
|
|
3337
|
-
: current.slug;
|
|
3338
|
-
|
|
3339
|
-
await (tx as any).$executeRawUnsafe(
|
|
3340
|
-
`UPDATE operations_contract_template
|
|
3341
|
-
SET slug = $1,
|
|
3342
|
-
code = $2,
|
|
3343
|
-
name = $3,
|
|
3344
|
-
description = $4,
|
|
3345
|
-
contract_category = $5::text::operations_contract_template_contract_category_49bb07a713_enum,
|
|
3346
|
-
contract_type = $6::text::operations_contract_template_contract_type_3962dbda6a_enum,
|
|
3347
|
-
billing_model = $7::text::operations_contract_template_billing_model_384a7c60e2_enum,
|
|
3348
|
-
signature_status = $8::text::operations_contract_template_signature_status_56cb6d625b_enum,
|
|
3349
|
-
is_active = $9,
|
|
3350
|
-
status = $10::text::operations_contract_template_status_c9d2e90231_enum,
|
|
3351
|
-
content_html = $11,
|
|
3352
|
-
updated_at = NOW()
|
|
3353
|
-
WHERE id = $12`,
|
|
3354
|
-
nextSlug,
|
|
3355
|
-
nextCode,
|
|
3356
|
-
nextName,
|
|
3357
|
-
data.description !== undefined
|
|
3358
|
-
? this.normalizeOptionalText(data.description)
|
|
3359
|
-
: (current.description ?? null),
|
|
3360
|
-
data.contractCategory ?? current.contractCategory ?? 'client',
|
|
3361
|
-
data.contractType ?? current.contractType ?? 'service_agreement',
|
|
3362
|
-
data.billingModel ?? current.billingModel ?? 'time_and_material',
|
|
3363
|
-
data.signatureStatus ?? current.signatureStatus ?? 'not_started',
|
|
3364
|
-
nextIsActive,
|
|
3365
|
-
nextStatus,
|
|
3366
|
-
data.contentHtml !== undefined
|
|
3367
|
-
? this.normalizeOptionalText(data.contentHtml)
|
|
3368
|
-
: (current.contentHtml ?? null),
|
|
3369
|
-
templateId
|
|
3370
|
-
);
|
|
3672
|
+
const queryParams = [...params];
|
|
3673
|
+
const limitPlaceholder = this.param(queryParams, pagination.pageSize);
|
|
3674
|
+
const offsetPlaceholder = this.param(queryParams, pagination.offset);
|
|
3675
|
+
const rows = await this.queryRows(
|
|
3676
|
+
`${baseQuery}
|
|
3677
|
+
ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, c.id ASC
|
|
3678
|
+
LIMIT ${limitPlaceholder}
|
|
3679
|
+
OFFSET ${offsetPlaceholder}`,
|
|
3680
|
+
queryParams
|
|
3681
|
+
);
|
|
3371
3682
|
|
|
3372
|
-
|
|
3373
|
-
|
|
3683
|
+
return this.buildPaginationResult(
|
|
3684
|
+
rows,
|
|
3685
|
+
Number(totalRow?.total ?? 0),
|
|
3686
|
+
pagination.page,
|
|
3687
|
+
pagination.pageSize
|
|
3688
|
+
);
|
|
3374
3689
|
}
|
|
3375
3690
|
|
|
3376
|
-
async
|
|
3691
|
+
async getContractStats(userId: number) {
|
|
3377
3692
|
const actor = await this.getActorContext(userId);
|
|
3378
3693
|
const params: unknown[] = [];
|
|
3379
3694
|
const accessClause = actor.isDirector
|
|
@@ -3389,84 +3704,36 @@ export class OperationsService {
|
|
|
3389
3704
|
)
|
|
3390
3705
|
)`;
|
|
3391
3706
|
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
c.
|
|
3400
|
-
c.is_active AS
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
c.end_date AS "endDate",
|
|
3412
|
-
c.signed_at AS "signedAt",
|
|
3413
|
-
c.effective_date AS "effectiveDate",
|
|
3414
|
-
c.budget_amount AS "budgetAmount",
|
|
3415
|
-
c.monthly_hour_cap AS "monthlyHourCap",
|
|
3416
|
-
c.status,
|
|
3417
|
-
c.creation_mode AS "creationMode",
|
|
3418
|
-
c.wizard_step AS "wizardStep",
|
|
3419
|
-
c.description,
|
|
3420
|
-
m.display_name AS "accountManagerName",
|
|
3421
|
-
linked.display_name AS "relatedCollaboratorName",
|
|
3422
|
-
MAX(COALESCE(primary_party.display_name, linked.display_name, c.client_name)) AS "mainRelatedPartyName",
|
|
3423
|
-
MAX(COALESCE(financials.value_amount, 0)) AS "valueAmount",
|
|
3424
|
-
MAX(COALESCE(financials.payment_amount, 0)) AS "paymentAmount",
|
|
3425
|
-
MAX(COALESCE(financials.revenue_amount, 0)) AS "revenueAmount",
|
|
3426
|
-
MAX(COALESCE(financials.fine_amount, 0)) AS "fineAmount",
|
|
3427
|
-
MAX(COALESCE(pdf_document.file_name, '')) AS "currentPdfFileName",
|
|
3428
|
-
COUNT(DISTINCT p.id)::int AS "projectCount"
|
|
3707
|
+
const stats = await this.querySingle<{
|
|
3708
|
+
total: number | string;
|
|
3709
|
+
active: number | string;
|
|
3710
|
+
inactive: number | string;
|
|
3711
|
+
withFile: number | string;
|
|
3712
|
+
}>(
|
|
3713
|
+
`SELECT COUNT(*)::int AS total,
|
|
3714
|
+
COUNT(*) FILTER (WHERE c.is_active = true)::int AS active,
|
|
3715
|
+
COUNT(*) FILTER (WHERE c.is_active = false)::int AS inactive,
|
|
3716
|
+
COUNT(*) FILTER (
|
|
3717
|
+
WHERE EXISTS (
|
|
3718
|
+
SELECT 1
|
|
3719
|
+
FROM operations_contract_document cd
|
|
3720
|
+
WHERE cd.contract_id = c.id
|
|
3721
|
+
AND cd.deleted_at IS NULL
|
|
3722
|
+
AND cd.is_current = true
|
|
3723
|
+
AND cd.document_type IN ('source_upload', 'generated_pdf')
|
|
3724
|
+
)
|
|
3725
|
+
)::int AS "withFile"
|
|
3429
3726
|
FROM operations_contract c
|
|
3430
|
-
|
|
3431
|
-
LEFT JOIN operations_collaborator linked ON linked.id = c.related_collaborator_id
|
|
3432
|
-
LEFT JOIN operations_contract_template template
|
|
3433
|
-
ON template.id = c.contract_template_id
|
|
3434
|
-
LEFT JOIN LATERAL (
|
|
3435
|
-
SELECT cp.display_name
|
|
3436
|
-
FROM operations_contract_party cp
|
|
3437
|
-
WHERE cp.contract_id = c.id
|
|
3438
|
-
AND cp.deleted_at IS NULL
|
|
3439
|
-
ORDER BY cp.is_primary DESC, cp.id ASC
|
|
3440
|
-
LIMIT 1
|
|
3441
|
-
) primary_party ON TRUE
|
|
3442
|
-
LEFT JOIN LATERAL (
|
|
3443
|
-
SELECT
|
|
3444
|
-
SUM(CASE WHEN term_type = 'value' THEN amount ELSE 0 END) AS value_amount,
|
|
3445
|
-
SUM(CASE WHEN term_type = 'payment' THEN amount ELSE 0 END) AS payment_amount,
|
|
3446
|
-
SUM(CASE WHEN term_type = 'revenue' THEN amount ELSE 0 END) AS revenue_amount,
|
|
3447
|
-
SUM(CASE WHEN term_type = 'fine' THEN amount ELSE 0 END) AS fine_amount
|
|
3448
|
-
FROM operations_contract_financial_term ft
|
|
3449
|
-
WHERE ft.contract_id = c.id
|
|
3450
|
-
AND ft.deleted_at IS NULL
|
|
3451
|
-
) financials ON TRUE
|
|
3452
|
-
LEFT JOIN LATERAL (
|
|
3453
|
-
SELECT cd.file_name
|
|
3454
|
-
FROM operations_contract_document cd
|
|
3455
|
-
WHERE cd.contract_id = c.id
|
|
3456
|
-
AND cd.deleted_at IS NULL
|
|
3457
|
-
AND cd.is_current = true
|
|
3458
|
-
AND cd.document_type IN ('source_upload', 'generated_pdf')
|
|
3459
|
-
ORDER BY cd.id DESC
|
|
3460
|
-
LIMIT 1
|
|
3461
|
-
) pdf_document ON TRUE
|
|
3462
|
-
LEFT JOIN operations_project p
|
|
3463
|
-
ON p.contract_id = c.id
|
|
3464
|
-
AND p.deleted_at IS NULL
|
|
3465
|
-
WHERE ${accessClause}
|
|
3466
|
-
GROUP BY c.id, m.id, linked.id, template.id
|
|
3467
|
-
ORDER BY COALESCE(c.name, c.code, CONCAT('draft-', c.id)) ASC`,
|
|
3727
|
+
WHERE ${accessClause}`,
|
|
3468
3728
|
params
|
|
3469
3729
|
);
|
|
3730
|
+
|
|
3731
|
+
return {
|
|
3732
|
+
total: Number(stats?.total ?? 0),
|
|
3733
|
+
active: Number(stats?.active ?? 0),
|
|
3734
|
+
inactive: Number(stats?.inactive ?? 0),
|
|
3735
|
+
withFile: Number(stats?.withFile ?? 0),
|
|
3736
|
+
};
|
|
3470
3737
|
}
|
|
3471
3738
|
|
|
3472
3739
|
async getContractById(
|
|
@@ -3486,10 +3753,6 @@ export class OperationsService {
|
|
|
3486
3753
|
c.billing_model AS "billingModel",
|
|
3487
3754
|
c.account_manager_collaborator_id AS "accountManagerCollaboratorId",
|
|
3488
3755
|
c.related_collaborator_id AS "relatedCollaboratorId",
|
|
3489
|
-
c.contract_template_id AS "contractTemplateId",
|
|
3490
|
-
template.name AS "contractTemplateName",
|
|
3491
|
-
template.slug AS "contractTemplateSlug",
|
|
3492
|
-
template.code AS "contractTemplateCode",
|
|
3493
3756
|
c.origin_type AS "originType",
|
|
3494
3757
|
c.origin_id AS "originId",
|
|
3495
3758
|
c.start_date AS "startDate",
|
|
@@ -3508,8 +3771,6 @@ export class OperationsService {
|
|
|
3508
3771
|
FROM operations_contract c
|
|
3509
3772
|
LEFT JOIN operations_collaborator m ON m.id = c.account_manager_collaborator_id
|
|
3510
3773
|
LEFT JOIN operations_collaborator linked ON linked.id = c.related_collaborator_id
|
|
3511
|
-
LEFT JOIN operations_contract_template template
|
|
3512
|
-
ON template.id = c.contract_template_id
|
|
3513
3774
|
WHERE c.id = $1
|
|
3514
3775
|
AND c.deleted_at IS NULL`,
|
|
3515
3776
|
[contractId]
|
|
@@ -3543,7 +3804,7 @@ export class OperationsService {
|
|
|
3543
3804
|
}
|
|
3544
3805
|
}
|
|
3545
3806
|
|
|
3546
|
-
const [projects, scheduleSummary, parties,
|
|
3807
|
+
const [projects, scheduleSummary, parties, documents, history] =
|
|
3547
3808
|
await Promise.all([
|
|
3548
3809
|
this.queryRows<ContractProjectSummary>(
|
|
3549
3810
|
`SELECT id, code, name, status
|
|
@@ -3582,33 +3843,6 @@ export class OperationsService {
|
|
|
3582
3843
|
ORDER BY is_primary DESC, id ASC`,
|
|
3583
3844
|
[contractId]
|
|
3584
3845
|
),
|
|
3585
|
-
this.queryRows<NonNullable<ContractPayload['signatures']>[number]>(
|
|
3586
|
-
`SELECT id,
|
|
3587
|
-
signer_name AS "signerName",
|
|
3588
|
-
signer_role AS "signerRole",
|
|
3589
|
-
signer_email AS "signerEmail",
|
|
3590
|
-
signer_status AS status,
|
|
3591
|
-
signed_at AS "signedAt"
|
|
3592
|
-
FROM operations_contract_signature
|
|
3593
|
-
WHERE contract_id = $1
|
|
3594
|
-
AND deleted_at IS NULL
|
|
3595
|
-
ORDER BY id ASC`,
|
|
3596
|
-
[contractId]
|
|
3597
|
-
),
|
|
3598
|
-
this.queryRows<NonNullable<ContractPayload['financialTerms']>[number]>(
|
|
3599
|
-
`SELECT id,
|
|
3600
|
-
term_type AS "termType",
|
|
3601
|
-
label,
|
|
3602
|
-
amount,
|
|
3603
|
-
recurrence,
|
|
3604
|
-
due_day AS "dueDay",
|
|
3605
|
-
notes
|
|
3606
|
-
FROM operations_contract_financial_term
|
|
3607
|
-
WHERE contract_id = $1
|
|
3608
|
-
AND deleted_at IS NULL
|
|
3609
|
-
ORDER BY id ASC`,
|
|
3610
|
-
[contractId]
|
|
3611
|
-
),
|
|
3612
3846
|
this.queryRows<ContractDocumentRecord>(
|
|
3613
3847
|
`SELECT id,
|
|
3614
3848
|
document_type AS "documentType",
|
|
@@ -3627,19 +3861,6 @@ export class OperationsService {
|
|
|
3627
3861
|
ORDER BY is_current DESC, id DESC`,
|
|
3628
3862
|
[contractId]
|
|
3629
3863
|
),
|
|
3630
|
-
this.queryRows<NonNullable<ContractPayload['revisions']>[number]>(
|
|
3631
|
-
`SELECT id,
|
|
3632
|
-
revision_type AS "revisionType",
|
|
3633
|
-
title,
|
|
3634
|
-
effective_date AS "effectiveDate",
|
|
3635
|
-
status,
|
|
3636
|
-
summary
|
|
3637
|
-
FROM operations_contract_revision
|
|
3638
|
-
WHERE contract_id = $1
|
|
3639
|
-
AND deleted_at IS NULL
|
|
3640
|
-
ORDER BY effective_date DESC NULLS LAST, id DESC`,
|
|
3641
|
-
[contractId]
|
|
3642
|
-
),
|
|
3643
3864
|
this.queryRows<ContractHistoryRecord>(
|
|
3644
3865
|
`SELECT id,
|
|
3645
3866
|
actor_user_id AS "actorUserId",
|
|
@@ -3663,10 +3884,7 @@ export class OperationsService {
|
|
|
3663
3884
|
projects,
|
|
3664
3885
|
scheduleSummary,
|
|
3665
3886
|
parties,
|
|
3666
|
-
signatures,
|
|
3667
|
-
financialTerms,
|
|
3668
3887
|
documents,
|
|
3669
|
-
revisions,
|
|
3670
3888
|
history,
|
|
3671
3889
|
};
|
|
3672
3890
|
}
|
|
@@ -3691,7 +3909,6 @@ export class OperationsService {
|
|
|
3691
3909
|
billing_model,
|
|
3692
3910
|
account_manager_collaborator_id,
|
|
3693
3911
|
related_collaborator_id,
|
|
3694
|
-
contract_template_id,
|
|
3695
3912
|
origin_type,
|
|
3696
3913
|
origin_id,
|
|
3697
3914
|
start_date,
|
|
@@ -3716,513 +3933,67 @@ export class OperationsService {
|
|
|
3716
3933
|
$7,
|
|
3717
3934
|
$8::operations_contract_billing_model_409dc7fea2_enum,
|
|
3718
3935
|
$9,
|
|
3719
|
-
$10,
|
|
3720
|
-
$11,
|
|
3721
|
-
$12
|
|
3722
|
-
$13,
|
|
3723
|
-
$14::date,
|
|
3724
|
-
$
|
|
3725
|
-
$20::
|
|
3726
|
-
$21
|
|
3727
|
-
$22,
|
|
3728
|
-
$23,
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
data.
|
|
3736
|
-
data.
|
|
3737
|
-
|
|
3738
|
-
data.
|
|
3739
|
-
data.
|
|
3740
|
-
data.
|
|
3741
|
-
data.
|
|
3742
|
-
data.
|
|
3743
|
-
data.
|
|
3744
|
-
data.
|
|
3745
|
-
data.
|
|
3746
|
-
|
|
3747
|
-
data.
|
|
3748
|
-
data.
|
|
3749
|
-
data.
|
|
3750
|
-
data.
|
|
3751
|
-
data.
|
|
3752
|
-
data.
|
|
3753
|
-
data.
|
|
3754
|
-
data.
|
|
3755
|
-
data.description ?? null,
|
|
3756
|
-
data.contentHtml ?? null
|
|
3757
|
-
);
|
|
3758
|
-
|
|
3759
|
-
const contractId = (created as { id: number }[])[0]?.id;
|
|
3760
|
-
await this.replaceContractParties(tx as any, contractId, data.parties);
|
|
3761
|
-
await this.replaceContractSignatures(tx as any, contractId, data.signatures);
|
|
3762
|
-
await this.replaceContractFinancialTerms(
|
|
3763
|
-
tx as any,
|
|
3764
|
-
contractId,
|
|
3765
|
-
data.financialTerms
|
|
3766
|
-
);
|
|
3767
|
-
await this.replaceContractRevisions(tx as any, contractId, data.revisions);
|
|
3768
|
-
if (data.replaceUploadedPdfDocument) {
|
|
3769
|
-
await this.replaceContractDocument(
|
|
3770
|
-
tx as any,
|
|
3771
|
-
contractId,
|
|
3772
|
-
'source_upload',
|
|
3773
|
-
data.replaceUploadedPdfDocument
|
|
3774
|
-
);
|
|
3775
|
-
}
|
|
3776
|
-
await this.insertContractHistory(
|
|
3777
|
-
tx as any,
|
|
3778
|
-
contractId,
|
|
3779
|
-
userId,
|
|
3780
|
-
'created',
|
|
3781
|
-
data.originType === 'manual'
|
|
3782
|
-
? 'Manual contract created from registry.'
|
|
3783
|
-
: `Contract registered from origin ${data.originType}.`
|
|
3784
|
-
);
|
|
3785
|
-
return contractId;
|
|
3786
|
-
});
|
|
3787
|
-
|
|
3788
|
-
return this.getContractById(userId, createdId);
|
|
3789
|
-
}
|
|
3790
|
-
|
|
3791
|
-
async createContractDraft(userId: number, data: ContractDraftPayload) {
|
|
3792
|
-
const actor = await this.getActorContext(userId);
|
|
3793
|
-
this.ensureDirector(actor);
|
|
3794
|
-
|
|
3795
|
-
const creationMode = this.normalizeEnumValue(
|
|
3796
|
-
data.creationMode,
|
|
3797
|
-
CONTRACT_CREATION_MODE_VALUES,
|
|
3798
|
-
'blank'
|
|
3799
|
-
);
|
|
3800
|
-
const selectedTemplate =
|
|
3801
|
-
data.templateId && data.templateId > 0
|
|
3802
|
-
? await this.getContractTemplateById(userId, data.templateId)
|
|
3803
|
-
: null;
|
|
3804
|
-
const duplicateSource =
|
|
3805
|
-
data.duplicateFromId && data.duplicateFromId > 0
|
|
3806
|
-
? await this.getContractById(userId, data.duplicateFromId)
|
|
3807
|
-
: null;
|
|
3808
|
-
const storedSourceFile =
|
|
3809
|
-
data.sourceFileId && data.sourceFileId > 0
|
|
3810
|
-
? await this.prisma.file.findUnique({
|
|
3811
|
-
where: { id: data.sourceFileId },
|
|
3812
|
-
include: { file_mimetype: true },
|
|
3813
|
-
})
|
|
3814
|
-
: null;
|
|
3815
|
-
|
|
3816
|
-
if (data.sourceFileId && !storedSourceFile) {
|
|
3817
|
-
throw new NotFoundException('Source contract file not found.');
|
|
3818
|
-
}
|
|
3819
|
-
|
|
3820
|
-
const createdId = await this.prisma.$transaction(async (tx) => {
|
|
3821
|
-
const generatedCode = await this.generateContractCode(tx as any);
|
|
3822
|
-
const duplicateNameBase =
|
|
3823
|
-
this.normalizeOptionalText(duplicateSource?.name) ??
|
|
3824
|
-
this.normalizeOptionalText(duplicateSource?.code) ??
|
|
3825
|
-
'Contract';
|
|
3826
|
-
const created = await (tx as any).$queryRawUnsafe(
|
|
3827
|
-
`INSERT INTO operations_contract (
|
|
3828
|
-
code,
|
|
3829
|
-
name,
|
|
3830
|
-
contract_category,
|
|
3831
|
-
contract_type,
|
|
3832
|
-
client_name,
|
|
3833
|
-
signature_status,
|
|
3834
|
-
is_active,
|
|
3835
|
-
billing_model,
|
|
3836
|
-
account_manager_collaborator_id,
|
|
3837
|
-
related_collaborator_id,
|
|
3838
|
-
contract_template_id,
|
|
3839
|
-
origin_type,
|
|
3840
|
-
origin_id,
|
|
3841
|
-
start_date,
|
|
3842
|
-
end_date,
|
|
3843
|
-
signed_at,
|
|
3844
|
-
effective_date,
|
|
3845
|
-
budget_amount,
|
|
3846
|
-
monthly_hour_cap,
|
|
3847
|
-
status,
|
|
3848
|
-
creation_mode,
|
|
3849
|
-
wizard_step,
|
|
3850
|
-
description,
|
|
3851
|
-
content_html,
|
|
3852
|
-
created_by_user_id,
|
|
3853
|
-
updated_by_user_id,
|
|
3854
|
-
created_at,
|
|
3855
|
-
updated_at
|
|
3856
|
-
) VALUES (
|
|
3857
|
-
$1,
|
|
3858
|
-
$2,
|
|
3859
|
-
$3::operations_contract_contract_category_70d553ea09_enum,
|
|
3860
|
-
$4::operations_contract_contract_type_48331e2ebf_enum,
|
|
3861
|
-
$5,
|
|
3862
|
-
$6::operations_contract_signature_status_2cb7282a7b_enum,
|
|
3863
|
-
$7,
|
|
3864
|
-
$8::operations_contract_billing_model_409dc7fea2_enum,
|
|
3865
|
-
$9,
|
|
3866
|
-
$10,
|
|
3867
|
-
$11,
|
|
3868
|
-
$12::operations_contract_origin_type_07a7cc2b5d_enum,
|
|
3869
|
-
$13,
|
|
3870
|
-
$14::date,
|
|
3871
|
-
$15::date,
|
|
3872
|
-
$16::date,
|
|
3873
|
-
$17::date,
|
|
3874
|
-
$18,
|
|
3875
|
-
$19,
|
|
3876
|
-
$20::operations_contract_status_a0395962df_enum,
|
|
3877
|
-
$21::operations_contract_creation_mode_98ba669209_enum,
|
|
3878
|
-
$22,
|
|
3879
|
-
$23,
|
|
3880
|
-
$24,
|
|
3881
|
-
$25,
|
|
3882
|
-
$25,
|
|
3883
|
-
NOW(),
|
|
3884
|
-
NOW()
|
|
3885
|
-
)
|
|
3886
|
-
RETURNING id`,
|
|
3887
|
-
generatedCode,
|
|
3888
|
-
creationMode === 'duplicate'
|
|
3889
|
-
? `${duplicateNameBase} Copy`
|
|
3890
|
-
: this.normalizeOptionalText(selectedTemplate?.name),
|
|
3891
|
-
duplicateSource?.contractCategory ??
|
|
3892
|
-
selectedTemplate?.contractCategory ??
|
|
3893
|
-
'client',
|
|
3894
|
-
duplicateSource?.contractType ??
|
|
3895
|
-
selectedTemplate?.contractType ??
|
|
3896
|
-
'service_agreement',
|
|
3897
|
-
this.normalizeOptionalText(duplicateSource?.clientName),
|
|
3898
|
-
duplicateSource?.signatureStatus ??
|
|
3899
|
-
selectedTemplate?.signatureStatus ??
|
|
3900
|
-
'not_started',
|
|
3901
|
-
duplicateSource?.isActive ?? true,
|
|
3902
|
-
duplicateSource?.billingModel ??
|
|
3903
|
-
selectedTemplate?.billingModel ??
|
|
3904
|
-
'time_and_material',
|
|
3905
|
-
duplicateSource?.accountManagerCollaboratorId ?? null,
|
|
3906
|
-
duplicateSource?.relatedCollaboratorId ?? null,
|
|
3907
|
-
duplicateSource?.contractTemplateId ?? selectedTemplate?.id ?? null,
|
|
3908
|
-
duplicateSource?.originType ?? 'manual',
|
|
3909
|
-
duplicateSource?.originId ?? null,
|
|
3910
|
-
duplicateSource?.startDate ?? null,
|
|
3911
|
-
duplicateSource?.endDate ?? null,
|
|
3912
|
-
duplicateSource?.signedAt ?? null,
|
|
3913
|
-
duplicateSource?.effectiveDate ?? null,
|
|
3914
|
-
duplicateSource?.budgetAmount ?? null,
|
|
3915
|
-
duplicateSource?.monthlyHourCap ?? null,
|
|
3916
|
-
'draft',
|
|
3917
|
-
creationMode,
|
|
3918
|
-
creationMode === 'upload' ? 0 : creationMode === 'blank' ? 0 : 1,
|
|
3919
|
-
duplicateSource?.description ?? selectedTemplate?.description ?? null,
|
|
3920
|
-
duplicateSource?.contentHtml ?? selectedTemplate?.contentHtml ?? null,
|
|
3921
|
-
userId
|
|
3922
|
-
);
|
|
3923
|
-
|
|
3924
|
-
const contractId = (created as { id: number }[])[0]?.id;
|
|
3925
|
-
|
|
3926
|
-
if (duplicateSource) {
|
|
3927
|
-
await this.replaceContractParties(
|
|
3928
|
-
tx as any,
|
|
3929
|
-
contractId,
|
|
3930
|
-
duplicateSource.parties
|
|
3931
|
-
);
|
|
3932
|
-
await this.replaceContractSignatures(
|
|
3933
|
-
tx as any,
|
|
3934
|
-
contractId,
|
|
3935
|
-
duplicateSource.signatures
|
|
3936
|
-
);
|
|
3937
|
-
await this.replaceContractFinancialTerms(
|
|
3938
|
-
tx as any,
|
|
3939
|
-
contractId,
|
|
3940
|
-
duplicateSource.financialTerms
|
|
3941
|
-
);
|
|
3942
|
-
await this.replaceContractRevisions(
|
|
3943
|
-
tx as any,
|
|
3944
|
-
contractId,
|
|
3945
|
-
duplicateSource.revisions
|
|
3946
|
-
);
|
|
3947
|
-
|
|
3948
|
-
const currentSourceDocument =
|
|
3949
|
-
duplicateSource.documents.find(
|
|
3950
|
-
(document) => document.isCurrent && document.documentType === 'source_upload'
|
|
3951
|
-
) ?? null;
|
|
3952
|
-
|
|
3953
|
-
if (currentSourceDocument) {
|
|
3954
|
-
await this.replaceContractDocument(
|
|
3955
|
-
tx as any,
|
|
3956
|
-
contractId,
|
|
3957
|
-
'source_upload',
|
|
3958
|
-
{
|
|
3959
|
-
fileId: currentSourceDocument.fileId ?? null,
|
|
3960
|
-
fileName: currentSourceDocument.fileName,
|
|
3961
|
-
mimeType: currentSourceDocument.mimeType,
|
|
3962
|
-
fileContentBase64: currentSourceDocument.fileContentBase64 ?? null,
|
|
3963
|
-
notes: currentSourceDocument.notes ?? null,
|
|
3964
|
-
extractionStatus: currentSourceDocument.extractionStatus ?? 'skipped',
|
|
3965
|
-
extractionSummary: currentSourceDocument.extractionSummary ?? null,
|
|
3966
|
-
}
|
|
3967
|
-
);
|
|
3968
|
-
}
|
|
3969
|
-
}
|
|
3970
|
-
|
|
3971
|
-
if (storedSourceFile) {
|
|
3972
|
-
await this.replaceContractDocument(tx as any, contractId, 'source_upload', {
|
|
3973
|
-
fileId: storedSourceFile.id,
|
|
3974
|
-
fileName:
|
|
3975
|
-
this.normalizeOptionalText(data.sourceFileName) ??
|
|
3976
|
-
storedSourceFile.filename,
|
|
3977
|
-
mimeType:
|
|
3978
|
-
this.normalizeOptionalText(data.sourceMimeType) ??
|
|
3979
|
-
storedSourceFile.file_mimetype?.name ??
|
|
3980
|
-
'application/pdf',
|
|
3981
|
-
notes: 'Source contract document uploaded during draft creation.',
|
|
3982
|
-
extractionStatus: 'pending',
|
|
3983
|
-
extractionSummary: null,
|
|
3984
|
-
});
|
|
3985
|
-
}
|
|
3986
|
-
|
|
3987
|
-
await this.insertContractHistory(
|
|
3988
|
-
tx as any,
|
|
3989
|
-
contractId,
|
|
3990
|
-
userId,
|
|
3991
|
-
'draft_created',
|
|
3992
|
-
`Contract draft created with mode ${creationMode}.`,
|
|
3993
|
-
JSON.stringify({
|
|
3994
|
-
creationMode,
|
|
3995
|
-
templateId: selectedTemplate?.id ?? null,
|
|
3996
|
-
duplicateFromId: duplicateSource?.id ?? null,
|
|
3997
|
-
sourceFileId: storedSourceFile?.id ?? null,
|
|
3998
|
-
})
|
|
3999
|
-
);
|
|
4000
|
-
|
|
4001
|
-
return contractId;
|
|
4002
|
-
});
|
|
4003
|
-
|
|
4004
|
-
return this.getContractById(userId, createdId);
|
|
4005
|
-
}
|
|
4006
|
-
|
|
4007
|
-
async extractContractSource(
|
|
4008
|
-
userId: number,
|
|
4009
|
-
contractId: number,
|
|
4010
|
-
data: Omit<ContractExtractDraftPayload, 'fileName' | 'mimeType' | 'fileContentBase64' | 'contractId'>
|
|
4011
|
-
) {
|
|
4012
|
-
const actor = await this.getActorContext(userId);
|
|
4013
|
-
this.ensureDirector(actor);
|
|
4014
|
-
|
|
4015
|
-
const contract = await this.getContractById(userId, contractId);
|
|
4016
|
-
const sourceDocument = contract.documents.find(
|
|
4017
|
-
(document) => document.isCurrent && document.documentType === 'source_upload'
|
|
4018
|
-
);
|
|
4019
|
-
|
|
4020
|
-
if (!sourceDocument) {
|
|
4021
|
-
throw new BadRequestException(
|
|
4022
|
-
'No source document is attached to this contract draft.'
|
|
4023
|
-
);
|
|
4024
|
-
}
|
|
4025
|
-
|
|
4026
|
-
await this.prisma.$executeRawUnsafe(
|
|
4027
|
-
`UPDATE operations_contract_document
|
|
4028
|
-
SET extraction_status = 'processing',
|
|
4029
|
-
updated_at = NOW()
|
|
4030
|
-
WHERE id = $1`,
|
|
4031
|
-
sourceDocument.id
|
|
4032
|
-
);
|
|
4033
|
-
|
|
4034
|
-
try {
|
|
4035
|
-
const extracted = await this.extractContractDraft(userId, {
|
|
4036
|
-
contractId,
|
|
4037
|
-
provider: data.provider ?? null,
|
|
4038
|
-
promptMessage: data.promptMessage ?? null,
|
|
4039
|
-
});
|
|
4040
|
-
|
|
4041
|
-
await this.prisma.$executeRawUnsafe(
|
|
4042
|
-
`UPDATE operations_contract_document
|
|
4043
|
-
SET extraction_status = 'completed',
|
|
4044
|
-
extraction_summary = $2,
|
|
4045
|
-
updated_at = NOW()
|
|
4046
|
-
WHERE id = $1`,
|
|
4047
|
-
sourceDocument.id,
|
|
4048
|
-
extracted.summary || extracted.description || extracted.name || null
|
|
4049
|
-
);
|
|
4050
|
-
|
|
4051
|
-
await this.insertContractHistory(
|
|
4052
|
-
this.prisma,
|
|
4053
|
-
contractId,
|
|
4054
|
-
userId,
|
|
4055
|
-
'source_extracted',
|
|
4056
|
-
'Source contract document extracted with AI.'
|
|
4057
|
-
);
|
|
4058
|
-
|
|
4059
|
-
return extracted;
|
|
4060
|
-
} catch (error) {
|
|
4061
|
-
await this.prisma.$executeRawUnsafe(
|
|
4062
|
-
`UPDATE operations_contract_document
|
|
4063
|
-
SET extraction_status = 'failed',
|
|
4064
|
-
updated_at = NOW()
|
|
4065
|
-
WHERE id = $1`,
|
|
4066
|
-
sourceDocument.id
|
|
4067
|
-
);
|
|
4068
|
-
throw error;
|
|
4069
|
-
}
|
|
4070
|
-
}
|
|
4071
|
-
|
|
4072
|
-
async generateContractContent(
|
|
4073
|
-
userId: number,
|
|
4074
|
-
contractId: number,
|
|
4075
|
-
data: ContractGenerateContentPayload = {}
|
|
4076
|
-
) {
|
|
4077
|
-
const actor = await this.getActorContext(userId);
|
|
4078
|
-
this.ensureDirector(actor);
|
|
4079
|
-
|
|
4080
|
-
const contract = await this.getContractById(userId, contractId);
|
|
4081
|
-
const contentHtml = await this.generateContractContentHtml(contract, data);
|
|
4082
|
-
|
|
4083
|
-
await this.prisma.$transaction(async (tx) => {
|
|
4084
|
-
await (tx as any).$executeRawUnsafe(
|
|
4085
|
-
`UPDATE operations_contract
|
|
4086
|
-
SET content_html = $2,
|
|
4087
|
-
wizard_step = CASE
|
|
4088
|
-
WHEN COALESCE(wizard_step, 0) < 4 THEN 4
|
|
4089
|
-
ELSE wizard_step
|
|
4090
|
-
END,
|
|
4091
|
-
updated_by_user_id = $3,
|
|
4092
|
-
updated_at = NOW()
|
|
4093
|
-
WHERE id = $1`,
|
|
4094
|
-
contractId,
|
|
4095
|
-
contentHtml,
|
|
4096
|
-
userId
|
|
4097
|
-
);
|
|
4098
|
-
|
|
4099
|
-
await this.insertContractHistory(
|
|
4100
|
-
tx as any,
|
|
4101
|
-
contractId,
|
|
4102
|
-
userId,
|
|
4103
|
-
'content_generated',
|
|
4104
|
-
'Contract content generated automatically for editing.',
|
|
4105
|
-
JSON.stringify({
|
|
4106
|
-
provider: data.provider ?? null,
|
|
4107
|
-
overwrite: Boolean(data.overwrite),
|
|
4108
|
-
hasPrompt: Boolean(this.normalizeOptionalText(data.promptMessage)),
|
|
4109
|
-
})
|
|
3936
|
+
$10,
|
|
3937
|
+
$11::operations_contract_origin_type_07a7cc2b5d_enum,
|
|
3938
|
+
$12,
|
|
3939
|
+
$13::date,
|
|
3940
|
+
$14::date, $15::date, $16::date, $17, $18,
|
|
3941
|
+
$19::operations_contract_status_a0395962df_enum,
|
|
3942
|
+
$20::operations_contract_creation_mode_98ba669209_enum,
|
|
3943
|
+
$21,
|
|
3944
|
+
$22,
|
|
3945
|
+
$23,
|
|
3946
|
+
NOW(), NOW()
|
|
3947
|
+
)
|
|
3948
|
+
RETURNING id`,
|
|
3949
|
+
normalizedCode,
|
|
3950
|
+
this.normalizeOptionalText(data.name),
|
|
3951
|
+
data.contractCategory ?? 'client',
|
|
3952
|
+
data.contractType ?? 'service_agreement',
|
|
3953
|
+
this.normalizeOptionalText(data.clientName),
|
|
3954
|
+
data.signatureStatus ?? 'not_started',
|
|
3955
|
+
data.isActive ?? true,
|
|
3956
|
+
data.billingModel ?? 'time_and_material',
|
|
3957
|
+
data.accountManagerCollaboratorId ?? null,
|
|
3958
|
+
data.relatedCollaboratorId ?? null,
|
|
3959
|
+
data.originType ?? 'manual',
|
|
3960
|
+
data.originId ?? null,
|
|
3961
|
+
this.normalizeOptionalText(data.startDate ?? null),
|
|
3962
|
+
data.endDate ?? null,
|
|
3963
|
+
data.signedAt ?? null,
|
|
3964
|
+
data.effectiveDate ?? data.startDate ?? null,
|
|
3965
|
+
data.budgetAmount ?? null,
|
|
3966
|
+
data.monthlyHourCap ?? null,
|
|
3967
|
+
data.status ?? 'draft',
|
|
3968
|
+
data.creationMode ?? 'blank',
|
|
3969
|
+
data.wizardStep ?? 0,
|
|
3970
|
+
data.description ?? null,
|
|
3971
|
+
data.contentHtml ?? null
|
|
4110
3972
|
);
|
|
4111
|
-
});
|
|
4112
|
-
|
|
4113
|
-
return this.getContractById(userId, contractId);
|
|
4114
|
-
}
|
|
4115
|
-
|
|
4116
|
-
async reviewContractLegally(
|
|
4117
|
-
userId: number,
|
|
4118
|
-
contractId: number,
|
|
4119
|
-
data: ContractLegalReviewPayload = {}
|
|
4120
|
-
) {
|
|
4121
|
-
const actor = await this.getActorContext(userId);
|
|
4122
|
-
this.ensureDirector(actor);
|
|
4123
|
-
|
|
4124
|
-
const contract = await this.getContractById(userId, contractId);
|
|
4125
|
-
const review = await this.buildContractLegalReview(contract, data);
|
|
4126
|
-
|
|
4127
|
-
await this.insertContractHistory(
|
|
4128
|
-
this.prisma,
|
|
4129
|
-
contractId,
|
|
4130
|
-
userId,
|
|
4131
|
-
'legal_reviewed',
|
|
4132
|
-
'Advisory legal checklist updated for this contract.',
|
|
4133
|
-
JSON.stringify(review)
|
|
4134
|
-
);
|
|
4135
|
-
|
|
4136
|
-
return review;
|
|
4137
|
-
}
|
|
4138
|
-
|
|
4139
|
-
async generateContractPdf(userId: number, contractId: number) {
|
|
4140
|
-
const actor = await this.getActorContext(userId);
|
|
4141
|
-
this.ensureDirector(actor);
|
|
4142
|
-
|
|
4143
|
-
const contract = await this.getContractById(userId, contractId);
|
|
4144
|
-
const pdfBuffer = await this.renderContractPdfBuffer(contract);
|
|
4145
|
-
const fileName = this.buildContractPdfFileName(contract);
|
|
4146
|
-
const uploadedFile = await this.fileService.upload('operations/contracts/generated', {
|
|
4147
|
-
fieldname: 'file',
|
|
4148
|
-
originalname: fileName,
|
|
4149
|
-
encoding: '7bit',
|
|
4150
|
-
mimetype: 'application/pdf',
|
|
4151
|
-
size: pdfBuffer.length,
|
|
4152
|
-
destination: '',
|
|
4153
|
-
filename: fileName,
|
|
4154
|
-
path: '',
|
|
4155
|
-
buffer: pdfBuffer,
|
|
4156
|
-
});
|
|
4157
|
-
|
|
4158
|
-
await this.prisma.$transaction(async (tx) => {
|
|
4159
|
-
await this.replaceContractDocument(tx as any, contractId, 'generated_pdf', {
|
|
4160
|
-
fileId: uploadedFile.id,
|
|
4161
|
-
fileName,
|
|
4162
|
-
mimeType: 'application/pdf',
|
|
4163
|
-
notes: 'PDF generated from contract rich text content.',
|
|
4164
|
-
extractionStatus: 'skipped',
|
|
4165
|
-
extractionSummary: null,
|
|
4166
|
-
});
|
|
4167
3973
|
|
|
3974
|
+
const contractId = (created as { id: number }[])[0]?.id;
|
|
3975
|
+
await this.replaceContractParties(tx as any, contractId, data.parties);
|
|
3976
|
+
if (data.replaceUploadedPdfDocument) {
|
|
3977
|
+
await this.replaceContractDocument(
|
|
3978
|
+
tx as any,
|
|
3979
|
+
contractId,
|
|
3980
|
+
'source_upload',
|
|
3981
|
+
data.replaceUploadedPdfDocument
|
|
3982
|
+
);
|
|
3983
|
+
}
|
|
4168
3984
|
await this.insertContractHistory(
|
|
4169
3985
|
tx as any,
|
|
4170
3986
|
contractId,
|
|
4171
3987
|
userId,
|
|
4172
|
-
'
|
|
4173
|
-
|
|
3988
|
+
'created',
|
|
3989
|
+
data.originType === 'manual'
|
|
3990
|
+
? 'Manual contract created from registry.'
|
|
3991
|
+
: `Contract registered from origin ${data.originType}.`
|
|
4174
3992
|
);
|
|
3993
|
+
return contractId;
|
|
4175
3994
|
});
|
|
4176
3995
|
|
|
4177
|
-
return
|
|
4178
|
-
contractId,
|
|
4179
|
-
fileId: uploadedFile.id,
|
|
4180
|
-
fileName,
|
|
4181
|
-
mimeType: 'application/pdf',
|
|
4182
|
-
documentType: 'generated_pdf',
|
|
4183
|
-
downloadUrl: `/file/open/${uploadedFile.id}`,
|
|
4184
|
-
};
|
|
4185
|
-
}
|
|
4186
|
-
|
|
4187
|
-
async extractContractDraft(
|
|
4188
|
-
userId: number,
|
|
4189
|
-
data: ContractExtractDraftPayload
|
|
4190
|
-
) {
|
|
4191
|
-
const actor = await this.getActorContext(userId);
|
|
4192
|
-
this.ensureDirector(actor);
|
|
4193
|
-
|
|
4194
|
-
const uploadFile = await this.resolveContractExtractionFile(userId, data);
|
|
4195
|
-
|
|
4196
|
-
const aiResult = await this.aiService.chat(
|
|
4197
|
-
{
|
|
4198
|
-
provider: data.provider === 'gemini' ? 'gemini' : 'openai',
|
|
4199
|
-
model: data.provider === 'gemini' ? 'gemini-1.5-flash' : 'gpt-4o-mini',
|
|
4200
|
-
message:
|
|
4201
|
-
this.normalizeExtractionString(data.promptMessage) ||
|
|
4202
|
-
'Analyze the attached contract and extract a structured draft for the contract form.',
|
|
4203
|
-
systemPrompt: this.buildContractExtractionSystemPrompt(),
|
|
4204
|
-
},
|
|
4205
|
-
uploadFile,
|
|
4206
|
-
);
|
|
4207
|
-
|
|
4208
|
-
const parsed = this.parseAiJsonPayload(String(aiResult?.content ?? ''));
|
|
4209
|
-
const draft = this.normalizeContractExtractDraft(parsed);
|
|
4210
|
-
const warnings = [...draft.warnings];
|
|
4211
|
-
|
|
4212
|
-
if (
|
|
4213
|
-
uploadFile.mimetype === 'application/msword' ||
|
|
4214
|
-
uploadFile.mimetype ===
|
|
4215
|
-
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
4216
|
-
) {
|
|
4217
|
-
warnings.push(
|
|
4218
|
-
'Word extraction is best-effort. Review names, dates, and values before saving.'
|
|
4219
|
-
);
|
|
4220
|
-
}
|
|
4221
|
-
|
|
4222
|
-
return {
|
|
4223
|
-
...draft,
|
|
4224
|
-
warnings: Array.from(new Set(warnings.filter(Boolean))),
|
|
4225
|
-
};
|
|
3996
|
+
return this.getContractById(userId, createdId);
|
|
4226
3997
|
}
|
|
4227
3998
|
|
|
4228
3999
|
async createContractFromProposalIntegration(
|
|
@@ -4261,37 +4032,6 @@ export class OperationsService {
|
|
|
4261
4032
|
const contractName =
|
|
4262
4033
|
this.normalizeOptionalText(proposal.title) ?? `Proposal ${sourceEntityId}`;
|
|
4263
4034
|
|
|
4264
|
-
const financialTerms = items
|
|
4265
|
-
.map((item) => ({
|
|
4266
|
-
termType: this.normalizeEnumValue(
|
|
4267
|
-
item.termType,
|
|
4268
|
-
FINANCIAL_TERM_TYPE_VALUES,
|
|
4269
|
-
'value',
|
|
4270
|
-
),
|
|
4271
|
-
label: this.normalizeOptionalText(item.name) ?? 'Commercial term',
|
|
4272
|
-
amount: Number(item.amount ?? Number(item.totalAmountCents || 0) / 100),
|
|
4273
|
-
recurrence: this.normalizeEnumValue(
|
|
4274
|
-
item.recurrence,
|
|
4275
|
-
RECURRENCE_VALUES,
|
|
4276
|
-
'one_time',
|
|
4277
|
-
),
|
|
4278
|
-
dueDay: item.dueDay ?? null,
|
|
4279
|
-
notes: this.normalizeOptionalText(item.description),
|
|
4280
|
-
}))
|
|
4281
|
-
.filter((term) => Number.isFinite(term.amount) && term.amount > 0);
|
|
4282
|
-
|
|
4283
|
-
const fallbackAmount = Number(proposal.totalAmount ?? 0);
|
|
4284
|
-
if (financialTerms.length === 0 && Number.isFinite(fallbackAmount) && fallbackAmount > 0) {
|
|
4285
|
-
financialTerms.push({
|
|
4286
|
-
termType: 'revenue',
|
|
4287
|
-
label: contractName,
|
|
4288
|
-
amount: fallbackAmount,
|
|
4289
|
-
recurrence: 'one_time',
|
|
4290
|
-
dueDay: null,
|
|
4291
|
-
notes: this.normalizeOptionalText(proposal.notes),
|
|
4292
|
-
});
|
|
4293
|
-
}
|
|
4294
|
-
|
|
4295
4035
|
const primaryPartyRole = ['employee', 'contractor'].includes(contractCategory)
|
|
4296
4036
|
? 'employee'
|
|
4297
4037
|
: ['supplier', 'vendor'].includes(contractCategory)
|
|
@@ -4342,7 +4082,6 @@ export class OperationsService {
|
|
|
4342
4082
|
billing_model,
|
|
4343
4083
|
account_manager_collaborator_id,
|
|
4344
4084
|
related_collaborator_id,
|
|
4345
|
-
contract_template_id,
|
|
4346
4085
|
origin_type,
|
|
4347
4086
|
origin_id,
|
|
4348
4087
|
start_date,
|
|
@@ -4408,7 +4147,7 @@ export class OperationsService {
|
|
|
4408
4147
|
proposal.validUntil ?? null,
|
|
4409
4148
|
null,
|
|
4410
4149
|
proposal.validFrom ?? null,
|
|
4411
|
-
|
|
4150
|
+
Number(proposal.totalAmount ?? 0) > 0 ? Number(proposal.totalAmount ?? 0) : null,
|
|
4412
4151
|
null,
|
|
4413
4152
|
'draft',
|
|
4414
4153
|
'blank',
|
|
@@ -4435,31 +4174,6 @@ export class OperationsService {
|
|
|
4435
4174
|
},
|
|
4436
4175
|
]);
|
|
4437
4176
|
|
|
4438
|
-
await this.replaceContractFinancialTerms(
|
|
4439
|
-
tx as any,
|
|
4440
|
-
contractId,
|
|
4441
|
-
financialTerms.map((term) => ({
|
|
4442
|
-
termType: term.termType as any,
|
|
4443
|
-
label: term.label,
|
|
4444
|
-
amount: term.amount,
|
|
4445
|
-
recurrence: term.recurrence as any,
|
|
4446
|
-
dueDay: term.dueDay,
|
|
4447
|
-
notes: term.notes,
|
|
4448
|
-
})),
|
|
4449
|
-
);
|
|
4450
|
-
|
|
4451
|
-
await this.replaceContractRevisions(tx as any, contractId, [
|
|
4452
|
-
{
|
|
4453
|
-
revisionType: 'revision',
|
|
4454
|
-
title: payload.revision?.title ?? contractName,
|
|
4455
|
-
effectiveDate: proposal.validFrom ?? null,
|
|
4456
|
-
status: 'draft',
|
|
4457
|
-
summary:
|
|
4458
|
-
this.normalizeOptionalText(payload.revision?.summary) ??
|
|
4459
|
-
this.normalizeOptionalText(proposal.notes),
|
|
4460
|
-
},
|
|
4461
|
-
]);
|
|
4462
|
-
|
|
4463
4177
|
await this.insertContractHistory(
|
|
4464
4178
|
tx as any,
|
|
4465
4179
|
contractId,
|
|
@@ -4499,7 +4213,6 @@ export class OperationsService {
|
|
|
4499
4213
|
startDate: proposal.validFrom ?? null,
|
|
4500
4214
|
endDate: proposal.validUntil ?? null,
|
|
4501
4215
|
description: this.normalizeOptionalText(proposal.notes),
|
|
4502
|
-
financialTerms,
|
|
4503
4216
|
},
|
|
4504
4217
|
payload,
|
|
4505
4218
|
String(payload.locale || '').trim() || 'en',
|
|
@@ -4542,7 +4255,6 @@ export class OperationsService {
|
|
|
4542
4255
|
startDate?: string | null;
|
|
4543
4256
|
endDate?: string | null;
|
|
4544
4257
|
description?: string | null;
|
|
4545
|
-
financialTerms?: ContractPayload['financialTerms'];
|
|
4546
4258
|
},
|
|
4547
4259
|
payload: ProposalApprovedEventPayload,
|
|
4548
4260
|
locale = 'en',
|
|
@@ -4578,7 +4290,6 @@ export class OperationsService {
|
|
|
4578
4290
|
startDate: contract.startDate ?? null,
|
|
4579
4291
|
endDate: contract.endDate ?? null,
|
|
4580
4292
|
description: contract.description ?? null,
|
|
4581
|
-
financialTerms: contract.financialTerms ?? [],
|
|
4582
4293
|
},
|
|
4583
4294
|
proposal: payload.proposal ?? {
|
|
4584
4295
|
code: payload.code ?? null,
|
|
@@ -4607,7 +4318,6 @@ export class OperationsService {
|
|
|
4607
4318
|
effectiveDate?: string | null;
|
|
4608
4319
|
budgetAmount?: number | null;
|
|
4609
4320
|
description?: string | null;
|
|
4610
|
-
financialTerms?: ContractPayload['financialTerms'];
|
|
4611
4321
|
parties?: ContractPayload['parties'];
|
|
4612
4322
|
},
|
|
4613
4323
|
locale = 'en',
|
|
@@ -4687,20 +4397,7 @@ export class OperationsService {
|
|
|
4687
4397
|
}
|
|
4688
4398
|
}
|
|
4689
4399
|
|
|
4690
|
-
const
|
|
4691
|
-
label: term.label,
|
|
4692
|
-
termType: term.termType ?? 'value',
|
|
4693
|
-
amount: Number(term.amount ?? 0),
|
|
4694
|
-
recurrence: term.recurrence ?? 'one_time',
|
|
4695
|
-
dueDay: term.dueDay ?? null,
|
|
4696
|
-
notes: term.notes ?? null,
|
|
4697
|
-
}));
|
|
4698
|
-
|
|
4699
|
-
const totalAmount =
|
|
4700
|
-
financialTerms.reduce(
|
|
4701
|
-
(sum, term) => sum + (Number.isFinite(term.amount) ? term.amount : 0),
|
|
4702
|
-
0,
|
|
4703
|
-
) || Number(contract.budgetAmount ?? 0);
|
|
4400
|
+
const totalAmount = Number(contract.budgetAmount ?? 0);
|
|
4704
4401
|
|
|
4705
4402
|
return {
|
|
4706
4403
|
contractId: contract.id,
|
|
@@ -4733,7 +4430,6 @@ export class OperationsService {
|
|
|
4733
4430
|
effectiveDate: contract.effectiveDate ?? null,
|
|
4734
4431
|
budgetAmount: contract.budgetAmount ?? null,
|
|
4735
4432
|
description: contract.description ?? null,
|
|
4736
|
-
financialTerms,
|
|
4737
4433
|
parties: contract.parties ?? [],
|
|
4738
4434
|
},
|
|
4739
4435
|
person: personRecord
|
|
@@ -4811,7 +4507,6 @@ export class OperationsService {
|
|
|
4811
4507
|
);
|
|
4812
4508
|
this.pushUpdate(updates, params, 'account_manager_collaborator_id', data.accountManagerCollaboratorId);
|
|
4813
4509
|
this.pushUpdate(updates, params, 'related_collaborator_id', data.relatedCollaboratorId);
|
|
4814
|
-
this.pushUpdate(updates, params, 'contract_template_id', data.contractTemplateId);
|
|
4815
4510
|
this.pushUpdate(
|
|
4816
4511
|
updates,
|
|
4817
4512
|
params,
|
|
@@ -4859,27 +4554,6 @@ export class OperationsService {
|
|
|
4859
4554
|
if (data.parties) {
|
|
4860
4555
|
await this.replaceContractParties(tx as any, contractId, data.parties);
|
|
4861
4556
|
}
|
|
4862
|
-
if (data.signatures) {
|
|
4863
|
-
await this.replaceContractSignatures(
|
|
4864
|
-
tx as any,
|
|
4865
|
-
contractId,
|
|
4866
|
-
data.signatures
|
|
4867
|
-
);
|
|
4868
|
-
}
|
|
4869
|
-
if (data.financialTerms) {
|
|
4870
|
-
await this.replaceContractFinancialTerms(
|
|
4871
|
-
tx as any,
|
|
4872
|
-
contractId,
|
|
4873
|
-
data.financialTerms
|
|
4874
|
-
);
|
|
4875
|
-
}
|
|
4876
|
-
if (data.revisions) {
|
|
4877
|
-
await this.replaceContractRevisions(
|
|
4878
|
-
tx as any,
|
|
4879
|
-
contractId,
|
|
4880
|
-
data.revisions
|
|
4881
|
-
);
|
|
4882
|
-
}
|
|
4883
4557
|
if (data.replaceUploadedPdfDocument) {
|
|
4884
4558
|
await this.replaceContractDocument(
|
|
4885
4559
|
tx as any,
|
|
@@ -4915,7 +4589,6 @@ export class OperationsService {
|
|
|
4915
4589
|
effectiveDate: data.effectiveDate ?? current.effectiveDate,
|
|
4916
4590
|
budgetAmount: data.budgetAmount ?? current.budgetAmount,
|
|
4917
4591
|
description: data.description ?? current.description,
|
|
4918
|
-
financialTerms: data.financialTerms ?? current.financialTerms,
|
|
4919
4592
|
parties: data.parties ?? current.parties,
|
|
4920
4593
|
},
|
|
4921
4594
|
'en',
|
|
@@ -5004,10 +4677,7 @@ export class OperationsService {
|
|
|
5004
4677
|
|
|
5005
4678
|
for (const tableName of [
|
|
5006
4679
|
'operations_contract_party',
|
|
5007
|
-
'operations_contract_signature',
|
|
5008
|
-
'operations_contract_financial_term',
|
|
5009
4680
|
'operations_contract_document',
|
|
5010
|
-
'operations_contract_revision',
|
|
5011
4681
|
]) {
|
|
5012
4682
|
await (tx as any).$executeRawUnsafe(
|
|
5013
4683
|
`UPDATE ${tableName}
|
|
@@ -5042,9 +4712,57 @@ export class OperationsService {
|
|
|
5042
4712
|
return { success: true };
|
|
5043
4713
|
}
|
|
5044
4714
|
|
|
5045
|
-
async listTimesheets(
|
|
4715
|
+
async listTimesheets(
|
|
4716
|
+
userId: number,
|
|
4717
|
+
filters: {
|
|
4718
|
+
page?: number;
|
|
4719
|
+
pageSize?: number;
|
|
4720
|
+
search?: string;
|
|
4721
|
+
sortField?: string;
|
|
4722
|
+
sortOrder?: string;
|
|
4723
|
+
status?: string;
|
|
4724
|
+
} = {}
|
|
4725
|
+
) {
|
|
5046
4726
|
const actor = await this.getActorContext(userId);
|
|
5047
4727
|
const filter = this.buildIdFilter(actor.visibleCollaboratorIds, 't.collaborator_id', actor.isDirector);
|
|
4728
|
+
const pagination = this.shouldPaginate(filters)
|
|
4729
|
+
? this.normalizePaginationParams(filters, {
|
|
4730
|
+
defaultSortField: 'weekStartDate',
|
|
4731
|
+
defaultSortOrder: 'desc',
|
|
4732
|
+
allowedSortFields: ['weekStartDate', 'weekEndDate', 'collaboratorName', 'status'],
|
|
4733
|
+
})
|
|
4734
|
+
: null;
|
|
4735
|
+
const params: unknown[] = [...filter.params];
|
|
4736
|
+
const where = ['t.deleted_at IS NULL', filter.clause];
|
|
4737
|
+
|
|
4738
|
+
if (filters.status && filters.status !== 'all') {
|
|
4739
|
+
where.push(`t.status::text = ${this.param(params, filters.status)}`);
|
|
4740
|
+
}
|
|
4741
|
+
|
|
4742
|
+
if (pagination?.search) {
|
|
4743
|
+
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
4744
|
+
where.push(`(
|
|
4745
|
+
COALESCE(c.display_name, '') ILIKE ${searchPlaceholder}
|
|
4746
|
+
OR COALESCE(a.display_name, '') ILIKE ${searchPlaceholder}
|
|
4747
|
+
OR COALESCE(t.notes, '') ILIKE ${searchPlaceholder}
|
|
4748
|
+
OR COALESCE(approval.decision_note, '') ILIKE ${searchPlaceholder}
|
|
4749
|
+
)`);
|
|
4750
|
+
}
|
|
4751
|
+
|
|
4752
|
+
const whereClause = where.join(' AND ');
|
|
4753
|
+
const sortColumn =
|
|
4754
|
+
{
|
|
4755
|
+
weekStartDate: 't.week_start_date',
|
|
4756
|
+
weekEndDate: 't.week_end_date',
|
|
4757
|
+
collaboratorName: 'c.display_name',
|
|
4758
|
+
status: 't.status',
|
|
4759
|
+
}[pagination?.sortField ?? 'weekStartDate'] ?? 't.week_start_date';
|
|
4760
|
+
const headerParams = [...params];
|
|
4761
|
+
const limitSql =
|
|
4762
|
+
pagination
|
|
4763
|
+
? `LIMIT ${this.param(headerParams, pagination.pageSize)}
|
|
4764
|
+
OFFSET ${this.param(headerParams, pagination.offset)}`
|
|
4765
|
+
: '';
|
|
5048
4766
|
|
|
5049
4767
|
const headers = await this.queryRows<{
|
|
5050
4768
|
id: number;
|
|
@@ -5081,15 +4799,39 @@ export class OperationsService {
|
|
|
5081
4799
|
ON approval.target_type = 'timesheet'
|
|
5082
4800
|
AND approval.target_id = t.id
|
|
5083
4801
|
AND approval.deleted_at IS NULL
|
|
5084
|
-
WHERE
|
|
5085
|
-
ORDER BY
|
|
5086
|
-
|
|
4802
|
+
WHERE ${whereClause}
|
|
4803
|
+
ORDER BY ${sortColumn} ${pagination?.sortOrder?.toUpperCase() ?? 'DESC'}, t.id DESC
|
|
4804
|
+
${limitSql}`,
|
|
4805
|
+
headerParams
|
|
5087
4806
|
);
|
|
5088
4807
|
|
|
5089
4808
|
if (!headers.length) {
|
|
4809
|
+
if (pagination) {
|
|
4810
|
+
return this.buildPaginationResult([], 0, pagination.page, pagination.pageSize);
|
|
4811
|
+
}
|
|
4812
|
+
|
|
5090
4813
|
return headers;
|
|
5091
4814
|
}
|
|
5092
4815
|
|
|
4816
|
+
const total = pagination
|
|
4817
|
+
? Number(
|
|
4818
|
+
(
|
|
4819
|
+
await this.querySingle<{ total: string }>(
|
|
4820
|
+
`SELECT COUNT(*)::text AS total
|
|
4821
|
+
FROM operations_timesheet t
|
|
4822
|
+
JOIN operations_collaborator c ON c.id = t.collaborator_id
|
|
4823
|
+
LEFT JOIN operations_collaborator a ON a.id = t.approver_collaborator_id
|
|
4824
|
+
LEFT JOIN operations_approval approval
|
|
4825
|
+
ON approval.target_type = 'timesheet'
|
|
4826
|
+
AND approval.target_id = t.id
|
|
4827
|
+
AND approval.deleted_at IS NULL
|
|
4828
|
+
WHERE ${whereClause}`,
|
|
4829
|
+
params
|
|
4830
|
+
)
|
|
4831
|
+
)?.total ?? 0
|
|
4832
|
+
)
|
|
4833
|
+
: 0;
|
|
4834
|
+
|
|
5093
4835
|
const entries = await this.queryRows<{
|
|
5094
4836
|
id: number;
|
|
5095
4837
|
timesheetId: number;
|
|
@@ -5134,10 +4876,21 @@ export class OperationsService {
|
|
|
5134
4876
|
);
|
|
5135
4877
|
|
|
5136
4878
|
const grouped = this.groupBy(entries, 'timesheetId');
|
|
5137
|
-
|
|
4879
|
+
const data = headers.map((timesheet) => ({
|
|
5138
4880
|
...timesheet,
|
|
5139
4881
|
entries: grouped[timesheet.id] ?? [],
|
|
5140
4882
|
}));
|
|
4883
|
+
|
|
4884
|
+
if (!pagination) {
|
|
4885
|
+
return data;
|
|
4886
|
+
}
|
|
4887
|
+
|
|
4888
|
+
return this.buildPaginationResult(
|
|
4889
|
+
data,
|
|
4890
|
+
total,
|
|
4891
|
+
pagination.page,
|
|
4892
|
+
pagination.pageSize
|
|
4893
|
+
);
|
|
5141
4894
|
}
|
|
5142
4895
|
|
|
5143
4896
|
async createTimesheet(userId: number, data: TimesheetPayload) {
|
|
@@ -5315,19 +5068,52 @@ export class OperationsService {
|
|
|
5315
5068
|
return this.listSingleTimesheet(actor, timesheetId);
|
|
5316
5069
|
}
|
|
5317
5070
|
|
|
5318
|
-
async listTimeOffRequests(
|
|
5071
|
+
async listTimeOffRequests(
|
|
5072
|
+
userId: number,
|
|
5073
|
+
filters: {
|
|
5074
|
+
page?: number;
|
|
5075
|
+
pageSize?: number;
|
|
5076
|
+
search?: string;
|
|
5077
|
+
sortField?: string;
|
|
5078
|
+
sortOrder?: string;
|
|
5079
|
+
status?: string;
|
|
5080
|
+
} = {}
|
|
5081
|
+
) {
|
|
5319
5082
|
const actor = await this.getActorContext(userId);
|
|
5320
5083
|
const filter = this.buildIdFilter(actor.visibleCollaboratorIds, 'tor.collaborator_id', actor.isDirector);
|
|
5084
|
+
const pagination = this.shouldPaginate(filters)
|
|
5085
|
+
? this.normalizePaginationParams(filters, {
|
|
5086
|
+
defaultSortField: 'startDate',
|
|
5087
|
+
defaultSortOrder: 'desc',
|
|
5088
|
+
allowedSortFields: ['startDate', 'endDate', 'collaboratorName', 'status'],
|
|
5089
|
+
})
|
|
5090
|
+
: null;
|
|
5091
|
+
const params: unknown[] = [...filter.params];
|
|
5092
|
+
const where = ['tor.deleted_at IS NULL', filter.clause];
|
|
5321
5093
|
|
|
5322
|
-
|
|
5323
|
-
`
|
|
5094
|
+
if (filters.status && filters.status !== 'all') {
|
|
5095
|
+
where.push(`tor.status::text = ${this.param(params, filters.status)}`);
|
|
5096
|
+
}
|
|
5097
|
+
|
|
5098
|
+
if (pagination?.search) {
|
|
5099
|
+
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
5100
|
+
where.push(`(
|
|
5101
|
+
COALESCE(c.display_name, '') ILIKE ${searchPlaceholder}
|
|
5102
|
+
OR COALESCE(a.display_name, '') ILIKE ${searchPlaceholder}
|
|
5103
|
+
OR COALESCE(tor.reason, '') ILIKE ${searchPlaceholder}
|
|
5104
|
+
OR COALESCE(tor.request_type::text, '') ILIKE ${searchPlaceholder}
|
|
5105
|
+
)`);
|
|
5106
|
+
}
|
|
5107
|
+
|
|
5108
|
+
const whereClause = where.join(' AND ');
|
|
5109
|
+
const baseQuery = `SELECT tor.id,
|
|
5324
5110
|
tor.collaborator_id AS "collaboratorId",
|
|
5325
5111
|
c.display_name AS "collaboratorName",
|
|
5326
5112
|
tor.approver_collaborator_id AS "approverCollaboratorId",
|
|
5327
5113
|
a.display_name AS "approverName",
|
|
5328
5114
|
tor.request_type AS "requestType",
|
|
5329
|
-
tor.start_date AS "startDate",
|
|
5330
|
-
tor.end_date AS "endDate",
|
|
5115
|
+
tor.start_date::text AS "startDate",
|
|
5116
|
+
tor.end_date::text AS "endDate",
|
|
5331
5117
|
tor.total_days AS "totalDays",
|
|
5332
5118
|
tor.status,
|
|
5333
5119
|
tor.reason,
|
|
@@ -5341,9 +5127,49 @@ export class OperationsService {
|
|
|
5341
5127
|
ON approval.target_type = 'time_off_request'
|
|
5342
5128
|
AND approval.target_id = tor.id
|
|
5343
5129
|
AND approval.deleted_at IS NULL
|
|
5344
|
-
WHERE
|
|
5345
|
-
|
|
5346
|
-
|
|
5130
|
+
WHERE ${whereClause}`;
|
|
5131
|
+
|
|
5132
|
+
if (!pagination) {
|
|
5133
|
+
return this.queryRows(
|
|
5134
|
+
`${baseQuery}
|
|
5135
|
+
ORDER BY tor.start_date DESC, tor.id DESC`,
|
|
5136
|
+
params
|
|
5137
|
+
);
|
|
5138
|
+
}
|
|
5139
|
+
|
|
5140
|
+
const totalRow = await this.querySingle<{ total: string }>(
|
|
5141
|
+
`SELECT COUNT(*)::text AS total
|
|
5142
|
+
FROM operations_time_off_request tor
|
|
5143
|
+
JOIN operations_collaborator c ON c.id = tor.collaborator_id
|
|
5144
|
+
LEFT JOIN operations_collaborator a ON a.id = tor.approver_collaborator_id
|
|
5145
|
+
WHERE ${whereClause}`,
|
|
5146
|
+
params
|
|
5147
|
+
);
|
|
5148
|
+
|
|
5149
|
+
const sortColumn =
|
|
5150
|
+
{
|
|
5151
|
+
startDate: 'tor.start_date',
|
|
5152
|
+
endDate: 'tor.end_date',
|
|
5153
|
+
collaboratorName: 'c.display_name',
|
|
5154
|
+
status: 'tor.status',
|
|
5155
|
+
}[pagination.sortField] ?? 'tor.start_date';
|
|
5156
|
+
|
|
5157
|
+
const queryParams = [...params];
|
|
5158
|
+
const limitPlaceholder = this.param(queryParams, pagination.pageSize);
|
|
5159
|
+
const offsetPlaceholder = this.param(queryParams, pagination.offset);
|
|
5160
|
+
const rows = await this.queryRows(
|
|
5161
|
+
`${baseQuery}
|
|
5162
|
+
ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, tor.id DESC
|
|
5163
|
+
LIMIT ${limitPlaceholder}
|
|
5164
|
+
OFFSET ${offsetPlaceholder}`,
|
|
5165
|
+
queryParams
|
|
5166
|
+
);
|
|
5167
|
+
|
|
5168
|
+
return this.buildPaginationResult(
|
|
5169
|
+
rows,
|
|
5170
|
+
Number(totalRow?.total ?? 0),
|
|
5171
|
+
pagination.page,
|
|
5172
|
+
pagination.pageSize
|
|
5347
5173
|
);
|
|
5348
5174
|
}
|
|
5349
5175
|
|
|
@@ -5413,8 +5239,8 @@ export class OperationsService {
|
|
|
5413
5239
|
collaborator_id AS "collaboratorId",
|
|
5414
5240
|
approver_collaborator_id AS "approverCollaboratorId",
|
|
5415
5241
|
request_type AS "requestType",
|
|
5416
|
-
start_date AS "startDate",
|
|
5417
|
-
end_date AS "endDate",
|
|
5242
|
+
start_date::text AS "startDate",
|
|
5243
|
+
end_date::text AS "endDate",
|
|
5418
5244
|
total_days AS "totalDays",
|
|
5419
5245
|
status,
|
|
5420
5246
|
reason,
|
|
@@ -5426,9 +5252,62 @@ export class OperationsService {
|
|
|
5426
5252
|
);
|
|
5427
5253
|
}
|
|
5428
5254
|
|
|
5429
|
-
async listScheduleAdjustments(
|
|
5255
|
+
async listScheduleAdjustments(
|
|
5256
|
+
userId: number,
|
|
5257
|
+
filters: {
|
|
5258
|
+
page?: number;
|
|
5259
|
+
pageSize?: number;
|
|
5260
|
+
search?: string;
|
|
5261
|
+
sortField?: string;
|
|
5262
|
+
sortOrder?: string;
|
|
5263
|
+
status?: string;
|
|
5264
|
+
} = {}
|
|
5265
|
+
) {
|
|
5430
5266
|
const actor = await this.getActorContext(userId);
|
|
5431
5267
|
const filter = this.buildIdFilter(actor.visibleCollaboratorIds, 'sar.collaborator_id', actor.isDirector);
|
|
5268
|
+
const pagination = this.shouldPaginate(filters)
|
|
5269
|
+
? this.normalizePaginationParams(filters, {
|
|
5270
|
+
defaultSortField: 'effectiveStartDate',
|
|
5271
|
+
defaultSortOrder: 'desc',
|
|
5272
|
+
allowedSortFields: [
|
|
5273
|
+
'effectiveStartDate',
|
|
5274
|
+
'effectiveEndDate',
|
|
5275
|
+
'collaboratorName',
|
|
5276
|
+
'status',
|
|
5277
|
+
],
|
|
5278
|
+
})
|
|
5279
|
+
: null;
|
|
5280
|
+
const params: unknown[] = [...filter.params];
|
|
5281
|
+
const where = ['sar.deleted_at IS NULL', filter.clause];
|
|
5282
|
+
|
|
5283
|
+
if (filters.status && filters.status !== 'all') {
|
|
5284
|
+
where.push(`sar.status::text = ${this.param(params, filters.status)}`);
|
|
5285
|
+
}
|
|
5286
|
+
|
|
5287
|
+
if (pagination?.search) {
|
|
5288
|
+
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
5289
|
+
where.push(`(
|
|
5290
|
+
COALESCE(c.display_name, '') ILIKE ${searchPlaceholder}
|
|
5291
|
+
OR COALESCE(a.display_name, '') ILIKE ${searchPlaceholder}
|
|
5292
|
+
OR COALESCE(sar.reason, '') ILIKE ${searchPlaceholder}
|
|
5293
|
+
OR COALESCE(sar.request_scope::text, '') ILIKE ${searchPlaceholder}
|
|
5294
|
+
)`);
|
|
5295
|
+
}
|
|
5296
|
+
|
|
5297
|
+
const whereClause = where.join(' AND ');
|
|
5298
|
+
const sortColumn =
|
|
5299
|
+
{
|
|
5300
|
+
effectiveStartDate: 'sar.effective_start_date',
|
|
5301
|
+
effectiveEndDate: 'sar.effective_end_date',
|
|
5302
|
+
collaboratorName: 'c.display_name',
|
|
5303
|
+
status: 'sar.status',
|
|
5304
|
+
}[pagination?.sortField ?? 'effectiveStartDate'] ?? 'sar.effective_start_date';
|
|
5305
|
+
const queryParams = [...params];
|
|
5306
|
+
const limitSql =
|
|
5307
|
+
pagination
|
|
5308
|
+
? `LIMIT ${this.param(queryParams, pagination.pageSize)}
|
|
5309
|
+
OFFSET ${this.param(queryParams, pagination.offset)}`
|
|
5310
|
+
: '';
|
|
5432
5311
|
|
|
5433
5312
|
const requests = await this.queryRows<{
|
|
5434
5313
|
id: number;
|
|
@@ -5451,8 +5330,8 @@ export class OperationsService {
|
|
|
5451
5330
|
sar.approver_collaborator_id AS "approverCollaboratorId",
|
|
5452
5331
|
a.display_name AS "approverName",
|
|
5453
5332
|
sar.request_scope AS "requestScope",
|
|
5454
|
-
sar.effective_start_date AS "effectiveStartDate",
|
|
5455
|
-
sar.effective_end_date AS "effectiveEndDate",
|
|
5333
|
+
sar.effective_start_date::text AS "effectiveStartDate",
|
|
5334
|
+
sar.effective_end_date::text AS "effectiveEndDate",
|
|
5456
5335
|
sar.status,
|
|
5457
5336
|
sar.reason,
|
|
5458
5337
|
sar.submitted_at AS "submittedAt",
|
|
@@ -5465,15 +5344,35 @@ export class OperationsService {
|
|
|
5465
5344
|
ON approval.target_type = 'schedule_adjustment_request'
|
|
5466
5345
|
AND approval.target_id = sar.id
|
|
5467
5346
|
AND approval.deleted_at IS NULL
|
|
5468
|
-
WHERE
|
|
5469
|
-
ORDER BY
|
|
5470
|
-
|
|
5347
|
+
WHERE ${whereClause}
|
|
5348
|
+
ORDER BY ${sortColumn} ${pagination?.sortOrder?.toUpperCase() ?? 'DESC'}, sar.id DESC
|
|
5349
|
+
${limitSql}`,
|
|
5350
|
+
queryParams
|
|
5471
5351
|
);
|
|
5472
5352
|
|
|
5473
5353
|
if (!requests.length) {
|
|
5354
|
+
if (pagination) {
|
|
5355
|
+
return this.buildPaginationResult([], 0, pagination.page, pagination.pageSize);
|
|
5356
|
+
}
|
|
5357
|
+
|
|
5474
5358
|
return requests;
|
|
5475
5359
|
}
|
|
5476
5360
|
|
|
5361
|
+
const total = pagination
|
|
5362
|
+
? Number(
|
|
5363
|
+
(
|
|
5364
|
+
await this.querySingle<{ total: string }>(
|
|
5365
|
+
`SELECT COUNT(*)::text AS total
|
|
5366
|
+
FROM operations_schedule_adjustment_request sar
|
|
5367
|
+
JOIN operations_collaborator c ON c.id = sar.collaborator_id
|
|
5368
|
+
LEFT JOIN operations_collaborator a ON a.id = sar.approver_collaborator_id
|
|
5369
|
+
WHERE ${whereClause}`,
|
|
5370
|
+
params
|
|
5371
|
+
)
|
|
5372
|
+
)?.total ?? 0
|
|
5373
|
+
)
|
|
5374
|
+
: 0;
|
|
5375
|
+
|
|
5477
5376
|
const days = await this.queryRows<{
|
|
5478
5377
|
requestId: number;
|
|
5479
5378
|
weekday: string;
|
|
@@ -5520,11 +5419,22 @@ export class OperationsService {
|
|
|
5520
5419
|
currentSchedule,
|
|
5521
5420
|
'collaboratorId'
|
|
5522
5421
|
);
|
|
5523
|
-
|
|
5422
|
+
const data = requests.map((request) => ({
|
|
5524
5423
|
...request,
|
|
5525
5424
|
days: grouped[request.id] ?? [],
|
|
5526
5425
|
currentSchedule: currentScheduleByCollaborator[request.collaboratorId] ?? [],
|
|
5527
5426
|
}));
|
|
5427
|
+
|
|
5428
|
+
if (!pagination) {
|
|
5429
|
+
return data;
|
|
5430
|
+
}
|
|
5431
|
+
|
|
5432
|
+
return this.buildPaginationResult(
|
|
5433
|
+
data,
|
|
5434
|
+
total,
|
|
5435
|
+
pagination.page,
|
|
5436
|
+
pagination.pageSize
|
|
5437
|
+
);
|
|
5528
5438
|
}
|
|
5529
5439
|
|
|
5530
5440
|
async createScheduleAdjustmentRequest(
|
|
@@ -5624,8 +5534,8 @@ export class OperationsService {
|
|
|
5624
5534
|
collaborator_id AS "collaboratorId",
|
|
5625
5535
|
approver_collaborator_id AS "approverCollaboratorId",
|
|
5626
5536
|
request_scope AS "requestScope",
|
|
5627
|
-
effective_start_date AS "effectiveStartDate",
|
|
5628
|
-
effective_end_date AS "effectiveEndDate",
|
|
5537
|
+
effective_start_date::text AS "effectiveStartDate",
|
|
5538
|
+
effective_end_date::text AS "effectiveEndDate",
|
|
5629
5539
|
status,
|
|
5630
5540
|
reason,
|
|
5631
5541
|
submitted_at AS "submittedAt",
|
|
@@ -5636,7 +5546,18 @@ export class OperationsService {
|
|
|
5636
5546
|
);
|
|
5637
5547
|
}
|
|
5638
5548
|
|
|
5639
|
-
async listApprovals(
|
|
5549
|
+
async listApprovals(
|
|
5550
|
+
userId: number,
|
|
5551
|
+
filters: {
|
|
5552
|
+
page?: number;
|
|
5553
|
+
pageSize?: number;
|
|
5554
|
+
search?: string;
|
|
5555
|
+
sortField?: string;
|
|
5556
|
+
sortOrder?: string;
|
|
5557
|
+
status?: string;
|
|
5558
|
+
targetType?: string;
|
|
5559
|
+
} = {}
|
|
5560
|
+
) {
|
|
5640
5561
|
const actor = await this.getActorContext(userId);
|
|
5641
5562
|
this.ensureSupervisor(actor);
|
|
5642
5563
|
|
|
@@ -5647,9 +5568,36 @@ export class OperationsService {
|
|
|
5647
5568
|
params,
|
|
5648
5569
|
actor.collaboratorId
|
|
5649
5570
|
)}`;
|
|
5571
|
+
const pagination = this.shouldPaginate(filters)
|
|
5572
|
+
? this.normalizePaginationParams(filters, {
|
|
5573
|
+
defaultSortField: 'submittedAt',
|
|
5574
|
+
defaultSortOrder: 'desc',
|
|
5575
|
+
allowedSortFields: ['submittedAt', 'decidedAt', 'requesterName', 'status'],
|
|
5576
|
+
})
|
|
5577
|
+
: null;
|
|
5578
|
+
const where = [clause];
|
|
5650
5579
|
|
|
5651
|
-
|
|
5652
|
-
`
|
|
5580
|
+
if (filters.status && filters.status !== 'all') {
|
|
5581
|
+
where.push(`a.status::text = ${this.param(params, filters.status)}`);
|
|
5582
|
+
}
|
|
5583
|
+
|
|
5584
|
+
if (filters.targetType && filters.targetType !== 'all') {
|
|
5585
|
+
where.push(`a.target_type = ${this.param(params, filters.targetType)}::text::operations_approval_target_type_32d3f04385_enum`);
|
|
5586
|
+
}
|
|
5587
|
+
|
|
5588
|
+
if (pagination?.search) {
|
|
5589
|
+
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
5590
|
+
where.push(`(
|
|
5591
|
+
COALESCE(requester.display_name, '') ILIKE ${searchPlaceholder}
|
|
5592
|
+
OR COALESCE(approver.display_name, '') ILIKE ${searchPlaceholder}
|
|
5593
|
+
OR COALESCE(a.decision_note, '') ILIKE ${searchPlaceholder}
|
|
5594
|
+
OR COALESCE(tor.reason, '') ILIKE ${searchPlaceholder}
|
|
5595
|
+
OR COALESCE(sar.reason, '') ILIKE ${searchPlaceholder}
|
|
5596
|
+
)`);
|
|
5597
|
+
}
|
|
5598
|
+
|
|
5599
|
+
const whereClause = where.join(' AND ');
|
|
5600
|
+
const baseQuery = `SELECT a.id,
|
|
5653
5601
|
a.target_type AS "targetType",
|
|
5654
5602
|
a.target_id AS "targetId",
|
|
5655
5603
|
a.requester_collaborator_id AS "requesterCollaboratorId",
|
|
@@ -5668,12 +5616,12 @@ export class OperationsService {
|
|
|
5668
5616
|
''
|
|
5669
5617
|
) AS "timesheetProjectNames",
|
|
5670
5618
|
tor.request_type AS "timeOffType",
|
|
5671
|
-
tor.start_date AS "timeOffStartDate",
|
|
5672
|
-
tor.end_date AS "timeOffEndDate",
|
|
5619
|
+
tor.start_date::text AS "timeOffStartDate",
|
|
5620
|
+
tor.end_date::text AS "timeOffEndDate",
|
|
5673
5621
|
tor.reason AS "timeOffReason",
|
|
5674
5622
|
sar.request_scope AS "scheduleRequestScope",
|
|
5675
|
-
sar.effective_start_date AS "scheduleStartDate",
|
|
5676
|
-
sar.effective_end_date AS "scheduleEndDate",
|
|
5623
|
+
sar.effective_start_date::text AS "scheduleStartDate",
|
|
5624
|
+
sar.effective_end_date::text AS "scheduleEndDate",
|
|
5677
5625
|
sar.reason AS "scheduleReason"
|
|
5678
5626
|
FROM operations_approval a
|
|
5679
5627
|
JOIN operations_collaborator requester
|
|
@@ -5696,11 +5644,62 @@ export class OperationsService {
|
|
|
5696
5644
|
LEFT JOIN operations_schedule_adjustment_request sar
|
|
5697
5645
|
ON a.target_type = 'schedule_adjustment_request'
|
|
5698
5646
|
AND sar.id = a.target_id
|
|
5699
|
-
WHERE ${
|
|
5700
|
-
GROUP BY a.id, requester.id, approver.id, t.id, tor.id, sar.id
|
|
5701
|
-
|
|
5647
|
+
WHERE ${whereClause}
|
|
5648
|
+
GROUP BY a.id, requester.id, approver.id, t.id, tor.id, sar.id`;
|
|
5649
|
+
|
|
5650
|
+
if (!pagination) {
|
|
5651
|
+
return this.queryRows(
|
|
5652
|
+
`${baseQuery}
|
|
5653
|
+
ORDER BY a.submitted_at DESC, a.id DESC`,
|
|
5654
|
+
params
|
|
5655
|
+
);
|
|
5656
|
+
}
|
|
5657
|
+
|
|
5658
|
+
const totalRow = await this.querySingle<{ total: string }>(
|
|
5659
|
+
`SELECT COUNT(*)::text AS total
|
|
5660
|
+
FROM operations_approval a
|
|
5661
|
+
JOIN operations_collaborator requester
|
|
5662
|
+
ON requester.id = a.requester_collaborator_id
|
|
5663
|
+
LEFT JOIN operations_collaborator approver
|
|
5664
|
+
ON approver.id = a.approver_collaborator_id
|
|
5665
|
+
LEFT JOIN operations_timesheet t
|
|
5666
|
+
ON a.target_type = 'timesheet'
|
|
5667
|
+
AND t.id = a.target_id
|
|
5668
|
+
LEFT JOIN operations_time_off_request tor
|
|
5669
|
+
ON a.target_type = 'time_off_request'
|
|
5670
|
+
AND tor.id = a.target_id
|
|
5671
|
+
LEFT JOIN operations_schedule_adjustment_request sar
|
|
5672
|
+
ON a.target_type = 'schedule_adjustment_request'
|
|
5673
|
+
AND sar.id = a.target_id
|
|
5674
|
+
WHERE ${whereClause}`,
|
|
5702
5675
|
params
|
|
5703
5676
|
);
|
|
5677
|
+
|
|
5678
|
+
const sortColumn =
|
|
5679
|
+
{
|
|
5680
|
+
submittedAt: 'a.submitted_at',
|
|
5681
|
+
decidedAt: 'a.decided_at',
|
|
5682
|
+
requesterName: 'requester.display_name',
|
|
5683
|
+
status: 'a.status',
|
|
5684
|
+
}[pagination.sortField] ?? 'a.submitted_at';
|
|
5685
|
+
|
|
5686
|
+
const queryParams = [...params];
|
|
5687
|
+
const limitPlaceholder = this.param(queryParams, pagination.pageSize);
|
|
5688
|
+
const offsetPlaceholder = this.param(queryParams, pagination.offset);
|
|
5689
|
+
const rows = await this.queryRows(
|
|
5690
|
+
`${baseQuery}
|
|
5691
|
+
ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, a.id DESC
|
|
5692
|
+
LIMIT ${limitPlaceholder}
|
|
5693
|
+
OFFSET ${offsetPlaceholder}`,
|
|
5694
|
+
queryParams
|
|
5695
|
+
);
|
|
5696
|
+
|
|
5697
|
+
return this.buildPaginationResult(
|
|
5698
|
+
rows,
|
|
5699
|
+
Number(totalRow?.total ?? 0),
|
|
5700
|
+
pagination.page,
|
|
5701
|
+
pagination.pageSize
|
|
5702
|
+
);
|
|
5704
5703
|
}
|
|
5705
5704
|
|
|
5706
5705
|
async approve(userId: number, approvalId: number, data: DecisionPayload) {
|
|
@@ -5711,6 +5710,193 @@ export class OperationsService {
|
|
|
5711
5710
|
return this.decideApproval(userId, approvalId, 'reject', data);
|
|
5712
5711
|
}
|
|
5713
5712
|
|
|
5713
|
+
async getApprovalDetail(userId: number, approvalId: number) {
|
|
5714
|
+
const actor = await this.getActorContext(userId);
|
|
5715
|
+
this.ensureSupervisor(actor);
|
|
5716
|
+
|
|
5717
|
+
const params: unknown[] = [];
|
|
5718
|
+
const actorClause = actor.isDirector
|
|
5719
|
+
? 'a.deleted_at IS NULL'
|
|
5720
|
+
: `a.deleted_at IS NULL AND a.approver_collaborator_id = ${this.param(params, actor.collaboratorId)}`;
|
|
5721
|
+
const idPlaceholder = this.param(params, approvalId);
|
|
5722
|
+
|
|
5723
|
+
const row = await this.querySingle<{
|
|
5724
|
+
id: number;
|
|
5725
|
+
targetType: string;
|
|
5726
|
+
targetId: number;
|
|
5727
|
+
requesterCollaboratorId: number;
|
|
5728
|
+
requesterName: string;
|
|
5729
|
+
approverCollaboratorId: number | null;
|
|
5730
|
+
approverName: string | null;
|
|
5731
|
+
status: string;
|
|
5732
|
+
submittedAt: string | null;
|
|
5733
|
+
decidedAt: string | null;
|
|
5734
|
+
decisionNote: string | null;
|
|
5735
|
+
timesheetWeekStartDate: string | null;
|
|
5736
|
+
timesheetWeekEndDate: string | null;
|
|
5737
|
+
timesheetTotalHours: number | null;
|
|
5738
|
+
timesheetProjectNames: string | null;
|
|
5739
|
+
timeOffType: string | null;
|
|
5740
|
+
timeOffStartDate: string | null;
|
|
5741
|
+
timeOffEndDate: string | null;
|
|
5742
|
+
timeOffReason: string | null;
|
|
5743
|
+
scheduleRequestScope: string | null;
|
|
5744
|
+
scheduleStartDate: string | null;
|
|
5745
|
+
scheduleEndDate: string | null;
|
|
5746
|
+
scheduleReason: string | null;
|
|
5747
|
+
}>(
|
|
5748
|
+
`SELECT a.id,
|
|
5749
|
+
a.target_type AS "targetType",
|
|
5750
|
+
a.target_id AS "targetId",
|
|
5751
|
+
a.requester_collaborator_id AS "requesterCollaboratorId",
|
|
5752
|
+
requester.display_name AS "requesterName",
|
|
5753
|
+
a.approver_collaborator_id AS "approverCollaboratorId",
|
|
5754
|
+
approver.display_name AS "approverName",
|
|
5755
|
+
a.status,
|
|
5756
|
+
a.submitted_at AS "submittedAt",
|
|
5757
|
+
a.decided_at AS "decidedAt",
|
|
5758
|
+
a.decision_note AS "decisionNote",
|
|
5759
|
+
t.week_start_date AS "timesheetWeekStartDate",
|
|
5760
|
+
t.week_end_date AS "timesheetWeekEndDate",
|
|
5761
|
+
t.total_hours AS "timesheetTotalHours",
|
|
5762
|
+
COALESCE(
|
|
5763
|
+
STRING_AGG(DISTINCT p.name, ', ') FILTER (WHERE p.name IS NOT NULL),
|
|
5764
|
+
''
|
|
5765
|
+
) AS "timesheetProjectNames",
|
|
5766
|
+
tor.request_type AS "timeOffType",
|
|
5767
|
+
tor.start_date::text AS "timeOffStartDate",
|
|
5768
|
+
tor.end_date::text AS "timeOffEndDate",
|
|
5769
|
+
tor.reason AS "timeOffReason",
|
|
5770
|
+
sar.request_scope AS "scheduleRequestScope",
|
|
5771
|
+
sar.effective_start_date::text AS "scheduleStartDate",
|
|
5772
|
+
sar.effective_end_date::text AS "scheduleEndDate",
|
|
5773
|
+
sar.reason AS "scheduleReason"
|
|
5774
|
+
FROM operations_approval a
|
|
5775
|
+
JOIN operations_collaborator requester
|
|
5776
|
+
ON requester.id = a.requester_collaborator_id
|
|
5777
|
+
LEFT JOIN operations_collaborator approver
|
|
5778
|
+
ON approver.id = a.approver_collaborator_id
|
|
5779
|
+
LEFT JOIN operations_timesheet t
|
|
5780
|
+
ON a.target_type = 'timesheet'
|
|
5781
|
+
AND t.id = a.target_id
|
|
5782
|
+
LEFT JOIN operations_timesheet_entry te
|
|
5783
|
+
ON te.timesheet_id = t.id
|
|
5784
|
+
AND te.deleted_at IS NULL
|
|
5785
|
+
LEFT JOIN operations_project_assignment pa
|
|
5786
|
+
ON pa.id = te.project_assignment_id
|
|
5787
|
+
LEFT JOIN operations_project p
|
|
5788
|
+
ON p.id = pa.project_id
|
|
5789
|
+
LEFT JOIN operations_time_off_request tor
|
|
5790
|
+
ON a.target_type = 'time_off_request'
|
|
5791
|
+
AND tor.id = a.target_id
|
|
5792
|
+
LEFT JOIN operations_schedule_adjustment_request sar
|
|
5793
|
+
ON a.target_type = 'schedule_adjustment_request'
|
|
5794
|
+
AND sar.id = a.target_id
|
|
5795
|
+
WHERE ${actorClause}
|
|
5796
|
+
AND a.id = ${idPlaceholder}
|
|
5797
|
+
GROUP BY a.id, requester.id, approver.id, t.id, tor.id, sar.id`,
|
|
5798
|
+
params
|
|
5799
|
+
);
|
|
5800
|
+
|
|
5801
|
+
if (!row) {
|
|
5802
|
+
throw new Error('Approval not found or access denied');
|
|
5803
|
+
}
|
|
5804
|
+
|
|
5805
|
+
if (row.targetType === 'timesheet') {
|
|
5806
|
+
const entries = await this.queryRows<{
|
|
5807
|
+
id: number;
|
|
5808
|
+
timesheetId: number;
|
|
5809
|
+
projectAssignmentId: number | null;
|
|
5810
|
+
projectId: number | null;
|
|
5811
|
+
projectName: string | null;
|
|
5812
|
+
roleLabel: string | null;
|
|
5813
|
+
taskId: number | null;
|
|
5814
|
+
taskName: string | null;
|
|
5815
|
+
activityLabel: string | null;
|
|
5816
|
+
workDate: string;
|
|
5817
|
+
durationMinutes: number | null;
|
|
5818
|
+
hours: number;
|
|
5819
|
+
description: string | null;
|
|
5820
|
+
}>(
|
|
5821
|
+
`SELECT e.id,
|
|
5822
|
+
e.timesheet_id AS "timesheetId",
|
|
5823
|
+
e.project_assignment_id AS "projectAssignmentId",
|
|
5824
|
+
pa.project_id AS "projectId",
|
|
5825
|
+
p.name AS "projectName",
|
|
5826
|
+
pa.role_label AS "roleLabel",
|
|
5827
|
+
e.task_id AS "taskId",
|
|
5828
|
+
task_record.name AS "taskName",
|
|
5829
|
+
COALESCE(task_record.name, e.activity_label) AS "activityLabel",
|
|
5830
|
+
e.work_date AS "workDate",
|
|
5831
|
+
COALESCE(NULLIF(e.duration_minutes, 0), ROUND(COALESCE(e.hours, 0)::numeric * 60))::int AS "durationMinutes",
|
|
5832
|
+
COALESCE(
|
|
5833
|
+
e.hours,
|
|
5834
|
+
ROUND((COALESCE(NULLIF(e.duration_minutes, 0), 0)::numeric / 60), 2)
|
|
5835
|
+
) AS hours,
|
|
5836
|
+
e.description
|
|
5837
|
+
FROM operations_timesheet_entry e
|
|
5838
|
+
LEFT JOIN operations_project_assignment pa ON pa.id = e.project_assignment_id
|
|
5839
|
+
LEFT JOIN operations_project p ON p.id = pa.project_id
|
|
5840
|
+
LEFT JOIN operations_task task_record
|
|
5841
|
+
ON task_record.id = e.task_id
|
|
5842
|
+
AND task_record.deleted_at IS NULL
|
|
5843
|
+
WHERE e.deleted_at IS NULL
|
|
5844
|
+
AND e.timesheet_id = $1
|
|
5845
|
+
ORDER BY e.work_date ASC, e.id ASC`,
|
|
5846
|
+
[row.targetId]
|
|
5847
|
+
);
|
|
5848
|
+
return { ...row, entries };
|
|
5849
|
+
}
|
|
5850
|
+
|
|
5851
|
+
if (row.targetType === 'schedule_adjustment_request') {
|
|
5852
|
+
const days = await this.queryRows<{
|
|
5853
|
+
requestId: number;
|
|
5854
|
+
weekday: string;
|
|
5855
|
+
isWorkingDay: boolean;
|
|
5856
|
+
startTime: string | null;
|
|
5857
|
+
endTime: string | null;
|
|
5858
|
+
breakMinutes: number | null;
|
|
5859
|
+
}>(
|
|
5860
|
+
`SELECT schedule_adjustment_request_id AS "requestId",
|
|
5861
|
+
weekday,
|
|
5862
|
+
is_working_day AS "isWorkingDay",
|
|
5863
|
+
start_time AS "startTime",
|
|
5864
|
+
end_time AS "endTime",
|
|
5865
|
+
break_minutes AS "breakMinutes"
|
|
5866
|
+
FROM operations_schedule_adjustment_day
|
|
5867
|
+
WHERE schedule_adjustment_request_id = $1
|
|
5868
|
+
ORDER BY id ASC`,
|
|
5869
|
+
[row.targetId]
|
|
5870
|
+
);
|
|
5871
|
+
|
|
5872
|
+
const collaboratorIdParam: unknown[] = [row.requesterCollaboratorId];
|
|
5873
|
+
const currentSchedule = await this.queryRows<{
|
|
5874
|
+
collaboratorId: number;
|
|
5875
|
+
weekday: string;
|
|
5876
|
+
isWorkingDay: boolean;
|
|
5877
|
+
startTime: string | null;
|
|
5878
|
+
endTime: string | null;
|
|
5879
|
+
breakMinutes: number | null;
|
|
5880
|
+
}>(
|
|
5881
|
+
`SELECT DISTINCT ON (collaborator_id, weekday)
|
|
5882
|
+
collaborator_id AS "collaboratorId",
|
|
5883
|
+
weekday,
|
|
5884
|
+
is_working_day AS "isWorkingDay",
|
|
5885
|
+
start_time AS "startTime",
|
|
5886
|
+
end_time AS "endTime",
|
|
5887
|
+
break_minutes AS "breakMinutes"
|
|
5888
|
+
FROM operations_collaborator_schedule_day
|
|
5889
|
+
WHERE collaborator_id = $1
|
|
5890
|
+
ORDER BY collaborator_id, weekday, id DESC`,
|
|
5891
|
+
collaboratorIdParam
|
|
5892
|
+
);
|
|
5893
|
+
|
|
5894
|
+
return { ...row, days, currentSchedule };
|
|
5895
|
+
}
|
|
5896
|
+
|
|
5897
|
+
return row;
|
|
5898
|
+
}
|
|
5899
|
+
|
|
5714
5900
|
async publishAccountsPayableReference(
|
|
5715
5901
|
userId: number,
|
|
5716
5902
|
data: PublishAccountsPayableReferencePayload,
|
|
@@ -6681,200 +6867,51 @@ export class OperationsService {
|
|
|
6681
6867
|
return `${baseSlug}-${Date.now().toString(36)}`;
|
|
6682
6868
|
}
|
|
6683
6869
|
|
|
6684
|
-
private async resolveCollaboratorTypeReference(
|
|
6685
|
-
client: any,
|
|
6686
|
-
input: {
|
|
6687
|
-
collaboratorTypeId?: number | null;
|
|
6688
|
-
collaboratorTypeSlug?: string | null;
|
|
6689
|
-
}
|
|
6690
|
-
) {
|
|
6691
|
-
if (
|
|
6692
|
-
typeof input.collaboratorTypeId === 'number' &&
|
|
6693
|
-
Number.isInteger(input.collaboratorTypeId) &&
|
|
6694
|
-
input.collaboratorTypeId > 0
|
|
6695
|
-
) {
|
|
6696
|
-
const collaboratorTypes = (await client.$queryRawUnsafe(
|
|
6697
|
-
`SELECT id, slug, name
|
|
6698
|
-
FROM operations_collaborator_type
|
|
6699
|
-
WHERE id = $1
|
|
6700
|
-
AND deleted_at IS NULL
|
|
6701
|
-
LIMIT 1`,
|
|
6702
|
-
input.collaboratorTypeId
|
|
6703
|
-
)) as Array<{ id: number; slug: string; name: string }>;
|
|
6704
|
-
|
|
6705
|
-
return collaboratorTypes[0] ?? null;
|
|
6706
|
-
}
|
|
6707
|
-
|
|
6708
|
-
const normalizedLookup = this.normalizeOptionalText(input.collaboratorTypeSlug);
|
|
6709
|
-
if (!normalizedLookup) {
|
|
6710
|
-
return null;
|
|
6711
|
-
}
|
|
6712
|
-
|
|
6713
|
-
const collaboratorTypes = (await client.$queryRawUnsafe(
|
|
6714
|
-
`SELECT id, slug, name
|
|
6715
|
-
FROM operations_collaborator_type
|
|
6716
|
-
WHERE deleted_at IS NULL
|
|
6717
|
-
AND (
|
|
6718
|
-
LOWER(slug) = LOWER($1)
|
|
6719
|
-
OR LOWER(name) = LOWER($1)
|
|
6720
|
-
)
|
|
6721
|
-
ORDER BY id ASC
|
|
6722
|
-
LIMIT 1`,
|
|
6723
|
-
normalizedLookup
|
|
6724
|
-
)) as Array<{ id: number; slug: string; name: string }>;
|
|
6725
|
-
|
|
6726
|
-
return collaboratorTypes[0] ?? null;
|
|
6727
|
-
}
|
|
6728
|
-
|
|
6729
|
-
private async getContractTemplateRecord(
|
|
6730
|
-
client: any,
|
|
6731
|
-
templateId: number,
|
|
6732
|
-
includeInactive = false
|
|
6733
|
-
): Promise<{
|
|
6734
|
-
id: number;
|
|
6735
|
-
slug: string;
|
|
6736
|
-
code: string | null;
|
|
6737
|
-
name: string;
|
|
6738
|
-
description: string | null;
|
|
6739
|
-
contractCategory: string | null;
|
|
6740
|
-
contractType: string | null;
|
|
6741
|
-
billingModel: string | null;
|
|
6742
|
-
signatureStatus: string | null;
|
|
6743
|
-
isActive: boolean;
|
|
6744
|
-
status: string | null;
|
|
6745
|
-
contentHtml: string | null;
|
|
6746
|
-
usageCount: number;
|
|
6747
|
-
createdAt: Date;
|
|
6748
|
-
updatedAt: Date;
|
|
6749
|
-
}> {
|
|
6750
|
-
const template = (await client.$queryRawUnsafe(
|
|
6751
|
-
`SELECT t.id,
|
|
6752
|
-
t.slug,
|
|
6753
|
-
t.code,
|
|
6754
|
-
t.name,
|
|
6755
|
-
t.description,
|
|
6756
|
-
t.contract_category AS "contractCategory",
|
|
6757
|
-
t.contract_type AS "contractType",
|
|
6758
|
-
t.billing_model AS "billingModel",
|
|
6759
|
-
t.signature_status AS "signatureStatus",
|
|
6760
|
-
t.is_active AS "isActive",
|
|
6761
|
-
t.status,
|
|
6762
|
-
t.content_html AS "contentHtml",
|
|
6763
|
-
COUNT(DISTINCT c.id)::int AS "usageCount",
|
|
6764
|
-
t.created_at AS "createdAt",
|
|
6765
|
-
t.updated_at AS "updatedAt"
|
|
6766
|
-
FROM operations_contract_template t
|
|
6767
|
-
LEFT JOIN operations_contract c
|
|
6768
|
-
ON c.contract_template_id = t.id
|
|
6769
|
-
AND c.deleted_at IS NULL
|
|
6770
|
-
WHERE t.id = $1
|
|
6771
|
-
AND ($2::boolean = true OR t.deleted_at IS NULL)
|
|
6772
|
-
GROUP BY t.id
|
|
6773
|
-
LIMIT 1`,
|
|
6774
|
-
templateId,
|
|
6775
|
-
includeInactive
|
|
6776
|
-
)) as Array<{
|
|
6777
|
-
id: number;
|
|
6778
|
-
slug: string;
|
|
6779
|
-
code: string | null;
|
|
6780
|
-
name: string;
|
|
6781
|
-
description: string | null;
|
|
6782
|
-
contractCategory: string | null;
|
|
6783
|
-
contractType: string | null;
|
|
6784
|
-
billingModel: string | null;
|
|
6785
|
-
signatureStatus: string | null;
|
|
6786
|
-
isActive: boolean;
|
|
6787
|
-
status: string | null;
|
|
6788
|
-
contentHtml: string | null;
|
|
6789
|
-
usageCount: number;
|
|
6790
|
-
createdAt: Date;
|
|
6791
|
-
updatedAt: Date;
|
|
6792
|
-
}>;
|
|
6793
|
-
|
|
6794
|
-
const record = template[0];
|
|
6795
|
-
|
|
6796
|
-
if (!record) {
|
|
6797
|
-
throw new NotFoundException('Contract template not found.');
|
|
6798
|
-
}
|
|
6799
|
-
|
|
6800
|
-
return record;
|
|
6801
|
-
}
|
|
6802
|
-
|
|
6803
|
-
private async assertContractTemplateNameAvailable(
|
|
6804
|
-
client: any,
|
|
6805
|
-
name: string,
|
|
6806
|
-
excludeTemplateId?: number | null
|
|
6807
|
-
) {
|
|
6808
|
-
const existing = (await client.$queryRawUnsafe(
|
|
6809
|
-
`SELECT id
|
|
6810
|
-
FROM operations_contract_template
|
|
6811
|
-
WHERE LOWER(name) = LOWER($1)
|
|
6812
|
-
AND deleted_at IS NULL
|
|
6813
|
-
AND ($2::int IS NULL OR id <> $2)
|
|
6814
|
-
LIMIT 1`,
|
|
6815
|
-
name,
|
|
6816
|
-
excludeTemplateId ?? null
|
|
6817
|
-
)) as { id: number }[];
|
|
6818
|
-
|
|
6819
|
-
if (existing[0]) {
|
|
6820
|
-
throw new BadRequestException(
|
|
6821
|
-
'A contract template with this name already exists.'
|
|
6822
|
-
);
|
|
6823
|
-
}
|
|
6824
|
-
}
|
|
6825
|
-
|
|
6826
|
-
private async assertContractTemplateCodeAvailable(
|
|
6827
|
-
client: any,
|
|
6828
|
-
code: string,
|
|
6829
|
-
excludeTemplateId?: number | null
|
|
6830
|
-
) {
|
|
6831
|
-
const existing = (await client.$queryRawUnsafe(
|
|
6832
|
-
`SELECT id
|
|
6833
|
-
FROM operations_contract_template
|
|
6834
|
-
WHERE UPPER(COALESCE(code, '')) = UPPER($1)
|
|
6835
|
-
AND deleted_at IS NULL
|
|
6836
|
-
AND ($2::int IS NULL OR id <> $2)
|
|
6837
|
-
LIMIT 1`,
|
|
6838
|
-
code,
|
|
6839
|
-
excludeTemplateId ?? null
|
|
6840
|
-
)) as { id: number }[];
|
|
6841
|
-
|
|
6842
|
-
if (existing[0]) {
|
|
6843
|
-
throw new BadRequestException(
|
|
6844
|
-
'A contract template with this code already exists.'
|
|
6845
|
-
);
|
|
6846
|
-
}
|
|
6847
|
-
}
|
|
6848
|
-
|
|
6849
|
-
private async generateUniqueContractTemplateSlug(
|
|
6850
|
-
client: any,
|
|
6851
|
-
label: string,
|
|
6852
|
-
excludeTemplateId?: number | null
|
|
6853
|
-
) {
|
|
6854
|
-
const baseSlug =
|
|
6855
|
-
this.slugifyValue(label) || `contract-template-${Date.now().toString(36)}`;
|
|
6856
|
-
|
|
6857
|
-
for (let attempt = 0; attempt < 25; attempt += 1) {
|
|
6858
|
-
const candidate =
|
|
6859
|
-
attempt === 0 ? baseSlug : `${baseSlug}-${attempt + 1}`;
|
|
6860
|
-
const existing = (await client.$queryRawUnsafe(
|
|
6861
|
-
`SELECT id
|
|
6862
|
-
FROM operations_contract_template
|
|
6863
|
-
WHERE slug = $1
|
|
6864
|
-
AND ($2::int IS NULL OR id <> $2)
|
|
6865
|
-
LIMIT 1`,
|
|
6866
|
-
candidate,
|
|
6867
|
-
excludeTemplateId ?? null
|
|
6868
|
-
)) as { id: number }[];
|
|
6869
|
-
|
|
6870
|
-
if (!existing.length) {
|
|
6871
|
-
return candidate;
|
|
6872
|
-
}
|
|
6873
|
-
}
|
|
6874
|
-
|
|
6875
|
-
return `${baseSlug}-${Date.now().toString(36)}`;
|
|
6876
|
-
}
|
|
6877
|
-
|
|
6870
|
+
private async resolveCollaboratorTypeReference(
|
|
6871
|
+
client: any,
|
|
6872
|
+
input: {
|
|
6873
|
+
collaboratorTypeId?: number | null;
|
|
6874
|
+
collaboratorTypeSlug?: string | null;
|
|
6875
|
+
}
|
|
6876
|
+
) {
|
|
6877
|
+
if (
|
|
6878
|
+
typeof input.collaboratorTypeId === 'number' &&
|
|
6879
|
+
Number.isInteger(input.collaboratorTypeId) &&
|
|
6880
|
+
input.collaboratorTypeId > 0
|
|
6881
|
+
) {
|
|
6882
|
+
const collaboratorTypes = (await client.$queryRawUnsafe(
|
|
6883
|
+
`SELECT id, slug, name
|
|
6884
|
+
FROM operations_collaborator_type
|
|
6885
|
+
WHERE id = $1
|
|
6886
|
+
AND deleted_at IS NULL
|
|
6887
|
+
LIMIT 1`,
|
|
6888
|
+
input.collaboratorTypeId
|
|
6889
|
+
)) as Array<{ id: number; slug: string; name: string }>;
|
|
6890
|
+
|
|
6891
|
+
return collaboratorTypes[0] ?? null;
|
|
6892
|
+
}
|
|
6893
|
+
|
|
6894
|
+
const normalizedLookup = this.normalizeOptionalText(input.collaboratorTypeSlug);
|
|
6895
|
+
if (!normalizedLookup) {
|
|
6896
|
+
return null;
|
|
6897
|
+
}
|
|
6898
|
+
|
|
6899
|
+
const collaboratorTypes = (await client.$queryRawUnsafe(
|
|
6900
|
+
`SELECT id, slug, name
|
|
6901
|
+
FROM operations_collaborator_type
|
|
6902
|
+
WHERE deleted_at IS NULL
|
|
6903
|
+
AND (
|
|
6904
|
+
LOWER(slug) = LOWER($1)
|
|
6905
|
+
OR LOWER(name) = LOWER($1)
|
|
6906
|
+
)
|
|
6907
|
+
ORDER BY id ASC
|
|
6908
|
+
LIMIT 1`,
|
|
6909
|
+
normalizedLookup
|
|
6910
|
+
)) as Array<{ id: number; slug: string; name: string }>;
|
|
6911
|
+
|
|
6912
|
+
return collaboratorTypes[0] ?? null;
|
|
6913
|
+
}
|
|
6914
|
+
|
|
6878
6915
|
private slugifyValue(value: string | null | undefined) {
|
|
6879
6916
|
return (value ?? '')
|
|
6880
6917
|
.normalize('NFKD')
|
|
@@ -7077,12 +7114,12 @@ export class OperationsService {
|
|
|
7077
7114
|
),
|
|
7078
7115
|
this.querySingle<{
|
|
7079
7116
|
activeAssignments: string;
|
|
7080
|
-
|
|
7117
|
+
completedAssignments: string;
|
|
7081
7118
|
averageAllocation: string | null;
|
|
7082
7119
|
totalWeeklyHours: string | null;
|
|
7083
7120
|
}>(
|
|
7084
7121
|
`SELECT COUNT(*) FILTER (WHERE status IN ('planned', 'active'))::text AS "activeAssignments",
|
|
7085
|
-
COUNT(*) FILTER (WHERE
|
|
7122
|
+
COUNT(*) FILTER (WHERE status = 'completed')::text AS "completedAssignments",
|
|
7086
7123
|
COALESCE(AVG(allocation_percent), 0)::text AS "averageAllocation",
|
|
7087
7124
|
COALESCE(SUM(weekly_hours), 0)::text AS "totalWeeklyHours"
|
|
7088
7125
|
FROM operations_project_assignment
|
|
@@ -7103,8 +7140,8 @@ export class OperationsService {
|
|
|
7103
7140
|
},
|
|
7104
7141
|
operationalIndicators: {
|
|
7105
7142
|
activeAssignments: Number(operationalIndicators?.activeAssignments ?? 0),
|
|
7106
|
-
|
|
7107
|
-
operationalIndicators?.
|
|
7143
|
+
completedAssignments: Number(
|
|
7144
|
+
operationalIndicators?.completedAssignments ?? 0
|
|
7108
7145
|
),
|
|
7109
7146
|
averageAllocation: Number(
|
|
7110
7147
|
operationalIndicators?.averageAllocation ?? 0
|
|
@@ -7114,6 +7151,77 @@ export class OperationsService {
|
|
|
7114
7151
|
};
|
|
7115
7152
|
}
|
|
7116
7153
|
|
|
7154
|
+
async getProjectStats(userId: number, projectId: number) {
|
|
7155
|
+
const [weeklyVelocityRows, allocationRows, quickRadarRow] = await Promise.all([
|
|
7156
|
+
this.prisma.$queryRawUnsafe<Array<{ weekStart: string; loggedHours: string }>>(
|
|
7157
|
+
`SELECT TO_CHAR(DATE_TRUNC('week', e.work_date), 'DD/MM') AS "weekStart",
|
|
7158
|
+
COALESCE(SUM(e.hours), 0)::text AS "loggedHours"
|
|
7159
|
+
FROM operations_project_assignment pa
|
|
7160
|
+
JOIN operations_timesheet_entry e ON e.project_assignment_id = pa.id AND e.deleted_at IS NULL
|
|
7161
|
+
WHERE pa.project_id = $1
|
|
7162
|
+
AND pa.deleted_at IS NULL
|
|
7163
|
+
AND e.work_date >= NOW() - INTERVAL '7 weeks'
|
|
7164
|
+
GROUP BY DATE_TRUNC('week', e.work_date)
|
|
7165
|
+
ORDER BY DATE_TRUNC('week', e.work_date) ASC
|
|
7166
|
+
LIMIT 7`,
|
|
7167
|
+
projectId
|
|
7168
|
+
),
|
|
7169
|
+
this.prisma.$queryRawUnsafe<Array<{ name: string; allocation: string }>>(
|
|
7170
|
+
`SELECT c.display_name AS "name",
|
|
7171
|
+
COALESCE(pa.allocation_percent, 0)::text AS "allocation"
|
|
7172
|
+
FROM operations_project_assignment pa
|
|
7173
|
+
JOIN operations_collaborator c ON c.id = pa.collaborator_id AND c.deleted_at IS NULL
|
|
7174
|
+
WHERE pa.project_id = $1
|
|
7175
|
+
AND pa.deleted_at IS NULL
|
|
7176
|
+
AND pa.status IN ('planned', 'active')
|
|
7177
|
+
ORDER BY c.display_name ASC
|
|
7178
|
+
LIMIT 6`,
|
|
7179
|
+
projectId
|
|
7180
|
+
),
|
|
7181
|
+
this.prisma.$queryRawUnsafe<{ activeAssignments: string; pendingTimesheets: string; totalWeeklyHours: string }>(
|
|
7182
|
+
`SELECT
|
|
7183
|
+
(SELECT COUNT(*)::text
|
|
7184
|
+
FROM operations_task tk
|
|
7185
|
+
WHERE tk.project_id = $1
|
|
7186
|
+
AND tk.deleted_at IS NULL
|
|
7187
|
+
AND tk.assignee_collaborator_id IS NOT NULL
|
|
7188
|
+
AND tk.status IN ('todo', 'doing', 'review')
|
|
7189
|
+
) AS "activeAssignments",
|
|
7190
|
+
COUNT(DISTINCT ts.id) FILTER (WHERE ts.status = 'submitted')::text AS "pendingTimesheets",
|
|
7191
|
+
COALESCE(SUM(pa.weekly_hours) FILTER (WHERE pa.status IN ('planned', 'active')), 0)::text AS "totalWeeklyHours"
|
|
7192
|
+
FROM operations_project_assignment pa
|
|
7193
|
+
LEFT JOIN operations_timesheet_entry e ON e.project_assignment_id = pa.id AND e.deleted_at IS NULL
|
|
7194
|
+
LEFT JOIN operations_timesheet ts ON ts.id = e.timesheet_id AND ts.deleted_at IS NULL
|
|
7195
|
+
WHERE pa.project_id = $1 AND pa.deleted_at IS NULL`,
|
|
7196
|
+
projectId
|
|
7197
|
+
).then(rows => Array.isArray(rows) ? rows[0] : rows),
|
|
7198
|
+
]);
|
|
7199
|
+
|
|
7200
|
+
const getInitials = (name: string) =>
|
|
7201
|
+
name
|
|
7202
|
+
.split(' ')
|
|
7203
|
+
.map((part) => part[0])
|
|
7204
|
+
.slice(0, 2)
|
|
7205
|
+
.join('')
|
|
7206
|
+
.toUpperCase();
|
|
7207
|
+
|
|
7208
|
+
return {
|
|
7209
|
+
weeklyVelocity: weeklyVelocityRows.map((row) => ({
|
|
7210
|
+
weekLabel: row.weekStart,
|
|
7211
|
+
loggedHours: Number(row.loggedHours),
|
|
7212
|
+
})),
|
|
7213
|
+
allocationByCollaborator: allocationRows.map((row) => ({
|
|
7214
|
+
name: getInitials(row.name),
|
|
7215
|
+
allocation: Math.round(Number(row.allocation)),
|
|
7216
|
+
})),
|
|
7217
|
+
quickRadar: {
|
|
7218
|
+
activeAssignments: Number(quickRadarRow?.activeAssignments ?? 0),
|
|
7219
|
+
pendingTimesheets: Number(quickRadarRow?.pendingTimesheets ?? 0),
|
|
7220
|
+
totalWeeklyHours: Number(quickRadarRow?.totalWeeklyHours ?? 0),
|
|
7221
|
+
},
|
|
7222
|
+
};
|
|
7223
|
+
}
|
|
7224
|
+
|
|
7117
7225
|
private async getCollaboratorDetails(collaboratorId: number) {
|
|
7118
7226
|
const collaborator = await this.querySingle<{
|
|
7119
7227
|
id: number;
|
|
@@ -7321,8 +7429,8 @@ export class OperationsService {
|
|
|
7321
7429
|
this.queryRows(
|
|
7322
7430
|
`SELECT id,
|
|
7323
7431
|
request_scope AS "requestScope",
|
|
7324
|
-
effective_start_date AS "effectiveStartDate",
|
|
7325
|
-
effective_end_date AS "effectiveEndDate",
|
|
7432
|
+
effective_start_date::text AS "effectiveStartDate",
|
|
7433
|
+
effective_end_date::text AS "effectiveEndDate",
|
|
7326
7434
|
status,
|
|
7327
7435
|
reason
|
|
7328
7436
|
FROM operations_schedule_adjustment_request
|
|
@@ -8340,15 +8448,14 @@ export class OperationsService {
|
|
|
8340
8448
|
role_label,
|
|
8341
8449
|
allocation_percent,
|
|
8342
8450
|
weekly_hours,
|
|
8343
|
-
is_billable,
|
|
8344
8451
|
start_date,
|
|
8345
8452
|
end_date,
|
|
8346
8453
|
status,
|
|
8347
8454
|
created_at,
|
|
8348
8455
|
updated_at
|
|
8349
8456
|
) VALUES (
|
|
8350
|
-
$1, $2, $3, $4, $5, $6, $7
|
|
8351
|
-
$
|
|
8457
|
+
$1, $2, $3, $4, $5, $6, $7::date, $8::date,
|
|
8458
|
+
$9::operations_project_assignment_status_155b459bbf_enum,
|
|
8352
8459
|
NOW(), NOW()
|
|
8353
8460
|
)`,
|
|
8354
8461
|
projectId,
|
|
@@ -8359,7 +8466,6 @@ export class OperationsService {
|
|
|
8359
8466
|
'Team Member',
|
|
8360
8467
|
assignment.allocationPercent ?? null,
|
|
8361
8468
|
assignment.weeklyHours ?? null,
|
|
8362
|
-
assignment.isBillable ?? true,
|
|
8363
8469
|
assignment.startDate ?? null,
|
|
8364
8470
|
assignment.endDate ?? null,
|
|
8365
8471
|
assignment.status ?? 'active'
|
|
@@ -8733,185 +8839,6 @@ export class OperationsService {
|
|
|
8733
8839
|
);
|
|
8734
8840
|
}
|
|
8735
8841
|
|
|
8736
|
-
private async createProjectContractDraft(
|
|
8737
|
-
client: any,
|
|
8738
|
-
createdByUserId: number,
|
|
8739
|
-
input: {
|
|
8740
|
-
projectId: number;
|
|
8741
|
-
contractTemplateId: number | null;
|
|
8742
|
-
projectCode: string;
|
|
8743
|
-
projectName: string;
|
|
8744
|
-
clientName: string;
|
|
8745
|
-
managerCollaboratorId: number | null;
|
|
8746
|
-
startDate: string | null;
|
|
8747
|
-
endDate: string | null;
|
|
8748
|
-
budgetAmount: number | null;
|
|
8749
|
-
monthlyHourCap: number | null;
|
|
8750
|
-
billingModel:
|
|
8751
|
-
| 'time_and_material'
|
|
8752
|
-
| 'monthly_retainer'
|
|
8753
|
-
| 'fixed_price';
|
|
8754
|
-
contractCode: string | null;
|
|
8755
|
-
contractName: string | null;
|
|
8756
|
-
description: string | null;
|
|
8757
|
-
}
|
|
8758
|
-
) {
|
|
8759
|
-
const templateRows = input.contractTemplateId
|
|
8760
|
-
? ((await client.$queryRawUnsafe(
|
|
8761
|
-
`SELECT id,
|
|
8762
|
-
code,
|
|
8763
|
-
name,
|
|
8764
|
-
description,
|
|
8765
|
-
contract_category AS "contractCategory",
|
|
8766
|
-
contract_type AS "contractType",
|
|
8767
|
-
billing_model AS "billingModel",
|
|
8768
|
-
signature_status AS "signatureStatus",
|
|
8769
|
-
content_html AS "contentHtml"
|
|
8770
|
-
FROM operations_contract_template
|
|
8771
|
-
WHERE id = $1
|
|
8772
|
-
AND deleted_at IS NULL
|
|
8773
|
-
LIMIT 1`,
|
|
8774
|
-
input.contractTemplateId
|
|
8775
|
-
)) as Array<{
|
|
8776
|
-
id: number;
|
|
8777
|
-
code?: string | null;
|
|
8778
|
-
name?: string | null;
|
|
8779
|
-
description?: string | null;
|
|
8780
|
-
contractCategory?: string | null;
|
|
8781
|
-
contractType?: string | null;
|
|
8782
|
-
billingModel?: string | null;
|
|
8783
|
-
signatureStatus?: string | null;
|
|
8784
|
-
contentHtml?: string | null;
|
|
8785
|
-
}>)
|
|
8786
|
-
: [];
|
|
8787
|
-
|
|
8788
|
-
const selectedTemplate = templateRows[0] ?? null;
|
|
8789
|
-
const templateContext = {
|
|
8790
|
-
project_code: input.projectCode,
|
|
8791
|
-
project_name: input.projectName,
|
|
8792
|
-
client_name: input.clientName,
|
|
8793
|
-
start_date: input.startDate ?? '',
|
|
8794
|
-
end_date: input.endDate ?? '',
|
|
8795
|
-
budget_amount:
|
|
8796
|
-
input.budgetAmount !== null && input.budgetAmount !== undefined
|
|
8797
|
-
? String(input.budgetAmount)
|
|
8798
|
-
: '',
|
|
8799
|
-
monthly_hour_cap:
|
|
8800
|
-
input.monthlyHourCap !== null && input.monthlyHourCap !== undefined
|
|
8801
|
-
? String(input.monthlyHourCap)
|
|
8802
|
-
: '',
|
|
8803
|
-
};
|
|
8804
|
-
|
|
8805
|
-
const applyTemplateVariables = (value: string | null | undefined) => {
|
|
8806
|
-
const source = value ?? '';
|
|
8807
|
-
return Object.entries(templateContext).reduce(
|
|
8808
|
-
(result, [key, replacement]) =>
|
|
8809
|
-
result.split(`{{${key}}}`).join(replacement || ''),
|
|
8810
|
-
source
|
|
8811
|
-
);
|
|
8812
|
-
};
|
|
8813
|
-
|
|
8814
|
-
const templateCodePrefix = this.normalizeOptionalText(selectedTemplate?.code);
|
|
8815
|
-
const generatedContractCode = (
|
|
8816
|
-
this.normalizeOptionalText(input.contractCode) ??
|
|
8817
|
-
(templateCodePrefix
|
|
8818
|
-
? `${templateCodePrefix}-${input.projectCode}`
|
|
8819
|
-
: null) ??
|
|
8820
|
-
`PRJ-${input.projectCode}`
|
|
8821
|
-
).slice(0, 40);
|
|
8822
|
-
|
|
8823
|
-
const generatedContractName =
|
|
8824
|
-
this.normalizeOptionalText(input.contractName) ??
|
|
8825
|
-
this.normalizeOptionalText(applyTemplateVariables(selectedTemplate?.name)) ??
|
|
8826
|
-
`${input.projectName} Service Agreement`;
|
|
8827
|
-
|
|
8828
|
-
const generatedDescription = this.normalizeOptionalText(
|
|
8829
|
-
applyTemplateVariables(input.description ?? selectedTemplate?.description)
|
|
8830
|
-
);
|
|
8831
|
-
|
|
8832
|
-
const generatedContentHtml = this.normalizeOptionalText(
|
|
8833
|
-
applyTemplateVariables(selectedTemplate?.contentHtml)
|
|
8834
|
-
);
|
|
8835
|
-
|
|
8836
|
-
const created = await client.$queryRawUnsafe(
|
|
8837
|
-
`INSERT INTO operations_contract (
|
|
8838
|
-
code,
|
|
8839
|
-
name,
|
|
8840
|
-
contract_category,
|
|
8841
|
-
contract_type,
|
|
8842
|
-
client_name,
|
|
8843
|
-
signature_status,
|
|
8844
|
-
is_active,
|
|
8845
|
-
billing_model,
|
|
8846
|
-
account_manager_collaborator_id,
|
|
8847
|
-
related_collaborator_id,
|
|
8848
|
-
contract_template_id,
|
|
8849
|
-
origin_type,
|
|
8850
|
-
origin_id,
|
|
8851
|
-
start_date,
|
|
8852
|
-
end_date,
|
|
8853
|
-
signed_at,
|
|
8854
|
-
effective_date,
|
|
8855
|
-
budget_amount,
|
|
8856
|
-
monthly_hour_cap,
|
|
8857
|
-
status,
|
|
8858
|
-
description,
|
|
8859
|
-
content_html,
|
|
8860
|
-
created_by_user_id,
|
|
8861
|
-
updated_by_user_id,
|
|
8862
|
-
created_at,
|
|
8863
|
-
updated_at
|
|
8864
|
-
) VALUES (
|
|
8865
|
-
$1,
|
|
8866
|
-
$2,
|
|
8867
|
-
$3::operations_contract_contract_category_70d553ea09_enum,
|
|
8868
|
-
$4::operations_contract_contract_type_48331e2ebf_enum,
|
|
8869
|
-
$5,
|
|
8870
|
-
$6::operations_contract_signature_status_2cb7282a7b_enum,
|
|
8871
|
-
true,
|
|
8872
|
-
$7::operations_contract_billing_model_409dc7fea2_enum,
|
|
8873
|
-
$8,
|
|
8874
|
-
NULL,
|
|
8875
|
-
$9,
|
|
8876
|
-
'client_project',
|
|
8877
|
-
$10,
|
|
8878
|
-
$11::date,
|
|
8879
|
-
$12::date,
|
|
8880
|
-
NULL,
|
|
8881
|
-
$11::date,
|
|
8882
|
-
$13,
|
|
8883
|
-
$14,
|
|
8884
|
-
'draft',
|
|
8885
|
-
$15,
|
|
8886
|
-
$16,
|
|
8887
|
-
$17,
|
|
8888
|
-
$17,
|
|
8889
|
-
NOW(),
|
|
8890
|
-
NOW()
|
|
8891
|
-
)
|
|
8892
|
-
RETURNING id`,
|
|
8893
|
-
generatedContractCode,
|
|
8894
|
-
generatedContractName,
|
|
8895
|
-
selectedTemplate?.contractCategory ?? 'client',
|
|
8896
|
-
selectedTemplate?.contractType ?? 'service_agreement',
|
|
8897
|
-
input.clientName,
|
|
8898
|
-
selectedTemplate?.signatureStatus ?? 'not_started',
|
|
8899
|
-
selectedTemplate?.billingModel ?? input.billingModel,
|
|
8900
|
-
input.managerCollaboratorId,
|
|
8901
|
-
selectedTemplate?.id ?? null,
|
|
8902
|
-
input.projectId,
|
|
8903
|
-
input.startDate ?? new Date().toISOString().slice(0, 10),
|
|
8904
|
-
input.endDate ?? null,
|
|
8905
|
-
input.budgetAmount ?? null,
|
|
8906
|
-
input.monthlyHourCap ?? null,
|
|
8907
|
-
generatedDescription,
|
|
8908
|
-
generatedContentHtml,
|
|
8909
|
-
createdByUserId
|
|
8910
|
-
);
|
|
8911
|
-
|
|
8912
|
-
return (created as { id: number }[])[0]?.id;
|
|
8913
|
-
}
|
|
8914
|
-
|
|
8915
8842
|
private async replaceContractParties(
|
|
8916
8843
|
client: any,
|
|
8917
8844
|
contractId: number,
|
|
@@ -8957,137 +8884,6 @@ export class OperationsService {
|
|
|
8957
8884
|
}
|
|
8958
8885
|
}
|
|
8959
8886
|
|
|
8960
|
-
private async replaceContractSignatures(
|
|
8961
|
-
client: any,
|
|
8962
|
-
contractId: number,
|
|
8963
|
-
signatures?: ContractPayload['signatures']
|
|
8964
|
-
) {
|
|
8965
|
-
await client.$executeRawUnsafe(
|
|
8966
|
-
`UPDATE operations_contract_signature
|
|
8967
|
-
SET deleted_at = NOW(),
|
|
8968
|
-
updated_at = NOW()
|
|
8969
|
-
WHERE contract_id = $1
|
|
8970
|
-
AND deleted_at IS NULL`,
|
|
8971
|
-
contractId
|
|
8972
|
-
);
|
|
8973
|
-
|
|
8974
|
-
for (const signature of signatures ?? []) {
|
|
8975
|
-
await client.$executeRawUnsafe(
|
|
8976
|
-
`INSERT INTO operations_contract_signature (
|
|
8977
|
-
contract_id,
|
|
8978
|
-
signer_name,
|
|
8979
|
-
signer_role,
|
|
8980
|
-
signer_email,
|
|
8981
|
-
signer_status,
|
|
8982
|
-
signed_at,
|
|
8983
|
-
created_at,
|
|
8984
|
-
updated_at
|
|
8985
|
-
) VALUES (
|
|
8986
|
-
$1, $2, $3, $4,
|
|
8987
|
-
$5::operations_contract_signature_signer_status_1e6fbe2519_enum,
|
|
8988
|
-
$6::timestamp, NOW(), NOW()
|
|
8989
|
-
)`,
|
|
8990
|
-
contractId,
|
|
8991
|
-
signature.signerName,
|
|
8992
|
-
signature.signerRole ?? null,
|
|
8993
|
-
signature.signerEmail ?? null,
|
|
8994
|
-
signature.status ?? 'pending',
|
|
8995
|
-
signature.signedAt ?? null
|
|
8996
|
-
);
|
|
8997
|
-
}
|
|
8998
|
-
}
|
|
8999
|
-
|
|
9000
|
-
private async replaceContractFinancialTerms(
|
|
9001
|
-
client: any,
|
|
9002
|
-
contractId: number,
|
|
9003
|
-
financialTerms?: ContractPayload['financialTerms']
|
|
9004
|
-
) {
|
|
9005
|
-
await client.$executeRawUnsafe(
|
|
9006
|
-
`UPDATE operations_contract_financial_term
|
|
9007
|
-
SET deleted_at = NOW(),
|
|
9008
|
-
updated_at = NOW()
|
|
9009
|
-
WHERE contract_id = $1
|
|
9010
|
-
AND deleted_at IS NULL`,
|
|
9011
|
-
contractId
|
|
9012
|
-
);
|
|
9013
|
-
|
|
9014
|
-
for (const term of financialTerms ?? []) {
|
|
9015
|
-
await client.$executeRawUnsafe(
|
|
9016
|
-
`INSERT INTO operations_contract_financial_term (
|
|
9017
|
-
contract_id,
|
|
9018
|
-
term_type,
|
|
9019
|
-
label,
|
|
9020
|
-
amount,
|
|
9021
|
-
recurrence,
|
|
9022
|
-
due_day,
|
|
9023
|
-
notes,
|
|
9024
|
-
created_at,
|
|
9025
|
-
updated_at
|
|
9026
|
-
) VALUES (
|
|
9027
|
-
$1,
|
|
9028
|
-
$2::operations_contract_financial_term_term_type_700635c06a_enum,
|
|
9029
|
-
$3,
|
|
9030
|
-
$4,
|
|
9031
|
-
$5::operations_contract_financial_term_recurrence_ba90bbe3bf_enum,
|
|
9032
|
-
$6,
|
|
9033
|
-
$7,
|
|
9034
|
-
NOW(), NOW()
|
|
9035
|
-
)`,
|
|
9036
|
-
contractId,
|
|
9037
|
-
term.termType ?? 'value',
|
|
9038
|
-
term.label,
|
|
9039
|
-
term.amount,
|
|
9040
|
-
term.recurrence ?? 'one_time',
|
|
9041
|
-
term.dueDay ?? null,
|
|
9042
|
-
term.notes ?? null
|
|
9043
|
-
);
|
|
9044
|
-
}
|
|
9045
|
-
}
|
|
9046
|
-
|
|
9047
|
-
private async replaceContractRevisions(
|
|
9048
|
-
client: any,
|
|
9049
|
-
contractId: number,
|
|
9050
|
-
revisions?: ContractPayload['revisions']
|
|
9051
|
-
) {
|
|
9052
|
-
await client.$executeRawUnsafe(
|
|
9053
|
-
`UPDATE operations_contract_revision
|
|
9054
|
-
SET deleted_at = NOW(),
|
|
9055
|
-
updated_at = NOW()
|
|
9056
|
-
WHERE contract_id = $1
|
|
9057
|
-
AND deleted_at IS NULL`,
|
|
9058
|
-
contractId
|
|
9059
|
-
);
|
|
9060
|
-
|
|
9061
|
-
for (const revision of revisions ?? []) {
|
|
9062
|
-
await client.$executeRawUnsafe(
|
|
9063
|
-
`INSERT INTO operations_contract_revision (
|
|
9064
|
-
contract_id,
|
|
9065
|
-
revision_type,
|
|
9066
|
-
title,
|
|
9067
|
-
effective_date,
|
|
9068
|
-
status,
|
|
9069
|
-
summary,
|
|
9070
|
-
created_at,
|
|
9071
|
-
updated_at
|
|
9072
|
-
) VALUES (
|
|
9073
|
-
$1,
|
|
9074
|
-
$2::operations_contract_revision_revision_type_cf5ba1a538_enum,
|
|
9075
|
-
$3,
|
|
9076
|
-
$4::date,
|
|
9077
|
-
$5::operations_contract_revision_status_f44f35bb66_enum,
|
|
9078
|
-
$6,
|
|
9079
|
-
NOW(), NOW()
|
|
9080
|
-
)`,
|
|
9081
|
-
contractId,
|
|
9082
|
-
revision.revisionType ?? 'revision',
|
|
9083
|
-
revision.title,
|
|
9084
|
-
revision.effectiveDate ?? null,
|
|
9085
|
-
revision.status ?? 'draft',
|
|
9086
|
-
revision.summary ?? null
|
|
9087
|
-
);
|
|
9088
|
-
}
|
|
9089
|
-
}
|
|
9090
|
-
|
|
9091
8887
|
private async replaceContractDocument(
|
|
9092
8888
|
client: any,
|
|
9093
8889
|
contractId: number,
|
|
@@ -9100,7 +8896,7 @@ export class OperationsService {
|
|
|
9100
8896
|
updated_at = NOW()
|
|
9101
8897
|
WHERE contract_id = $1
|
|
9102
8898
|
AND deleted_at IS NULL
|
|
9103
|
-
AND document_type = $2`,
|
|
8899
|
+
AND document_type::text = $2`,
|
|
9104
8900
|
contractId,
|
|
9105
8901
|
documentType
|
|
9106
8902
|
);
|
|
@@ -9120,7 +8916,7 @@ export class OperationsService {
|
|
|
9120
8916
|
created_at,
|
|
9121
8917
|
updated_at
|
|
9122
8918
|
) VALUES (
|
|
9123
|
-
$1, $2, $3, $4, $5, $6, true, $7, $8, $9, NOW(), NOW()
|
|
8919
|
+
$1, $2::operations_contract_document_document_type_15ebaff0c9_enum, $3, $4, $5, $6, true, $7::operations_contract_document_extraction_status_6d94c231f3_enum, $8, $9, NOW(), NOW()
|
|
9124
8920
|
)`,
|
|
9125
8921
|
contractId,
|
|
9126
8922
|
documentType,
|
|
@@ -9308,22 +9104,6 @@ export class OperationsService {
|
|
|
9308
9104
|
)
|
|
9309
9105
|
.join('')
|
|
9310
9106
|
: '<li><strong>No parties registered yet.</strong><span>Complete this contract later if needed.</span></li>';
|
|
9311
|
-
const financialTermsHtml = (contract.financialTerms ?? []).length
|
|
9312
|
-
? contract.financialTerms
|
|
9313
|
-
.map(
|
|
9314
|
-
(term: any) => `
|
|
9315
|
-
<li>
|
|
9316
|
-
<strong>${this.escapeHtml(term.label || 'Term')}</strong>
|
|
9317
|
-
<span>${this.escapeHtml(
|
|
9318
|
-
[term.termType, term.amount, term.recurrence]
|
|
9319
|
-
.filter((item) => item !== null && item !== undefined && String(item).trim())
|
|
9320
|
-
.join(' • ')
|
|
9321
|
-
)}</span>
|
|
9322
|
-
</li>`
|
|
9323
|
-
)
|
|
9324
|
-
.join('')
|
|
9325
|
-
: '<li><strong>No financial terms registered yet.</strong><span>The draft is intentionally lightweight.</span></li>';
|
|
9326
|
-
|
|
9327
9107
|
return `<!DOCTYPE html>
|
|
9328
9108
|
<html lang="en">
|
|
9329
9109
|
<head>
|
|
@@ -9482,11 +9262,6 @@ export class OperationsService {
|
|
|
9482
9262
|
<ul>${partiesHtml}</ul>
|
|
9483
9263
|
</section>
|
|
9484
9264
|
|
|
9485
|
-
<section>
|
|
9486
|
-
<h2>Financial Terms</h2>
|
|
9487
|
-
<ul>${financialTermsHtml}</ul>
|
|
9488
|
-
</section>
|
|
9489
|
-
|
|
9490
9265
|
<section>
|
|
9491
9266
|
<h2>Contract Body</h2>
|
|
9492
9267
|
<div class="content">${contentHtml}</div>
|
|
@@ -9829,7 +9604,6 @@ export class OperationsService {
|
|
|
9829
9604
|
effectiveDate: contract.effectiveDate,
|
|
9830
9605
|
signatureStatus: contract.signatureStatus,
|
|
9831
9606
|
parties: contract.parties,
|
|
9832
|
-
financialTerms: contract.financialTerms,
|
|
9833
9607
|
},
|
|
9834
9608
|
null,
|
|
9835
9609
|
2
|
|
@@ -9929,11 +9703,11 @@ export class OperationsService {
|
|
|
9929
9703
|
warnings.push('Consider confirming the primary party document number.');
|
|
9930
9704
|
}
|
|
9931
9705
|
|
|
9932
|
-
if (
|
|
9933
|
-
warnings.push('Commercial conditions are still generic; define
|
|
9934
|
-
checklist.push('Attention: validate billing
|
|
9706
|
+
if (contract.budgetAmount == null) {
|
|
9707
|
+
warnings.push('Commercial conditions are still generic; define a budget amount before signature.');
|
|
9708
|
+
checklist.push('Attention: validate billing and penalties.');
|
|
9935
9709
|
} else {
|
|
9936
|
-
checklist.push('OK: there is
|
|
9710
|
+
checklist.push('OK: there is a budget reference to review.');
|
|
9937
9711
|
}
|
|
9938
9712
|
|
|
9939
9713
|
if (!contentText) {
|
|
@@ -10145,54 +9919,6 @@ export class OperationsService {
|
|
|
10145
9919
|
isPrimary: this.normalizeExtractionBoolean(party.isPrimary),
|
|
10146
9920
|
}))
|
|
10147
9921
|
.filter((party) => party.displayName),
|
|
10148
|
-
signatures: this.normalizeObjectList(raw.signatures)
|
|
10149
|
-
.map((signature) => ({
|
|
10150
|
-
signerName: this.normalizeExtractionString(signature.signerName),
|
|
10151
|
-
signerRole: this.normalizeExtractionString(signature.signerRole),
|
|
10152
|
-
signerEmail: this.normalizeExtractionString(signature.signerEmail),
|
|
10153
|
-
status: this.normalizeEnumValue(
|
|
10154
|
-
signature.status,
|
|
10155
|
-
SIGNATURE_ITEM_STATUS_VALUES,
|
|
10156
|
-
'pending'
|
|
10157
|
-
),
|
|
10158
|
-
signedAt: this.normalizeExtractionDate(signature.signedAt),
|
|
10159
|
-
}))
|
|
10160
|
-
.filter((signature) => signature.signerName),
|
|
10161
|
-
financialTerms: this.normalizeObjectList(raw.financialTerms)
|
|
10162
|
-
.map((term) => ({
|
|
10163
|
-
label: this.normalizeExtractionString(term.label),
|
|
10164
|
-
termType: this.normalizeEnumValue(
|
|
10165
|
-
term.termType,
|
|
10166
|
-
FINANCIAL_TERM_TYPE_VALUES,
|
|
10167
|
-
'value'
|
|
10168
|
-
),
|
|
10169
|
-
amount: this.normalizeExtractionNumber(term.amount),
|
|
10170
|
-
recurrence: this.normalizeEnumValue(
|
|
10171
|
-
term.recurrence,
|
|
10172
|
-
RECURRENCE_VALUES,
|
|
10173
|
-
'one_time'
|
|
10174
|
-
),
|
|
10175
|
-
dueDay: this.normalizeExtractionNumber(term.dueDay),
|
|
10176
|
-
notes: this.normalizeExtractionString(term.notes),
|
|
10177
|
-
}))
|
|
10178
|
-
.filter((term) => term.label),
|
|
10179
|
-
revisions: this.normalizeObjectList(raw.revisions)
|
|
10180
|
-
.map((revision) => ({
|
|
10181
|
-
title: this.normalizeExtractionString(revision.title),
|
|
10182
|
-
revisionType: this.normalizeEnumValue(
|
|
10183
|
-
revision.revisionType,
|
|
10184
|
-
REVISION_TYPE_VALUES,
|
|
10185
|
-
'revision'
|
|
10186
|
-
),
|
|
10187
|
-
effectiveDate: this.normalizeExtractionDate(revision.effectiveDate),
|
|
10188
|
-
status: this.normalizeEnumValue(
|
|
10189
|
-
revision.status,
|
|
10190
|
-
REVISION_STATUS_VALUES,
|
|
10191
|
-
'draft'
|
|
10192
|
-
),
|
|
10193
|
-
summary: this.normalizeExtractionString(revision.summary),
|
|
10194
|
-
}))
|
|
10195
|
-
.filter((revision) => revision.title),
|
|
10196
9922
|
};
|
|
10197
9923
|
|
|
10198
9924
|
if (!draft.name) missingFields.push('Contract title');
|
|
@@ -10339,6 +10065,22 @@ export class OperationsService {
|
|
|
10339
10065
|
};
|
|
10340
10066
|
}
|
|
10341
10067
|
|
|
10068
|
+
private shouldPaginate(input: {
|
|
10069
|
+
page?: number;
|
|
10070
|
+
pageSize?: number;
|
|
10071
|
+
search?: string;
|
|
10072
|
+
sortField?: string;
|
|
10073
|
+
sortOrder?: string;
|
|
10074
|
+
}) {
|
|
10075
|
+
return [
|
|
10076
|
+
input.page,
|
|
10077
|
+
input.pageSize,
|
|
10078
|
+
input.search,
|
|
10079
|
+
input.sortField,
|
|
10080
|
+
input.sortOrder,
|
|
10081
|
+
].some((value) => value !== undefined && value !== null && value !== '');
|
|
10082
|
+
}
|
|
10083
|
+
|
|
10342
10084
|
private buildPaginationResult<T>(
|
|
10343
10085
|
data: T[],
|
|
10344
10086
|
total: number,
|
|
@@ -10533,4 +10275,13 @@ export class OperationsService {
|
|
|
10533
10275
|
private async execute(sql: string, params: unknown[] = []) {
|
|
10534
10276
|
return this.prisma.$executeRawUnsafe(sql, ...params);
|
|
10535
10277
|
}
|
|
10278
|
+
|
|
10279
|
+
private async getNextCollaboratorTypeSortOrder(client: any = this.prisma) {
|
|
10280
|
+
const row = (await client.$queryRawUnsafe(
|
|
10281
|
+
`SELECT COALESCE(MAX(sort_order), 0) + 1 AS "nextSortOrder"
|
|
10282
|
+
FROM operations_collaborator_type`
|
|
10283
|
+
)) as { nextSortOrder: number }[];
|
|
10284
|
+
|
|
10285
|
+
return Number(row?.[0]?.nextSortOrder ?? 1);
|
|
10286
|
+
}
|
|
10536
10287
|
}
|