@hed-hog/operations 0.0.317 → 0.0.319
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-collaborator-costs.controller.d.ts +144 -0
- package/dist/controllers/operations-collaborator-costs.controller.d.ts.map +1 -0
- package/dist/controllers/operations-collaborator-costs.controller.js +162 -0
- package/dist/controllers/operations-collaborator-costs.controller.js.map +1 -0
- package/dist/controllers/operations-collaborators.controller.d.ts +14 -0
- package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
- package/dist/controllers/operations-collaborators.controller.js +11 -0
- package/dist/controllers/operations-collaborators.controller.js.map +1 -1
- package/dist/controllers/operations-projects.controller.d.ts +31 -0
- package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
- package/dist/controllers/operations-projects.controller.js +23 -0
- package/dist/controllers/operations-projects.controller.js.map +1 -1
- package/dist/controllers/operations-reports.controller.d.ts +199 -0
- package/dist/controllers/operations-reports.controller.d.ts.map +1 -0
- package/dist/controllers/operations-reports.controller.js +53 -0
- package/dist/controllers/operations-reports.controller.js.map +1 -0
- package/dist/controllers/operations-tasks.controller.d.ts +41 -2
- package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
- package/dist/controllers/operations-tasks.controller.js +17 -5
- package/dist/controllers/operations-tasks.controller.js.map +1 -1
- package/dist/dto/create-collaborator-cost.dto.d.ts +16 -0
- package/dist/dto/create-collaborator-cost.dto.d.ts.map +1 -0
- package/dist/dto/create-collaborator-cost.dto.js +88 -0
- package/dist/dto/create-collaborator-cost.dto.js.map +1 -0
- package/dist/dto/create-collaborator.dto.d.ts +0 -1
- package/dist/dto/create-collaborator.dto.d.ts.map +1 -1
- package/dist/dto/create-collaborator.dto.js +0 -6
- package/dist/dto/create-collaborator.dto.js.map +1 -1
- package/dist/dto/create-cost-type.dto.d.ts +13 -0
- package/dist/dto/create-cost-type.dto.d.ts.map +1 -0
- package/dist/dto/create-cost-type.dto.js +87 -0
- package/dist/dto/create-cost-type.dto.js.map +1 -0
- package/dist/dto/list-approvals.dto.d.ts +2 -0
- package/dist/dto/list-approvals.dto.d.ts.map +1 -1
- package/dist/dto/list-approvals.dto.js +10 -0
- package/dist/dto/list-approvals.dto.js.map +1 -1
- package/dist/dto/list-collaborator-costs.dto.d.ts +5 -0
- package/dist/dto/list-collaborator-costs.dto.d.ts.map +1 -0
- package/dist/dto/list-collaborator-costs.dto.js +23 -0
- package/dist/dto/list-collaborator-costs.dto.js.map +1 -0
- package/dist/dto/list-cost-types.dto.d.ts +6 -0
- package/dist/dto/list-cost-types.dto.d.ts.map +1 -0
- package/dist/dto/list-cost-types.dto.js +35 -0
- package/dist/dto/list-cost-types.dto.js.map +1 -0
- package/dist/dto/list-my-projects.dto.d.ts +5 -0
- package/dist/dto/list-my-projects.dto.d.ts.map +1 -0
- package/dist/dto/list-my-projects.dto.js +23 -0
- package/dist/dto/list-my-projects.dto.js.map +1 -0
- package/dist/dto/list-my-tasks.dto.d.ts +6 -0
- package/dist/dto/list-my-tasks.dto.d.ts.map +1 -0
- package/dist/dto/list-my-tasks.dto.js +33 -0
- package/dist/dto/list-my-tasks.dto.js.map +1 -0
- package/dist/dto/list-projects.dto.d.ts +1 -0
- package/dist/dto/list-projects.dto.d.ts.map +1 -1
- package/dist/dto/list-projects.dto.js +7 -0
- package/dist/dto/list-projects.dto.js.map +1 -1
- package/dist/dto/list-reports.dto.d.ts +16 -0
- package/dist/dto/list-reports.dto.d.ts.map +1 -0
- package/dist/dto/list-reports.dto.js +75 -0
- package/dist/dto/list-reports.dto.js.map +1 -0
- package/dist/dto/list-tasks.dto.d.ts +2 -0
- package/dist/dto/list-tasks.dto.d.ts.map +1 -1
- package/dist/dto/list-tasks.dto.js +12 -0
- package/dist/dto/list-tasks.dto.js.map +1 -1
- package/dist/dto/list-timesheets.dto.d.ts +2 -0
- package/dist/dto/list-timesheets.dto.d.ts.map +1 -1
- package/dist/dto/list-timesheets.dto.js +10 -0
- package/dist/dto/list-timesheets.dto.js.map +1 -1
- package/dist/dto/update-collaborator-cost.dto.d.ts +6 -0
- package/dist/dto/update-collaborator-cost.dto.d.ts.map +1 -0
- package/dist/dto/update-collaborator-cost.dto.js +9 -0
- package/dist/dto/update-collaborator-cost.dto.js.map +1 -0
- package/dist/dto/update-task.dto.d.ts +1 -0
- package/dist/dto/update-task.dto.d.ts.map +1 -1
- package/dist/dto/update-task.dto.js +6 -0
- package/dist/dto/update-task.dto.js.map +1 -1
- package/dist/operations.module.d.ts.map +1 -1
- package/dist/operations.module.js +4 -0
- package/dist/operations.module.js.map +1 -1
- package/dist/operations.service.d.ts +457 -3
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +1445 -208
- package/dist/operations.service.js.map +1 -1
- package/dist/operations.service.spec.js +31 -7
- package/dist/operations.service.spec.js.map +1 -1
- package/hedhog/data/menu.yaml +112 -7
- package/hedhog/data/operations_cost_type.yaml +166 -0
- package/hedhog/data/route.yaml +185 -0
- package/hedhog/frontend/app/_components/collaborator-costs-section.tsx.ejs +884 -0
- package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +94 -15
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +219 -94
- package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +21 -32
- package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +178 -89
- package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +1185 -0
- package/hedhog/frontend/app/_components/operations-calendar-view.tsx.ejs +306 -0
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +943 -782
- package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +223 -0
- package/hedhog/frontend/app/_lib/api.ts.ejs +162 -0
- package/hedhog/frontend/app/_lib/types.ts.ejs +229 -3
- package/hedhog/frontend/app/_lib/utils/format.ts.ejs +11 -3
- package/hedhog/frontend/app/approvals/page.tsx.ejs +191 -46
- package/hedhog/frontend/app/collaborators/page.tsx.ejs +133 -25
- package/hedhog/frontend/app/my-projects/[id]/page.tsx.ejs +11 -0
- package/hedhog/frontend/app/my-projects/page.tsx.ejs +440 -0
- package/hedhog/frontend/app/my-tasks/page.tsx.ejs +1304 -0
- package/hedhog/frontend/app/reports/collaborators/page.tsx.ejs +771 -0
- package/hedhog/frontend/app/reports/projects/page.tsx.ejs +809 -0
- package/hedhog/frontend/app/timesheets/page.tsx.ejs +322 -58
- package/hedhog/frontend/messages/en.json +234 -25
- package/hedhog/frontend/messages/pt.json +234 -25
- package/hedhog/table/operations_collaborator.yaml +0 -4
- package/hedhog/table/operations_collaborator_compensation_history.yaml +28 -0
- package/hedhog/table/operations_collaborator_cost.yaml +56 -0
- package/hedhog/table/operations_cost_type.yaml +38 -0
- package/package.json +6 -6
- package/src/controllers/operations-collaborator-costs.controller.ts +147 -0
- package/src/controllers/operations-collaborators.controller.ts +19 -8
- package/src/controllers/operations-projects.controller.ts +19 -8
- package/src/controllers/operations-reports.controller.ts +32 -0
- package/src/controllers/operations-tasks.controller.ts +32 -12
- package/src/dto/create-collaborator-cost.dto.ts +78 -0
- package/src/dto/create-collaborator.dto.ts +9 -14
- package/src/dto/create-cost-type.dto.ts +62 -0
- package/src/dto/list-approvals.dto.ts +8 -0
- package/src/dto/list-collaborator-costs.dto.ts +8 -0
- package/src/dto/list-cost-types.dto.ts +19 -0
- package/src/dto/list-my-projects.dto.ts +8 -0
- package/src/dto/list-my-tasks.dto.ts +17 -0
- package/src/dto/list-projects.dto.ts +7 -1
- package/src/dto/list-reports.dto.ts +51 -0
- package/src/dto/list-tasks.dto.ts +11 -1
- package/src/dto/list-timesheets.dto.ts +8 -0
- package/src/dto/update-collaborator-cost.dto.ts +4 -0
- package/src/dto/update-task.dto.ts +6 -0
- package/src/operations.module.ts +7 -3
- package/src/operations.service.spec.ts +45 -7
- package/src/operations.service.ts +1992 -225
|
@@ -528,7 +528,6 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
528
528
|
OR COALESCE(person_record.name, '') ILIKE ${searchPlaceholder}
|
|
529
529
|
OR COALESCE(c.code, '') ILIKE ${searchPlaceholder}
|
|
530
530
|
OR COALESCE(department_record.name, '') ILIKE ${searchPlaceholder}
|
|
531
|
-
OR COALESCE(c.department, '') ILIKE ${searchPlaceholder}
|
|
532
531
|
OR COALESCE(job_title_record.name, '') ILIKE ${searchPlaceholder}
|
|
533
532
|
OR COALESCE(c.title, '') ILIKE ${searchPlaceholder}
|
|
534
533
|
OR COALESCE(s.display_name, '') ILIKE ${searchPlaceholder}
|
|
@@ -546,7 +545,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
546
545
|
person_record.name AS "personName",
|
|
547
546
|
person_record.avatar_id AS "personAvatarId",
|
|
548
547
|
c.department_id AS "departmentId",
|
|
549
|
-
|
|
548
|
+
department_record.name AS "department",
|
|
550
549
|
c.job_title_id AS "jobTitleId",
|
|
551
550
|
COALESCE(NULLIF(job_title_record.name, ''), NULLIF(c.title, '')) AS "title",
|
|
552
551
|
c.level_label AS "levelLabel",
|
|
@@ -631,7 +630,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
631
630
|
return this.buildPaginationResult(rows, Number((_b = totalRow === null || totalRow === void 0 ? void 0 : totalRow.total) !== null && _b !== void 0 ? _b : 0), pagination.page, pagination.pageSize);
|
|
632
631
|
}
|
|
633
632
|
async getCollaboratorStats(userId, filters = {}) {
|
|
634
|
-
var _a, _b, _c, _d, _e;
|
|
633
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
635
634
|
const actor = await this.getActorContext(userId);
|
|
636
635
|
this.ensureCollaborator(actor);
|
|
637
636
|
const filter = this.buildIdFilter(actor.visibleCollaboratorIds, 'c.id', actor.isDirector);
|
|
@@ -664,45 +663,101 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
664
663
|
OR COALESCE(person_record.name, '') ILIKE ${searchPlaceholder}
|
|
665
664
|
OR COALESCE(c.code, '') ILIKE ${searchPlaceholder}
|
|
666
665
|
OR COALESCE(department_record.name, '') ILIKE ${searchPlaceholder}
|
|
667
|
-
OR COALESCE(c.department, '') ILIKE ${searchPlaceholder}
|
|
668
666
|
OR COALESCE(job_title_record.name, '') ILIKE ${searchPlaceholder}
|
|
669
667
|
OR COALESCE(c.title, '') ILIKE ${searchPlaceholder}
|
|
670
668
|
OR COALESCE(s.display_name, '') ILIKE ${searchPlaceholder}
|
|
671
669
|
)`);
|
|
672
670
|
}
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
671
|
+
const baseWhere = where.slice(0, 2); // access control only: deleted_at + visibility
|
|
672
|
+
const result = await this.querySingle(`WITH filtered AS (
|
|
673
|
+
SELECT DISTINCT c.id
|
|
674
|
+
FROM operations_collaborator c
|
|
675
|
+
LEFT JOIN person person_record
|
|
676
|
+
ON person_record.id = c.person_id
|
|
677
|
+
LEFT JOIN operations_collaborator_type collaborator_type
|
|
678
|
+
ON collaborator_type.id = c.collaborator_type_id
|
|
679
|
+
AND collaborator_type.deleted_at IS NULL
|
|
680
|
+
LEFT JOIN operations_department department_record
|
|
681
|
+
ON department_record.id = c.department_id
|
|
682
|
+
AND department_record.deleted_at IS NULL
|
|
683
|
+
LEFT JOIN operations_job_title job_title_record
|
|
684
|
+
ON job_title_record.id = c.job_title_id
|
|
685
|
+
AND job_title_record.deleted_at IS NULL
|
|
686
|
+
LEFT JOIN operations_collaborator s
|
|
687
|
+
ON s.id = c.supervisor_collaborator_id
|
|
688
|
+
WHERE ${where.join(' AND ')}
|
|
689
|
+
),
|
|
690
|
+
global_filtered AS (
|
|
691
|
+
SELECT DISTINCT c.id,
|
|
692
|
+
COALESCE(hc.budget_amount, 0) AS salary
|
|
693
|
+
FROM operations_collaborator c
|
|
694
|
+
LEFT JOIN LATERAL (
|
|
695
|
+
SELECT oc.budget_amount
|
|
696
|
+
FROM operations_contract oc
|
|
697
|
+
WHERE oc.related_collaborator_id = c.id
|
|
698
|
+
AND oc.deleted_at IS NULL
|
|
699
|
+
ORDER BY CASE WHEN oc.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
|
|
700
|
+
oc.created_at DESC
|
|
701
|
+
LIMIT 1
|
|
702
|
+
) hc ON TRUE
|
|
703
|
+
WHERE ${baseWhere.join(' AND ')}
|
|
704
|
+
),
|
|
705
|
+
global_cost_totals AS (
|
|
706
|
+
SELECT cc.collaborator_id,
|
|
707
|
+
COALESCE(SUM(cc.amount), 0) AS total_cost
|
|
708
|
+
FROM operations_collaborator_cost cc
|
|
709
|
+
WHERE cc.collaborator_id IN (SELECT id FROM global_filtered)
|
|
710
|
+
GROUP BY cc.collaborator_id
|
|
711
|
+
),
|
|
712
|
+
global_count AS (
|
|
713
|
+
SELECT COUNT(*)::int AS cnt FROM global_filtered
|
|
714
|
+
),
|
|
715
|
+
agg AS (
|
|
716
|
+
SELECT
|
|
717
|
+
(SELECT COUNT(*)::int FROM filtered) AS total,
|
|
718
|
+
COUNT(DISTINCT c.id) FILTER (WHERE c.status = 'active')::int AS active,
|
|
719
|
+
COUNT(DISTINCT c.id) FILTER (WHERE c.status = 'on_leave')::int AS "onLeave",
|
|
720
|
+
COUNT(DISTINCT c.id) FILTER (WHERE hc.id IS NOT NULL)::int AS "withContracts"
|
|
721
|
+
FROM operations_collaborator c
|
|
722
|
+
LEFT JOIN LATERAL (
|
|
723
|
+
SELECT oc.id
|
|
724
|
+
FROM operations_contract oc
|
|
725
|
+
WHERE oc.related_collaborator_id = c.id
|
|
726
|
+
AND oc.deleted_at IS NULL
|
|
727
|
+
ORDER BY CASE WHEN oc.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
|
|
728
|
+
oc.created_at DESC
|
|
729
|
+
LIMIT 1
|
|
730
|
+
) hc ON TRUE
|
|
731
|
+
WHERE c.id IN (SELECT id FROM filtered)
|
|
732
|
+
)
|
|
733
|
+
SELECT
|
|
734
|
+
agg.total,
|
|
735
|
+
agg.active,
|
|
736
|
+
agg."onLeave",
|
|
737
|
+
agg."withContracts",
|
|
738
|
+
COALESCE((SELECT SUM(f.salary) FROM global_filtered f), 0)::text AS "totalSalary",
|
|
739
|
+
COALESCE((SELECT SUM(ct.total_cost) FROM global_cost_totals ct), 0)::text AS "totalCosts",
|
|
740
|
+
(
|
|
741
|
+
COALESCE((SELECT SUM(f.salary) FROM global_filtered f), 0) +
|
|
742
|
+
COALESCE((SELECT SUM(ct.total_cost) FROM global_cost_totals ct), 0)
|
|
743
|
+
)::text AS "avgSalaryPlusCosts",
|
|
744
|
+
CASE WHEN (SELECT cnt FROM global_count) > 0
|
|
745
|
+
THEN (
|
|
746
|
+
COALESCE((SELECT SUM(f.salary) FROM global_filtered f), 0) +
|
|
747
|
+
COALESCE((SELECT SUM(ct.total_cost) FROM global_cost_totals ct), 0)
|
|
748
|
+
) / (SELECT cnt FROM global_count)
|
|
749
|
+
ELSE 0
|
|
750
|
+
END::text AS "avgSalaryPlusCostsPerCollaborator"
|
|
751
|
+
FROM agg`, params);
|
|
701
752
|
return {
|
|
702
753
|
total: Number((_b = result === null || result === void 0 ? void 0 : result.total) !== null && _b !== void 0 ? _b : 0),
|
|
703
754
|
active: Number((_c = result === null || result === void 0 ? void 0 : result.active) !== null && _c !== void 0 ? _c : 0),
|
|
704
755
|
onLeave: Number((_d = result === null || result === void 0 ? void 0 : result.onLeave) !== null && _d !== void 0 ? _d : 0),
|
|
705
756
|
withContracts: Number((_e = result === null || result === void 0 ? void 0 : result.withContracts) !== null && _e !== void 0 ? _e : 0),
|
|
757
|
+
totalSalary: Number((_f = result === null || result === void 0 ? void 0 : result.totalSalary) !== null && _f !== void 0 ? _f : 0),
|
|
758
|
+
totalCosts: Number((_g = result === null || result === void 0 ? void 0 : result.totalCosts) !== null && _g !== void 0 ? _g : 0),
|
|
759
|
+
avgSalaryPlusCosts: Number((_h = result === null || result === void 0 ? void 0 : result.avgSalaryPlusCosts) !== null && _h !== void 0 ? _h : 0),
|
|
760
|
+
avgSalaryPlusCostsPerCollaborator: Number((_j = result === null || result === void 0 ? void 0 : result.avgSalaryPlusCostsPerCollaborator) !== null && _j !== void 0 ? _j : 0),
|
|
706
761
|
};
|
|
707
762
|
}
|
|
708
763
|
async getMyCollaborator(userId) {
|
|
@@ -749,7 +804,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
749
804
|
person_record.name AS "personName",
|
|
750
805
|
person_record.avatar_id AS "personAvatarId",
|
|
751
806
|
c.department_id AS "departmentId",
|
|
752
|
-
|
|
807
|
+
department_record.name AS "department",
|
|
753
808
|
c.job_title_id AS "jobTitleId",
|
|
754
809
|
COALESCE(NULLIF(job_title_record.name, ''), NULLIF(c.title, '')) AS "title",
|
|
755
810
|
c.status,
|
|
@@ -942,12 +997,11 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
942
997
|
throw new common_1.BadRequestException('Field "personId" is required.');
|
|
943
998
|
}
|
|
944
999
|
const collaboratorId = await this.prisma.$transaction(async (tx) => {
|
|
945
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2
|
|
1000
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2;
|
|
946
1001
|
const normalizedCode = String((_a = data.code) !== null && _a !== void 0 ? _a : '').trim() ||
|
|
947
1002
|
(await this.generateCollaboratorCode(tx));
|
|
948
1003
|
const resolvedDepartment = await this.resolveDepartmentReference(tx, {
|
|
949
1004
|
departmentId: (_b = data.departmentId) !== null && _b !== void 0 ? _b : null,
|
|
950
|
-
departmentName: data.department,
|
|
951
1005
|
});
|
|
952
1006
|
const resolvedJobTitle = await this.resolveJobTitleReference(tx, {
|
|
953
1007
|
jobTitleId: (_c = data.jobTitleId) !== null && _c !== void 0 ? _c : null,
|
|
@@ -964,7 +1018,6 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
964
1018
|
code,
|
|
965
1019
|
collaborator_type_id,
|
|
966
1020
|
display_name,
|
|
967
|
-
department,
|
|
968
1021
|
department_id,
|
|
969
1022
|
job_title_id,
|
|
970
1023
|
title,
|
|
@@ -978,12 +1031,12 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
978
1031
|
updated_at
|
|
979
1032
|
) VALUES (
|
|
980
1033
|
$1, $2, $3, $4, $5,
|
|
981
|
-
$6, $7, $8, $9, $10, $11,
|
|
982
|
-
$
|
|
983
|
-
$
|
|
1034
|
+
$6, $7, $8, $9, $10, $11,
|
|
1035
|
+
$12::operations_collaborator_status_ef779877d4_enum,
|
|
1036
|
+
$13::date, $14::date, $15, NOW(), NOW()
|
|
984
1037
|
)
|
|
985
|
-
RETURNING id`, (_g = data.userId) !== null && _g !== void 0 ? _g : null, (_h = resolvedPerson === null || resolvedPerson === void 0 ? void 0 : resolvedPerson.id) !== null && _h !== void 0 ? _h : null, (_j = data.supervisorCollaboratorId) !== null && _j !== void 0 ? _j : null, normalizedCode, (_k = resolvedCollaboratorType === null || resolvedCollaboratorType === void 0 ? void 0 : resolvedCollaboratorType.id) !== null && _k !== void 0 ? _k : null, resolvedDisplayName, (_l = resolvedDepartment === null || resolvedDepartment === void 0 ? void 0 : resolvedDepartment.
|
|
986
|
-
const createdCollaboratorId = (
|
|
1038
|
+
RETURNING id`, (_g = data.userId) !== null && _g !== void 0 ? _g : null, (_h = resolvedPerson === null || resolvedPerson === void 0 ? void 0 : resolvedPerson.id) !== null && _h !== void 0 ? _h : null, (_j = data.supervisorCollaboratorId) !== null && _j !== void 0 ? _j : null, normalizedCode, (_k = resolvedCollaboratorType === null || resolvedCollaboratorType === void 0 ? void 0 : resolvedCollaboratorType.id) !== null && _k !== void 0 ? _k : null, resolvedDisplayName, (_l = resolvedDepartment === null || resolvedDepartment === void 0 ? void 0 : resolvedDepartment.id) !== null && _l !== void 0 ? _l : null, (_m = resolvedJobTitle === null || resolvedJobTitle === void 0 ? void 0 : resolvedJobTitle.id) !== null && _m !== void 0 ? _m : null, (_o = resolvedJobTitle === null || resolvedJobTitle === void 0 ? void 0 : resolvedJobTitle.name) !== null && _o !== void 0 ? _o : this.normalizeOptionalText(data.title), (_p = data.levelLabel) !== null && _p !== void 0 ? _p : null, (_q = data.weeklyCapacityHours) !== null && _q !== void 0 ? _q : null, normalizedStatus !== null && normalizedStatus !== void 0 ? normalizedStatus : 'active', (_r = data.joinedAt) !== null && _r !== void 0 ? _r : null, (_s = data.leftAt) !== null && _s !== void 0 ? _s : null, (_t = data.notes) !== null && _t !== void 0 ? _t : null));
|
|
1039
|
+
const createdCollaboratorId = (_u = created[0]) === null || _u === void 0 ? void 0 : _u.id;
|
|
987
1040
|
await this.replaceCollaboratorScheduleDays(tx, createdCollaboratorId, data.weeklySchedule);
|
|
988
1041
|
await this.replaceCollaboratorEquityParticipation(tx, createdCollaboratorId, data.equityParticipation);
|
|
989
1042
|
if (data.autoGenerateContractDraft !== false) {
|
|
@@ -991,14 +1044,17 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
991
1044
|
collaboratorId: createdCollaboratorId,
|
|
992
1045
|
collaboratorCode: normalizedCode,
|
|
993
1046
|
displayName: resolvedDisplayName,
|
|
994
|
-
collaboratorType: ((
|
|
995
|
-
supervisorCollaboratorId: (
|
|
996
|
-
startDate: (
|
|
997
|
-
weeklyCapacityHours: (
|
|
998
|
-
compensationAmount: (
|
|
999
|
-
description: (
|
|
1047
|
+
collaboratorType: ((_w = (_v = resolvedCollaboratorType === null || resolvedCollaboratorType === void 0 ? void 0 : resolvedCollaboratorType.slug) !== null && _v !== void 0 ? _v : data.collaboratorType) !== null && _w !== void 0 ? _w : 'other'),
|
|
1048
|
+
supervisorCollaboratorId: (_x = data.supervisorCollaboratorId) !== null && _x !== void 0 ? _x : null,
|
|
1049
|
+
startDate: (_y = data.joinedAt) !== null && _y !== void 0 ? _y : null,
|
|
1050
|
+
weeklyCapacityHours: (_z = data.weeklyCapacityHours) !== null && _z !== void 0 ? _z : null,
|
|
1051
|
+
compensationAmount: (_0 = data.compensationAmount) !== null && _0 !== void 0 ? _0 : null,
|
|
1052
|
+
description: (_2 = (_1 = data.contractDescription) !== null && _1 !== void 0 ? _1 : data.notes) !== null && _2 !== void 0 ? _2 : null,
|
|
1000
1053
|
});
|
|
1001
1054
|
}
|
|
1055
|
+
if (data.compensationAmount != null) {
|
|
1056
|
+
await this.insertCollaboratorCompensationHistory(tx, createdCollaboratorId, Number(data.compensationAmount), actor.userId, null);
|
|
1057
|
+
}
|
|
1002
1058
|
return createdCollaboratorId;
|
|
1003
1059
|
});
|
|
1004
1060
|
return this.getCollaboratorByIdForUser(userId, collaboratorId);
|
|
@@ -1029,7 +1085,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
1029
1085
|
this.pushUpdate(updates, params, 'left_at', data.leftAt, 'date');
|
|
1030
1086
|
this.pushUpdate(updates, params, 'notes', data.notes);
|
|
1031
1087
|
await this.prisma.$transaction(async (tx) => {
|
|
1032
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j
|
|
1088
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
1033
1089
|
if (data.collaboratorType !== undefined ||
|
|
1034
1090
|
data.collaboratorTypeId !== undefined ||
|
|
1035
1091
|
data.collaboratorTypeSlug !== undefined) {
|
|
@@ -1039,21 +1095,19 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
1039
1095
|
});
|
|
1040
1096
|
this.pushUpdate(updates, params, 'collaborator_type_id', (_d = resolvedCollaboratorType === null || resolvedCollaboratorType === void 0 ? void 0 : resolvedCollaboratorType.id) !== null && _d !== void 0 ? _d : null);
|
|
1041
1097
|
}
|
|
1042
|
-
if (data.
|
|
1098
|
+
if (data.departmentId !== undefined) {
|
|
1043
1099
|
const resolvedDepartment = await this.resolveDepartmentReference(tx, {
|
|
1044
1100
|
departmentId: (_e = data.departmentId) !== null && _e !== void 0 ? _e : null,
|
|
1045
|
-
departmentName: data.department,
|
|
1046
1101
|
});
|
|
1047
|
-
this.pushUpdate(updates, params, '
|
|
1048
|
-
this.pushUpdate(updates, params, 'department_id', (_g = resolvedDepartment === null || resolvedDepartment === void 0 ? void 0 : resolvedDepartment.id) !== null && _g !== void 0 ? _g : null);
|
|
1102
|
+
this.pushUpdate(updates, params, 'department_id', (_f = resolvedDepartment === null || resolvedDepartment === void 0 ? void 0 : resolvedDepartment.id) !== null && _f !== void 0 ? _f : null);
|
|
1049
1103
|
}
|
|
1050
1104
|
if (data.title !== undefined || data.jobTitleId !== undefined) {
|
|
1051
1105
|
const resolvedJobTitle = await this.resolveJobTitleReference(tx, {
|
|
1052
|
-
jobTitleId: (
|
|
1106
|
+
jobTitleId: (_g = data.jobTitleId) !== null && _g !== void 0 ? _g : null,
|
|
1053
1107
|
jobTitleName: data.title,
|
|
1054
1108
|
});
|
|
1055
|
-
this.pushUpdate(updates, params, 'job_title_id', (
|
|
1056
|
-
this.pushUpdate(updates, params, 'title', (
|
|
1109
|
+
this.pushUpdate(updates, params, 'job_title_id', (_h = resolvedJobTitle === null || resolvedJobTitle === void 0 ? void 0 : resolvedJobTitle.id) !== null && _h !== void 0 ? _h : null);
|
|
1110
|
+
this.pushUpdate(updates, params, 'title', (_j = resolvedJobTitle === null || resolvedJobTitle === void 0 ? void 0 : resolvedJobTitle.name) !== null && _j !== void 0 ? _j : this.normalizeOptionalText(data.title));
|
|
1057
1111
|
}
|
|
1058
1112
|
if (updates.length) {
|
|
1059
1113
|
params.push(collaboratorId);
|
|
@@ -1081,10 +1135,31 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
1081
1135
|
data.personId !== undefined ||
|
|
1082
1136
|
data.displayName !== undefined) {
|
|
1083
1137
|
await this.syncHiringContractDraft(tx, actor.userId, collaboratorId, data);
|
|
1138
|
+
if (data.compensationAmount !== undefined &&
|
|
1139
|
+
data.compensationAmount !== null) {
|
|
1140
|
+
await this.insertCollaboratorCompensationHistory(tx, collaboratorId, Number(data.compensationAmount), actor.userId, null);
|
|
1141
|
+
}
|
|
1084
1142
|
}
|
|
1085
1143
|
});
|
|
1086
1144
|
return this.getCollaboratorByIdForUser(userId, collaboratorId);
|
|
1087
1145
|
}
|
|
1146
|
+
async getCollaboratorCompensationHistory(userId, collaboratorId) {
|
|
1147
|
+
const actor = await this.getActorContext(userId);
|
|
1148
|
+
this.ensureDirector(actor);
|
|
1149
|
+
await this.getCollaboratorById(collaboratorId);
|
|
1150
|
+
return this.queryRows(`SELECT h.id,
|
|
1151
|
+
h.collaborator_id AS "collaboratorId",
|
|
1152
|
+
h.amount::text AS amount,
|
|
1153
|
+
h.effective_date::text AS "effectiveDate",
|
|
1154
|
+
h.actor_user_id AS "actorUserId",
|
|
1155
|
+
u.name AS "actorName",
|
|
1156
|
+
h.notes,
|
|
1157
|
+
h.created_at AS "createdAt"
|
|
1158
|
+
FROM operations_collaborator_compensation_history h
|
|
1159
|
+
LEFT JOIN "user" u ON u.id = h.actor_user_id
|
|
1160
|
+
WHERE h.collaborator_id = $1
|
|
1161
|
+
ORDER BY h.created_at DESC`, [collaboratorId]);
|
|
1162
|
+
}
|
|
1088
1163
|
async listDepartments(userId, filters = {}) {
|
|
1089
1164
|
var _a, _b;
|
|
1090
1165
|
const actor = await this.getActorContext(userId);
|
|
@@ -1125,13 +1200,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
1125
1200
|
FROM operations_department d
|
|
1126
1201
|
LEFT JOIN operations_collaborator c
|
|
1127
1202
|
ON c.deleted_at IS NULL
|
|
1128
|
-
AND
|
|
1129
|
-
c.department_id = d.id
|
|
1130
|
-
OR (
|
|
1131
|
-
c.department_id IS NULL
|
|
1132
|
-
AND LOWER(COALESCE(c.department, '')) = LOWER(d.name)
|
|
1133
|
-
)
|
|
1134
|
-
)
|
|
1203
|
+
AND c.department_id = d.id
|
|
1135
1204
|
${whereClause}
|
|
1136
1205
|
GROUP BY d.id`;
|
|
1137
1206
|
if (!pagination) {
|
|
@@ -1298,7 +1367,14 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
1298
1367
|
async listProjects(userId, filters = {}) {
|
|
1299
1368
|
var _a, _b;
|
|
1300
1369
|
const actor = await this.getActorContext(userId);
|
|
1301
|
-
const
|
|
1370
|
+
const myOnly = filters.myOnly === true;
|
|
1371
|
+
// When myOnly=true: restrict to projects the current collaborator is assigned to.
|
|
1372
|
+
// Otherwise (general list): show all projects in the system regardless of role scope.
|
|
1373
|
+
const filter = myOnly
|
|
1374
|
+
? this.buildIdFilter(actor.collaboratorId
|
|
1375
|
+
? await this.getAssignedProjectIds([actor.collaboratorId])
|
|
1376
|
+
: [], 'p.id', false)
|
|
1377
|
+
: this.buildIdFilter([], 'p.id', true);
|
|
1302
1378
|
const assignmentParams = [];
|
|
1303
1379
|
const ownAssignmentSelect = actor.collaboratorId
|
|
1304
1380
|
? `MAX(CASE WHEN pa.collaborator_id = ${this.param(assignmentParams, actor.collaboratorId)} THEN pa.id END)::int AS "myAssignmentId",
|
|
@@ -1313,21 +1389,36 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
1313
1389
|
})
|
|
1314
1390
|
: null;
|
|
1315
1391
|
const params = [...assignmentParams, ...filter.params];
|
|
1316
|
-
const
|
|
1392
|
+
const totalParams = [...filter.params];
|
|
1393
|
+
const where = [
|
|
1394
|
+
'p.deleted_at IS NULL',
|
|
1395
|
+
this.shiftSqlPlaceholders(filter.clause, assignmentParams.length),
|
|
1396
|
+
];
|
|
1397
|
+
const totalWhere = ['p.deleted_at IS NULL', filter.clause];
|
|
1317
1398
|
if (filters.status && filters.status !== 'all') {
|
|
1318
1399
|
where.push(`p.status::text = ${this.param(params, filters.status)}`);
|
|
1400
|
+
totalWhere.push(`p.status::text = ${this.param(totalParams, filters.status)}`);
|
|
1319
1401
|
}
|
|
1320
1402
|
if (pagination === null || pagination === void 0 ? void 0 : pagination.search) {
|
|
1321
1403
|
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
1404
|
+
const totalSearchPlaceholder = this.param(totalParams, `%${pagination.search}%`);
|
|
1322
1405
|
where.push(`(
|
|
1323
1406
|
COALESCE(p.name, '') ILIKE ${searchPlaceholder}
|
|
1324
1407
|
OR COALESCE(p.code, '') ILIKE ${searchPlaceholder}
|
|
1325
1408
|
OR COALESCE(p.client_name, '') ILIKE ${searchPlaceholder}
|
|
1326
1409
|
OR COALESCE(c.name, '') ILIKE ${searchPlaceholder}
|
|
1327
1410
|
OR COALESCE(m.display_name, '') ILIKE ${searchPlaceholder}
|
|
1411
|
+
)`);
|
|
1412
|
+
totalWhere.push(`(
|
|
1413
|
+
COALESCE(p.name, '') ILIKE ${totalSearchPlaceholder}
|
|
1414
|
+
OR COALESCE(p.code, '') ILIKE ${totalSearchPlaceholder}
|
|
1415
|
+
OR COALESCE(p.client_name, '') ILIKE ${totalSearchPlaceholder}
|
|
1416
|
+
OR COALESCE(c.name, '') ILIKE ${totalSearchPlaceholder}
|
|
1417
|
+
OR COALESCE(m.display_name, '') ILIKE ${totalSearchPlaceholder}
|
|
1328
1418
|
)`);
|
|
1329
1419
|
}
|
|
1330
1420
|
const whereClause = where.join(' AND ');
|
|
1421
|
+
const totalWhereClause = totalWhere.join(' AND ');
|
|
1331
1422
|
const baseQuery = `SELECT p.id,
|
|
1332
1423
|
p.contract_id AS "contractId",
|
|
1333
1424
|
p.manager_collaborator_id AS "managerCollaboratorId",
|
|
@@ -1362,7 +1453,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
1362
1453
|
FROM operations_project p
|
|
1363
1454
|
LEFT JOIN operations_contract c ON c.id = p.contract_id
|
|
1364
1455
|
LEFT JOIN operations_collaborator m ON m.id = p.manager_collaborator_id
|
|
1365
|
-
WHERE ${
|
|
1456
|
+
WHERE ${totalWhereClause}`, totalParams);
|
|
1366
1457
|
const sortColumn = (_a = {
|
|
1367
1458
|
name: 'p.name',
|
|
1368
1459
|
code: 'p.code',
|
|
@@ -1445,15 +1536,23 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
1445
1536
|
var _a, _b;
|
|
1446
1537
|
const actor = await this.getActorContext(userId);
|
|
1447
1538
|
this.ensureCollaborator(actor);
|
|
1539
|
+
const myOnly = paginationParams.myOnly === true;
|
|
1540
|
+
const archivedOnly = paginationParams.archived === true;
|
|
1448
1541
|
const pagination = this.normalizePaginationParams(paginationParams, {
|
|
1449
1542
|
defaultSortField: 'name',
|
|
1450
1543
|
defaultSortOrder: 'asc',
|
|
1451
1544
|
allowedSortFields: ['name', 'projectName', 'status', 'createdAt'],
|
|
1452
1545
|
});
|
|
1453
|
-
|
|
1546
|
+
// When myOnly=true: restrict to tasks linked to the current collaborator's assignment.
|
|
1547
|
+
// Otherwise (general list): show all tasks in the system regardless of role scope.
|
|
1548
|
+
const projectFilter = myOnly
|
|
1549
|
+
? this.buildIdFilter(actor.collaboratorId
|
|
1550
|
+
? await this.getAssignedProjectIds([actor.collaboratorId])
|
|
1551
|
+
: [], 'COALESCE(t.project_id, pa.project_id)', false)
|
|
1552
|
+
: this.buildIdFilter([], 'COALESCE(t.project_id, pa.project_id)', true);
|
|
1454
1553
|
const params = [...projectFilter.params];
|
|
1455
1554
|
const filters = [
|
|
1456
|
-
't.deleted_at IS NULL',
|
|
1555
|
+
archivedOnly ? 't.deleted_at IS NOT NULL' : 't.deleted_at IS NULL',
|
|
1457
1556
|
'p.deleted_at IS NULL',
|
|
1458
1557
|
projectFilter.clause,
|
|
1459
1558
|
`(
|
|
@@ -1477,6 +1576,9 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
1477
1576
|
if (paginationParams.projectAssignmentId) {
|
|
1478
1577
|
filters.push(`pa.id = ${this.param(params, paginationParams.projectAssignmentId)}`);
|
|
1479
1578
|
}
|
|
1579
|
+
else if (myOnly && actor.collaboratorId) {
|
|
1580
|
+
filters.push(`pa.collaborator_id = ${this.param(params, actor.collaboratorId)}`);
|
|
1581
|
+
}
|
|
1480
1582
|
if (paginationParams.projectId) {
|
|
1481
1583
|
filters.push(`COALESCE(t.project_id, pa.project_id) = ${this.param(params, paginationParams.projectId)}`);
|
|
1482
1584
|
}
|
|
@@ -1504,14 +1606,29 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
1504
1606
|
t.name,
|
|
1505
1607
|
t.description,
|
|
1506
1608
|
t.status,
|
|
1609
|
+
t.priority,
|
|
1507
1610
|
COALESCE(t.project_id, pa.project_id) AS "projectId",
|
|
1508
1611
|
pa.id AS "projectAssignmentId",
|
|
1509
1612
|
p.name AS "projectName",
|
|
1510
1613
|
p.code AS "projectCode",
|
|
1511
|
-
t.
|
|
1614
|
+
t.due_date AS "dueDate",
|
|
1615
|
+
t.estimate_hours AS "estimateHours",
|
|
1616
|
+
t.tags,
|
|
1617
|
+
ac.display_name AS "assigneeName",
|
|
1618
|
+
au.photo_id AS "assigneeUserPhotoId",
|
|
1619
|
+
ap.avatar_id AS "assigneePersonAvatarId",
|
|
1620
|
+
t.created_at AS "createdAt",
|
|
1621
|
+
t.deleted_at AS "deletedAt"
|
|
1512
1622
|
FROM operations_task t
|
|
1513
1623
|
LEFT JOIN operations_project_assignment pa
|
|
1514
1624
|
ON pa.id = t.project_assignment_id
|
|
1625
|
+
LEFT JOIN operations_collaborator ac
|
|
1626
|
+
ON ac.id = t.assignee_collaborator_id
|
|
1627
|
+
AND ac.deleted_at IS NULL
|
|
1628
|
+
LEFT JOIN "user" au
|
|
1629
|
+
ON au.id = ac.user_id
|
|
1630
|
+
LEFT JOIN person ap
|
|
1631
|
+
ON ap.id = ac.person_id
|
|
1515
1632
|
JOIN operations_project p
|
|
1516
1633
|
ON p.id = COALESCE(t.project_id, pa.project_id)
|
|
1517
1634
|
WHERE ${whereClause}
|
|
@@ -1521,7 +1638,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
1521
1638
|
return this.buildPaginationResult(rows.map((row) => (Object.assign(Object.assign({}, row), { label: [row.name, row.projectName].filter(Boolean).join(' • ') }))), Number((_b = totalRow === null || totalRow === void 0 ? void 0 : totalRow.total) !== null && _b !== void 0 ? _b : 0), pagination.page, pagination.pageSize);
|
|
1522
1639
|
}
|
|
1523
1640
|
async createTask(userId, data) {
|
|
1524
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
1641
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
1525
1642
|
const actor = await this.getActorContext(userId);
|
|
1526
1643
|
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
1527
1644
|
throw new common_1.ForbiddenException('Operations collaborator access is required.');
|
|
@@ -1581,17 +1698,17 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
1581
1698
|
RETURNING id`, [
|
|
1582
1699
|
projectId,
|
|
1583
1700
|
assignmentId,
|
|
1584
|
-
(_f = data.assigneeCollaboratorId) !== null && _f !== void 0 ? _f : null,
|
|
1701
|
+
(_g = (_f = data.assigneeCollaboratorId) !== null && _f !== void 0 ? _f : actor.collaboratorId) !== null && _g !== void 0 ? _g : null,
|
|
1585
1702
|
name,
|
|
1586
1703
|
this.normalizeOptionalText(data.description),
|
|
1587
|
-
(
|
|
1588
|
-
(
|
|
1589
|
-
(
|
|
1590
|
-
(
|
|
1591
|
-
(
|
|
1592
|
-
(
|
|
1704
|
+
(_h = data.priority) !== null && _h !== void 0 ? _h : 'medium',
|
|
1705
|
+
(_j = data.status) !== null && _j !== void 0 ? _j : 'todo',
|
|
1706
|
+
(_k = data.dueDate) !== null && _k !== void 0 ? _k : null,
|
|
1707
|
+
(_l = data.estimateHours) !== null && _l !== void 0 ? _l : null,
|
|
1708
|
+
(_m = data.position) !== null && _m !== void 0 ? _m : nextPosition,
|
|
1709
|
+
(_o = data.tags) !== null && _o !== void 0 ? _o : null,
|
|
1593
1710
|
]);
|
|
1594
|
-
return this.getProjectBoardTask((
|
|
1711
|
+
return this.getProjectBoardTask((_p = created === null || created === void 0 ? void 0 : created.id) !== null && _p !== void 0 ? _p : 0);
|
|
1595
1712
|
}
|
|
1596
1713
|
async updateTask(userId, taskId, data) {
|
|
1597
1714
|
const actor = await this.getActorContext(userId);
|
|
@@ -1607,9 +1724,15 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
1607
1724
|
throw new common_1.BadRequestException('Field "name" is required.');
|
|
1608
1725
|
}
|
|
1609
1726
|
await this.prisma.$transaction(async (tx) => {
|
|
1610
|
-
var _a, _b, _c, _d, _e, _f, _g, _h
|
|
1727
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1611
1728
|
let nextAssignmentId = current.projectAssignmentId;
|
|
1612
1729
|
let nextProjectId = current.projectId;
|
|
1730
|
+
const nextArchived = data.archived !== undefined ? data.archived : Boolean(current.deletedAt);
|
|
1731
|
+
const nextStatus = data.status !== undefined
|
|
1732
|
+
? data.status
|
|
1733
|
+
: current.deletedAt && data.archived === false
|
|
1734
|
+
? 'todo'
|
|
1735
|
+
: current.status;
|
|
1613
1736
|
if (data.projectId !== undefined || data.projectAssignmentId !== undefined) {
|
|
1614
1737
|
const nextAssignment = await this.resolveProjectAssignmentForActor(tx, actor, {
|
|
1615
1738
|
projectId: (_a = data.projectId) !== null && _a !== void 0 ? _a : null,
|
|
@@ -1631,17 +1754,21 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
1631
1754
|
estimate_hours = $9::decimal,
|
|
1632
1755
|
position = $10,
|
|
1633
1756
|
tags = $11,
|
|
1757
|
+
deleted_at = CASE
|
|
1758
|
+
WHEN $12::boolean THEN COALESCE(deleted_at, NOW())
|
|
1759
|
+
ELSE NULL
|
|
1760
|
+
END,
|
|
1634
1761
|
updated_at = NOW()
|
|
1635
|
-
WHERE id = $
|
|
1636
|
-
AND deleted_at IS NULL`, nextProjectId, nextAssignmentId, data.assigneeCollaboratorId !== undefined
|
|
1762
|
+
WHERE id = $13
|
|
1763
|
+
AND ($14::boolean = true OR deleted_at IS NULL)`, nextProjectId, nextAssignmentId, data.assigneeCollaboratorId !== undefined
|
|
1637
1764
|
? ((_c = data.assigneeCollaboratorId) !== null && _c !== void 0 ? _c : null)
|
|
1638
1765
|
: current.assigneeCollaboratorId, nextName, data.description !== undefined
|
|
1639
1766
|
? this.normalizeOptionalText(data.description)
|
|
1640
|
-
: ((_d = current.description) !== null && _d !== void 0 ? _d : null), (_e = data.priority) !== null && _e !== void 0 ? _e : current.priority,
|
|
1767
|
+
: ((_d = current.description) !== null && _d !== void 0 ? _d : null), (_e = data.priority) !== null && _e !== void 0 ? _e : current.priority, nextStatus, data.dueDate !== undefined ? ((_f = data.dueDate) !== null && _f !== void 0 ? _f : null) : current.dueDate, data.estimateHours !== undefined ? ((_g = data.estimateHours) !== null && _g !== void 0 ? _g : null) : current.estimateHours, data.position !== undefined ? data.position : current.position, data.tags !== undefined ? ((_h = data.tags) !== null && _h !== void 0 ? _h : null) : current.tags, nextArchived, taskId, Boolean(current.deletedAt));
|
|
1641
1768
|
});
|
|
1642
1769
|
return this.getProjectBoardTask(taskId);
|
|
1643
1770
|
}
|
|
1644
|
-
async removeTask(userId, taskId) {
|
|
1771
|
+
async removeTask(userId, taskId, permanent = false) {
|
|
1645
1772
|
const actor = await this.getActorContext(userId);
|
|
1646
1773
|
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
1647
1774
|
throw new common_1.ForbiddenException('Operations collaborator access is required.');
|
|
@@ -1649,6 +1776,11 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
1649
1776
|
const current = await this.getTaskRecordForActor(this.prisma, actor, taskId);
|
|
1650
1777
|
await this.assertProjectAccess(actor, current.projectId);
|
|
1651
1778
|
await this.prisma.$transaction(async (tx) => {
|
|
1779
|
+
if (permanent) {
|
|
1780
|
+
await tx.$executeRawUnsafe(`DELETE FROM operations_task
|
|
1781
|
+
WHERE id = $1`, taskId);
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1652
1784
|
await tx.$executeRawUnsafe(`UPDATE operations_task
|
|
1653
1785
|
SET deleted_at = COALESCE(deleted_at, NOW()),
|
|
1654
1786
|
updated_at = NOW()
|
|
@@ -1825,6 +1957,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
1825
1957
|
)
|
|
1826
1958
|
RETURNING id`, timesheetId, assignment.id, (_g = (_f = resolvedTask === null || resolvedTask === void 0 ? void 0 : resolvedTask.id) !== null && _f !== void 0 ? _f : data.taskId) !== null && _g !== void 0 ? _g : null, activityLabel, data.workDate, durationMinutes, Number((durationMinutes / 60).toFixed(2)), this.normalizeOptionalText(data.description)));
|
|
1827
1959
|
await this.refreshTimesheetTotal(tx, timesheetId);
|
|
1960
|
+
await this.submitTimesheetForApproval(tx, timesheetId, actor.collaboratorId);
|
|
1828
1961
|
return (_j = (_h = created[0]) === null || _h === void 0 ? void 0 : _h.id) !== null && _j !== void 0 ? _j : 0;
|
|
1829
1962
|
});
|
|
1830
1963
|
return this.getTimesheetEntryByIdForActor(actor, createdEntryId);
|
|
@@ -1841,8 +1974,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
1841
1974
|
if (!actor.isDirector && entry.collaboratorId !== actor.collaboratorId) {
|
|
1842
1975
|
throw new common_1.ForbiddenException('Only the entry owner can update this timesheet entry.');
|
|
1843
1976
|
}
|
|
1844
|
-
if (
|
|
1845
|
-
throw new common_1.BadRequestException('
|
|
1977
|
+
if (entry.status === 'approved') {
|
|
1978
|
+
throw new common_1.BadRequestException('Approved timesheet entries can no longer be edited.');
|
|
1846
1979
|
}
|
|
1847
1980
|
const collaboratorId = actor.isDirector
|
|
1848
1981
|
? entry.collaboratorId
|
|
@@ -1888,6 +2021,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
1888
2021
|
await this.refreshTimesheetTotal(tx, targetTimesheetId);
|
|
1889
2022
|
}
|
|
1890
2023
|
await this.cleanupEmptyEditableTimesheet(tx, entry.timesheetId);
|
|
2024
|
+
await this.submitTimesheetForApproval(tx, targetTimesheetId, collaboratorId);
|
|
1891
2025
|
});
|
|
1892
2026
|
return this.getTimesheetEntryByIdForActor(actor, entryId);
|
|
1893
2027
|
}
|
|
@@ -1901,8 +2035,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
1901
2035
|
if (!actor.isDirector && entry.collaboratorId !== actor.collaboratorId) {
|
|
1902
2036
|
throw new common_1.ForbiddenException('Only the entry owner can delete this timesheet entry.');
|
|
1903
2037
|
}
|
|
1904
|
-
if (
|
|
1905
|
-
throw new common_1.BadRequestException('
|
|
2038
|
+
if (entry.status === 'approved') {
|
|
2039
|
+
throw new common_1.BadRequestException('Approved timesheet entries can no longer be deleted.');
|
|
1906
2040
|
}
|
|
1907
2041
|
await this.prisma.$transaction(async (tx) => {
|
|
1908
2042
|
await tx.$executeRawUnsafe(`UPDATE operations_timesheet_entry
|
|
@@ -2298,7 +2432,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2298
2432
|
const actor = await this.getActorContext(userId);
|
|
2299
2433
|
this.ensureDirector(actor);
|
|
2300
2434
|
const createdId = await this.prisma.$transaction(async (tx) => {
|
|
2301
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
|
|
2435
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0;
|
|
2302
2436
|
const normalizedCode = (_b = (_a = this.normalizeOptionalText(data.code)) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : (await this.generateContractCode(tx));
|
|
2303
2437
|
const created = await tx.$queryRawUnsafe(`INSERT INTO operations_contract (
|
|
2304
2438
|
code,
|
|
@@ -2353,6 +2487,9 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2353
2487
|
if (data.replaceUploadedPdfDocument) {
|
|
2354
2488
|
await this.replaceContractDocument(tx, contractId, 'source_upload', data.replaceUploadedPdfDocument);
|
|
2355
2489
|
}
|
|
2490
|
+
if ((_0 = data.additionalUploadedDocuments) === null || _0 === void 0 ? void 0 : _0.length) {
|
|
2491
|
+
await this.appendContractDocuments(tx, contractId, 'attachment', data.additionalUploadedDocuments);
|
|
2492
|
+
}
|
|
2356
2493
|
await this.insertContractHistory(tx, contractId, userId, 'created', data.originType === 'manual'
|
|
2357
2494
|
? 'Manual contract created from registry.'
|
|
2358
2495
|
: `Contract registered from origin ${data.originType}.`);
|
|
@@ -2720,7 +2857,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2720
2857
|
this.pushUpdate(updates, params, 'description', data.description);
|
|
2721
2858
|
this.pushUpdate(updates, params, 'content_html', data.contentHtml);
|
|
2722
2859
|
await this.prisma.$transaction(async (tx) => {
|
|
2723
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
|
|
2860
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0;
|
|
2724
2861
|
if (updates.length) {
|
|
2725
2862
|
params.push(contractId);
|
|
2726
2863
|
await tx.$executeRawUnsafe(`UPDATE operations_contract
|
|
@@ -2734,25 +2871,34 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2734
2871
|
if (data.replaceUploadedPdfDocument) {
|
|
2735
2872
|
await this.replaceContractDocument(tx, contractId, 'source_upload', data.replaceUploadedPdfDocument);
|
|
2736
2873
|
}
|
|
2874
|
+
if ((_a = data.additionalUploadedDocuments) === null || _a === void 0 ? void 0 : _a.length) {
|
|
2875
|
+
await this.appendContractDocuments(tx, contractId, 'attachment', data.additionalUploadedDocuments);
|
|
2876
|
+
}
|
|
2877
|
+
if ((_b = data.deletedDocumentIds) === null || _b === void 0 ? void 0 : _b.length) {
|
|
2878
|
+
await this.softDeleteContractDocuments(tx, contractId, data.deletedDocumentIds);
|
|
2879
|
+
}
|
|
2737
2880
|
await this.insertContractHistory(tx, contractId, userId, 'updated', 'Contract registry data updated.');
|
|
2881
|
+
if ((_c = data.deletedDocumentIds) === null || _c === void 0 ? void 0 : _c.length) {
|
|
2882
|
+
await this.insertContractHistory(tx, contractId, userId, 'updated', 'Contract documents removed.');
|
|
2883
|
+
}
|
|
2738
2884
|
if (shouldPublishSigned || shouldPublishActivation) {
|
|
2739
2885
|
const contractEventPayload = await this.buildContractActivatedEventPayload({
|
|
2740
2886
|
id: current.id,
|
|
2741
|
-
code: (
|
|
2742
|
-
name: (
|
|
2743
|
-
clientName: (
|
|
2744
|
-
contractCategory: (
|
|
2745
|
-
contractType: (
|
|
2746
|
-
billingModel: (
|
|
2747
|
-
originType: (
|
|
2748
|
-
originId: (
|
|
2749
|
-
startDate: (
|
|
2750
|
-
endDate: (
|
|
2751
|
-
signedAt: (
|
|
2752
|
-
effectiveDate: (
|
|
2753
|
-
budgetAmount: (
|
|
2754
|
-
description: (
|
|
2755
|
-
parties: (
|
|
2887
|
+
code: (_d = data.code) !== null && _d !== void 0 ? _d : current.code,
|
|
2888
|
+
name: (_e = data.name) !== null && _e !== void 0 ? _e : current.name,
|
|
2889
|
+
clientName: (_f = data.clientName) !== null && _f !== void 0 ? _f : current.clientName,
|
|
2890
|
+
contractCategory: (_g = data.contractCategory) !== null && _g !== void 0 ? _g : current.contractCategory,
|
|
2891
|
+
contractType: (_h = data.contractType) !== null && _h !== void 0 ? _h : current.contractType,
|
|
2892
|
+
billingModel: (_j = data.billingModel) !== null && _j !== void 0 ? _j : current.billingModel,
|
|
2893
|
+
originType: (_k = data.originType) !== null && _k !== void 0 ? _k : current.originType,
|
|
2894
|
+
originId: (_l = data.originId) !== null && _l !== void 0 ? _l : current.originId,
|
|
2895
|
+
startDate: (_m = data.startDate) !== null && _m !== void 0 ? _m : current.startDate,
|
|
2896
|
+
endDate: (_o = data.endDate) !== null && _o !== void 0 ? _o : current.endDate,
|
|
2897
|
+
signedAt: (_p = data.signedAt) !== null && _p !== void 0 ? _p : current.signedAt,
|
|
2898
|
+
effectiveDate: (_q = data.effectiveDate) !== null && _q !== void 0 ? _q : current.effectiveDate,
|
|
2899
|
+
budgetAmount: (_r = data.budgetAmount) !== null && _r !== void 0 ? _r : current.budgetAmount,
|
|
2900
|
+
description: (_s = data.description) !== null && _s !== void 0 ? _s : current.description,
|
|
2901
|
+
parties: (_t = data.parties) !== null && _t !== void 0 ? _t : current.parties,
|
|
2756
2902
|
}, 'en', userId);
|
|
2757
2903
|
if (shouldPublishSigned) {
|
|
2758
2904
|
await this.integrationApi.publishEvent({
|
|
@@ -2760,15 +2906,15 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2760
2906
|
sourceModule: 'operations',
|
|
2761
2907
|
aggregateType: 'contract',
|
|
2762
2908
|
aggregateId: String(contractId),
|
|
2763
|
-
payload: Object.assign(Object.assign({}, contractEventPayload), { signedByUserId: userId !== null && userId !== void 0 ? userId : null, signedAt: (
|
|
2909
|
+
payload: Object.assign(Object.assign({}, contractEventPayload), { signedByUserId: userId !== null && userId !== void 0 ? userId : null, signedAt: (_w = (_v = (_u = data.signedAt) !== null && _u !== void 0 ? _u : current.signedAt) !== null && _v !== void 0 ? _v : contractEventPayload.signedAt) !== null && _w !== void 0 ? _w : new Date().toISOString() }),
|
|
2764
2910
|
metadata: {
|
|
2765
2911
|
producer: 'operations',
|
|
2766
2912
|
correlationId: contractEventPayload.correlationId || `contract:${contractId}`,
|
|
2767
2913
|
sourceModule: 'operations',
|
|
2768
2914
|
sourceEntity: 'contract',
|
|
2769
2915
|
sourceId: String(contractId),
|
|
2770
|
-
originType: (
|
|
2771
|
-
originId: (
|
|
2916
|
+
originType: (_x = data.originType) !== null && _x !== void 0 ? _x : current.originType,
|
|
2917
|
+
originId: (_y = data.originId) !== null && _y !== void 0 ? _y : current.originId,
|
|
2772
2918
|
lifecycle: 'signed',
|
|
2773
2919
|
},
|
|
2774
2920
|
}, {
|
|
@@ -2788,8 +2934,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2788
2934
|
sourceModule: 'operations',
|
|
2789
2935
|
sourceEntity: 'contract',
|
|
2790
2936
|
sourceId: String(contractId),
|
|
2791
|
-
originType: (
|
|
2792
|
-
originId: (
|
|
2937
|
+
originType: (_z = data.originType) !== null && _z !== void 0 ? _z : current.originType,
|
|
2938
|
+
originId: (_0 = data.originId) !== null && _0 !== void 0 ? _0 : current.originId,
|
|
2793
2939
|
lifecycle: 'activated',
|
|
2794
2940
|
},
|
|
2795
2941
|
}, {
|
|
@@ -2848,6 +2994,12 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2848
2994
|
if (filters.status && filters.status !== 'all') {
|
|
2849
2995
|
where.push(`t.status::text = ${this.param(params, filters.status)}`);
|
|
2850
2996
|
}
|
|
2997
|
+
if (filters.dateFrom) {
|
|
2998
|
+
where.push(`t.week_start_date >= ${this.param(params, filters.dateFrom)}::date`);
|
|
2999
|
+
}
|
|
3000
|
+
if (filters.dateTo) {
|
|
3001
|
+
where.push(`t.week_start_date <= ${this.param(params, filters.dateTo)}::date`);
|
|
3002
|
+
}
|
|
2851
3003
|
if (pagination === null || pagination === void 0 ? void 0 : pagination.search) {
|
|
2852
3004
|
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
2853
3005
|
where.push(`(
|
|
@@ -2874,8 +3026,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2874
3026
|
c.display_name AS "collaboratorName",
|
|
2875
3027
|
t.approver_collaborator_id AS "approverCollaboratorId",
|
|
2876
3028
|
a.display_name AS "approverName",
|
|
2877
|
-
t.week_start_date AS "weekStartDate",
|
|
2878
|
-
t.week_end_date AS "weekEndDate",
|
|
3029
|
+
TO_CHAR(t.week_start_date, 'YYYY-MM-DD') AS "weekStartDate",
|
|
3030
|
+
TO_CHAR(t.week_end_date, 'YYYY-MM-DD') AS "weekEndDate",
|
|
2879
3031
|
t.total_hours AS "totalHours",
|
|
2880
3032
|
t.status,
|
|
2881
3033
|
t.submitted_at AS "submittedAt",
|
|
@@ -2984,8 +3136,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
2984
3136
|
if (!actor.isDirector && current.collaboratorId !== actor.collaboratorId) {
|
|
2985
3137
|
throw new common_1.ForbiddenException('You can only update your own timesheets.');
|
|
2986
3138
|
}
|
|
2987
|
-
if (!actor.isDirector &&
|
|
2988
|
-
throw new common_1.BadRequestException('
|
|
3139
|
+
if (!actor.isDirector && current.status === 'approved') {
|
|
3140
|
+
throw new common_1.BadRequestException('Approved timesheets can no longer be edited.');
|
|
2989
3141
|
}
|
|
2990
3142
|
await this.prisma.$transaction(async (tx) => {
|
|
2991
3143
|
const updates = [];
|
|
@@ -3008,50 +3160,16 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3008
3160
|
return this.listSingleTimesheet(actor, timesheetId);
|
|
3009
3161
|
}
|
|
3010
3162
|
async submitTimesheet(userId, timesheetId) {
|
|
3011
|
-
var _a, _b, _c;
|
|
3012
3163
|
const actor = await this.getActorContext(userId);
|
|
3013
3164
|
const current = await this.getTimesheetById(timesheetId);
|
|
3014
3165
|
if (!actor.isDirector && current.collaboratorId !== actor.collaboratorId) {
|
|
3015
3166
|
throw new common_1.ForbiddenException('You can only submit your own timesheets.');
|
|
3016
3167
|
}
|
|
3017
|
-
if (!actor.isDirector &&
|
|
3018
|
-
throw new common_1.BadRequestException('
|
|
3019
|
-
}
|
|
3020
|
-
const collaborator = await this.getCollaboratorById(current.collaboratorId);
|
|
3021
|
-
const projectManagerRow = await this.querySingle(`SELECT p.manager_collaborator_id AS "managerCollaboratorId"
|
|
3022
|
-
FROM operations_timesheet_entry e
|
|
3023
|
-
LEFT JOIN operations_project_assignment pa ON pa.id = e.project_assignment_id
|
|
3024
|
-
LEFT JOIN operations_project p ON p.id = pa.project_id
|
|
3025
|
-
WHERE e.timesheet_id = $1
|
|
3026
|
-
AND e.deleted_at IS NULL
|
|
3027
|
-
AND p.manager_collaborator_id IS NOT NULL
|
|
3028
|
-
LIMIT 1`, [timesheetId]);
|
|
3029
|
-
const approverId = (_c = (_b = (_a = projectManagerRow === null || projectManagerRow === void 0 ? void 0 : projectManagerRow.managerCollaboratorId) !== null && _a !== void 0 ? _a : current.approverCollaboratorId) !== null && _b !== void 0 ? _b : collaborator.supervisorId) !== null && _c !== void 0 ? _c : null;
|
|
3030
|
-
if (!approverId) {
|
|
3031
|
-
throw new common_1.BadRequestException('An approver is required before submitting a timesheet.');
|
|
3032
|
-
}
|
|
3033
|
-
const activeEntries = await this.querySingle(`SELECT EXISTS (
|
|
3034
|
-
SELECT 1
|
|
3035
|
-
FROM operations_timesheet_entry
|
|
3036
|
-
WHERE timesheet_id = $1
|
|
3037
|
-
AND deleted_at IS NULL
|
|
3038
|
-
) AS exists`, [timesheetId]);
|
|
3039
|
-
if (!(activeEntries === null || activeEntries === void 0 ? void 0 : activeEntries.exists)) {
|
|
3040
|
-
throw new common_1.BadRequestException('Cannot submit a timesheet without active entries.');
|
|
3168
|
+
if (!actor.isDirector && current.status === 'approved') {
|
|
3169
|
+
throw new common_1.BadRequestException('Approved timesheets can no longer be submitted.');
|
|
3041
3170
|
}
|
|
3042
3171
|
await this.prisma.$transaction(async (tx) => {
|
|
3043
|
-
await tx
|
|
3044
|
-
SET status = 'submitted',
|
|
3045
|
-
approver_collaborator_id = $1,
|
|
3046
|
-
submitted_at = NOW(),
|
|
3047
|
-
updated_at = NOW()
|
|
3048
|
-
WHERE id = $2`, approverId, timesheetId);
|
|
3049
|
-
await this.upsertApproval(tx, {
|
|
3050
|
-
targetType: 'timesheet',
|
|
3051
|
-
targetId: timesheetId,
|
|
3052
|
-
requesterCollaboratorId: current.collaboratorId,
|
|
3053
|
-
approverCollaboratorId: approverId,
|
|
3054
|
-
});
|
|
3172
|
+
await this.submitTimesheetForApproval(tx, timesheetId, current.collaboratorId, current.approverCollaboratorId);
|
|
3055
3173
|
});
|
|
3056
3174
|
return this.listSingleTimesheet(actor, timesheetId);
|
|
3057
3175
|
}
|
|
@@ -3399,6 +3517,20 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3399
3517
|
if (filters.targetType && filters.targetType !== 'all') {
|
|
3400
3518
|
where.push(`a.target_type = ${this.param(params, filters.targetType)}::text::operations_approval_target_type_32d3f04385_enum`);
|
|
3401
3519
|
}
|
|
3520
|
+
if (filters.dateFrom) {
|
|
3521
|
+
where.push(`(
|
|
3522
|
+
(a.target_type = 'timesheet' AND t.week_start_date >= ${this.param(params, filters.dateFrom)}::date)
|
|
3523
|
+
OR (a.target_type = 'time_off_request' AND tor.start_date >= ${this.param(params, filters.dateFrom)}::date)
|
|
3524
|
+
OR (a.target_type = 'schedule_adjustment_request' AND sar.effective_start_date >= ${this.param(params, filters.dateFrom)}::date)
|
|
3525
|
+
)`);
|
|
3526
|
+
}
|
|
3527
|
+
if (filters.dateTo) {
|
|
3528
|
+
where.push(`(
|
|
3529
|
+
(a.target_type = 'timesheet' AND t.week_start_date <= ${this.param(params, filters.dateTo)}::date)
|
|
3530
|
+
OR (a.target_type = 'time_off_request' AND tor.start_date <= ${this.param(params, filters.dateTo)}::date)
|
|
3531
|
+
OR (a.target_type = 'schedule_adjustment_request' AND sar.effective_start_date <= ${this.param(params, filters.dateTo)}::date)
|
|
3532
|
+
)`);
|
|
3533
|
+
}
|
|
3402
3534
|
if (pagination === null || pagination === void 0 ? void 0 : pagination.search) {
|
|
3403
3535
|
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
3404
3536
|
where.push(`(
|
|
@@ -3421,8 +3553,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3421
3553
|
a.submitted_at AS "submittedAt",
|
|
3422
3554
|
a.decided_at AS "decidedAt",
|
|
3423
3555
|
a.decision_note AS "decisionNote",
|
|
3424
|
-
t.week_start_date AS "timesheetWeekStartDate",
|
|
3425
|
-
t.week_end_date AS "timesheetWeekEndDate",
|
|
3556
|
+
TO_CHAR(t.week_start_date, 'YYYY-MM-DD') AS "timesheetWeekStartDate",
|
|
3557
|
+
TO_CHAR(t.week_end_date, 'YYYY-MM-DD') AS "timesheetWeekEndDate",
|
|
3426
3558
|
t.total_hours AS "timesheetTotalHours",
|
|
3427
3559
|
COALESCE(
|
|
3428
3560
|
STRING_AGG(DISTINCT p.name, ', ') FILTER (WHERE p.name IS NOT NULL),
|
|
@@ -3519,8 +3651,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3519
3651
|
a.submitted_at AS "submittedAt",
|
|
3520
3652
|
a.decided_at AS "decidedAt",
|
|
3521
3653
|
a.decision_note AS "decisionNote",
|
|
3522
|
-
t.week_start_date AS "timesheetWeekStartDate",
|
|
3523
|
-
t.week_end_date AS "timesheetWeekEndDate",
|
|
3654
|
+
TO_CHAR(t.week_start_date, 'YYYY-MM-DD') AS "timesheetWeekStartDate",
|
|
3655
|
+
TO_CHAR(t.week_end_date, 'YYYY-MM-DD') AS "timesheetWeekEndDate",
|
|
3524
3656
|
t.total_hours AS "timesheetTotalHours",
|
|
3525
3657
|
COALESCE(
|
|
3526
3658
|
STRING_AGG(DISTINCT p.name, ', ') FILTER (WHERE p.name IS NOT NULL),
|
|
@@ -3742,7 +3874,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3742
3874
|
WHERE id = $1`, [approvalId]);
|
|
3743
3875
|
}
|
|
3744
3876
|
async getActorContext(userId) {
|
|
3745
|
-
var _a
|
|
3877
|
+
var _a;
|
|
3746
3878
|
const roleSlugs = (await this.prisma.role.findMany({
|
|
3747
3879
|
where: {
|
|
3748
3880
|
role_user: {
|
|
@@ -3762,7 +3894,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3762
3894
|
if (!collaborator && isCollaborator && !isDirector) {
|
|
3763
3895
|
throw new common_1.NotFoundException('The authenticated user does not have a linked operations collaborator profile.');
|
|
3764
3896
|
}
|
|
3765
|
-
const collaboratorId = (
|
|
3897
|
+
const collaboratorId = this.normalizeIntegerId(collaborator === null || collaborator === void 0 ? void 0 : collaborator.id);
|
|
3766
3898
|
const teamCollaboratorIds = isSupervisor && collaboratorId
|
|
3767
3899
|
? await this.getDirectReportIds(collaboratorId)
|
|
3768
3900
|
: [];
|
|
@@ -3770,7 +3902,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3770
3902
|
userId,
|
|
3771
3903
|
roleSlugs,
|
|
3772
3904
|
collaboratorId,
|
|
3773
|
-
collaboratorName: (
|
|
3905
|
+
collaboratorName: (_a = collaborator === null || collaborator === void 0 ? void 0 : collaborator.displayName) !== null && _a !== void 0 ? _a : null,
|
|
3774
3906
|
isDirector,
|
|
3775
3907
|
isSupervisor,
|
|
3776
3908
|
isCollaborator,
|
|
@@ -3893,33 +4025,6 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
3893
4025
|
}
|
|
3894
4026
|
}
|
|
3895
4027
|
async resolveDepartmentReference(client, input) {
|
|
3896
|
-
var _a;
|
|
3897
|
-
if (input.departmentName !== undefined) {
|
|
3898
|
-
const normalizedDepartmentName = this.normalizeOptionalText(input.departmentName);
|
|
3899
|
-
if (!normalizedDepartmentName) {
|
|
3900
|
-
return null;
|
|
3901
|
-
}
|
|
3902
|
-
const existingByName = (await client.$queryRawUnsafe(`SELECT id, name
|
|
3903
|
-
FROM operations_department
|
|
3904
|
-
WHERE deleted_at IS NULL
|
|
3905
|
-
AND LOWER(name) = LOWER($1)
|
|
3906
|
-
ORDER BY id ASC
|
|
3907
|
-
LIMIT 1`, normalizedDepartmentName));
|
|
3908
|
-
if (existingByName[0]) {
|
|
3909
|
-
return existingByName[0];
|
|
3910
|
-
}
|
|
3911
|
-
const slug = await this.generateUniqueDepartmentSlug(client, normalizedDepartmentName);
|
|
3912
|
-
const created = (await client.$queryRawUnsafe(`INSERT INTO operations_department (
|
|
3913
|
-
slug,
|
|
3914
|
-
code,
|
|
3915
|
-
name,
|
|
3916
|
-
description,
|
|
3917
|
-
created_at,
|
|
3918
|
-
updated_at
|
|
3919
|
-
) VALUES ($1, NULL, $2, NULL, NOW(), NOW())
|
|
3920
|
-
RETURNING id, name`, slug, normalizedDepartmentName));
|
|
3921
|
-
return (_a = created[0]) !== null && _a !== void 0 ? _a : null;
|
|
3922
|
-
}
|
|
3923
4028
|
if (typeof input.departmentId === 'number' &&
|
|
3924
4029
|
Number.isInteger(input.departmentId) &&
|
|
3925
4030
|
input.departmentId > 0) {
|
|
@@ -4422,7 +4527,7 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
4422
4527
|
collaborator_type.name AS "collaboratorType",
|
|
4423
4528
|
COALESCE(NULLIF(c.display_name, ''), person_record.name) AS "displayName",
|
|
4424
4529
|
c.department_id AS "departmentId",
|
|
4425
|
-
|
|
4530
|
+
department_record.name AS "department",
|
|
4426
4531
|
c.job_title_id AS "jobTitleId",
|
|
4427
4532
|
COALESCE(NULLIF(job_title_record.name, ''), NULLIF(c.title, '')) AS "title",
|
|
4428
4533
|
c.level_label AS "levelLabel",
|
|
@@ -4573,20 +4678,21 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
4573
4678
|
}, scheduleAdjustmentRequests });
|
|
4574
4679
|
}
|
|
4575
4680
|
async getDirectReportIds(collaboratorId) {
|
|
4576
|
-
return (await this.queryRows(`SELECT id
|
|
4681
|
+
return this.uniqueNumbers((await this.queryRows(`SELECT id
|
|
4577
4682
|
FROM operations_collaborator
|
|
4578
4683
|
WHERE supervisor_collaborator_id = $1
|
|
4579
4684
|
AND deleted_at IS NULL
|
|
4580
|
-
ORDER BY id ASC`, [collaboratorId])).map((row) => row.id);
|
|
4685
|
+
ORDER BY id ASC`, [collaboratorId])).map((row) => row.id));
|
|
4581
4686
|
}
|
|
4582
4687
|
async getAssignedProjectIds(collaboratorIds) {
|
|
4583
|
-
|
|
4688
|
+
const normalizedCollaboratorIds = this.uniqueNumbers(collaboratorIds);
|
|
4689
|
+
if (!normalizedCollaboratorIds.length)
|
|
4584
4690
|
return [];
|
|
4585
|
-
return (await this.queryRows(`SELECT DISTINCT project_id AS "projectId"
|
|
4691
|
+
return this.uniqueNumbers((await this.queryRows(`SELECT DISTINCT project_id AS "projectId"
|
|
4586
4692
|
FROM operations_project_assignment
|
|
4587
4693
|
WHERE deleted_at IS NULL
|
|
4588
4694
|
AND status IN ('planned', 'active')
|
|
4589
|
-
AND collaborator_id = ANY($1::int[])`, [
|
|
4695
|
+
AND collaborator_id = ANY($1::int[])`, [normalizedCollaboratorIds])).map((row) => row.projectId));
|
|
4590
4696
|
}
|
|
4591
4697
|
async resolveOwnedProjectAssignment(client, collaboratorId, input) {
|
|
4592
4698
|
if (!input.projectId && !input.projectAssignmentId) {
|
|
@@ -4670,7 +4776,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
4670
4776
|
t.project_assignment_id AS "projectAssignmentId",
|
|
4671
4777
|
COALESCE(t.project_id, pa.project_id) AS "projectId",
|
|
4672
4778
|
p.name AS "projectName",
|
|
4673
|
-
p.code AS "projectCode"
|
|
4779
|
+
p.code AS "projectCode",
|
|
4780
|
+
t.deleted_at AS "deletedAt"
|
|
4674
4781
|
FROM operations_task t
|
|
4675
4782
|
LEFT JOIN operations_project_assignment pa
|
|
4676
4783
|
ON pa.id = t.project_assignment_id
|
|
@@ -4679,7 +4786,6 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
4679
4786
|
ON p.id = COALESCE(t.project_id, pa.project_id)
|
|
4680
4787
|
AND p.deleted_at IS NULL
|
|
4681
4788
|
WHERE t.id = $1
|
|
4682
|
-
AND t.deleted_at IS NULL
|
|
4683
4789
|
AND (
|
|
4684
4790
|
pa.collaborator_id = $2
|
|
4685
4791
|
OR t.project_id IN (
|
|
@@ -4755,7 +4861,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
4755
4861
|
ap.avatar_id AS "assigneePersonAvatarId",
|
|
4756
4862
|
t.project_assignment_id AS "projectAssignmentId",
|
|
4757
4863
|
COALESCE(t.project_id, pa.project_id) AS "projectId",
|
|
4758
|
-
t.created_at AS "createdAt"
|
|
4864
|
+
t.created_at AS "createdAt",
|
|
4865
|
+
t.deleted_at AS "deletedAt"
|
|
4759
4866
|
FROM operations_task t
|
|
4760
4867
|
LEFT JOIN operations_collaborator ac
|
|
4761
4868
|
ON ac.id = t.assignee_collaborator_id AND ac.deleted_at IS NULL
|
|
@@ -4779,8 +4886,8 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
4779
4886
|
ORDER BY id DESC
|
|
4780
4887
|
LIMIT 1`, collaboratorId, weekStartDate, weekEndDate));
|
|
4781
4888
|
if (existing[0]) {
|
|
4782
|
-
if (
|
|
4783
|
-
throw new common_1.BadRequestException('The timesheet for this week has already been
|
|
4889
|
+
if (existing[0].status === 'approved') {
|
|
4890
|
+
throw new common_1.BadRequestException('The timesheet for this week has already been approved.');
|
|
4784
4891
|
}
|
|
4785
4892
|
return existing[0].id;
|
|
4786
4893
|
}
|
|
@@ -4954,11 +5061,12 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
4954
5061
|
}
|
|
4955
5062
|
async cleanupEmptyEditableTimesheet(client, timesheetId) {
|
|
4956
5063
|
var _a;
|
|
4957
|
-
const candidate = (await client.$queryRawUnsafe(`SELECT t.id
|
|
5064
|
+
const candidate = (await client.$queryRawUnsafe(`SELECT t.id,
|
|
5065
|
+
t.status
|
|
4958
5066
|
FROM operations_timesheet t
|
|
4959
5067
|
WHERE t.id = $1
|
|
4960
5068
|
AND t.deleted_at IS NULL
|
|
4961
|
-
AND t.status IN ('draft', 'rejected')
|
|
5069
|
+
AND t.status IN ('draft', 'rejected', 'submitted')
|
|
4962
5070
|
AND NOT EXISTS (
|
|
4963
5071
|
SELECT 1
|
|
4964
5072
|
FROM operations_timesheet_entry e
|
|
@@ -4974,6 +5082,52 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
4974
5082
|
updated_at = NOW()
|
|
4975
5083
|
WHERE id = $1
|
|
4976
5084
|
AND deleted_at IS NULL`, timesheetId);
|
|
5085
|
+
if (candidate[0].status === 'submitted') {
|
|
5086
|
+
await client.$executeRawUnsafe(`UPDATE operations_approval
|
|
5087
|
+
SET deleted_at = NOW(),
|
|
5088
|
+
updated_at = NOW()
|
|
5089
|
+
WHERE target_type = 'timesheet'::operations_approval_target_type_32d3f04385_enum
|
|
5090
|
+
AND target_id = $1
|
|
5091
|
+
AND deleted_at IS NULL`, timesheetId);
|
|
5092
|
+
}
|
|
5093
|
+
}
|
|
5094
|
+
async submitTimesheetForApproval(client, timesheetId, collaboratorId, currentApproverCollaboratorId) {
|
|
5095
|
+
var _a, _b, _c, _d, _e;
|
|
5096
|
+
const collaborator = await this.getCollaboratorById(collaboratorId);
|
|
5097
|
+
const projectManagerRow = await client.$queryRawUnsafe(`SELECT p.manager_collaborator_id AS "managerCollaboratorId"
|
|
5098
|
+
FROM operations_timesheet_entry e
|
|
5099
|
+
LEFT JOIN operations_project_assignment pa ON pa.id = e.project_assignment_id
|
|
5100
|
+
LEFT JOIN operations_project p ON p.id = pa.project_id
|
|
5101
|
+
WHERE e.timesheet_id = $1
|
|
5102
|
+
AND e.deleted_at IS NULL
|
|
5103
|
+
AND p.manager_collaborator_id IS NOT NULL
|
|
5104
|
+
LIMIT 1`, timesheetId);
|
|
5105
|
+
const approverId = (_d = (_c = (_b = (_a = projectManagerRow[0]) === null || _a === void 0 ? void 0 : _a.managerCollaboratorId) !== null && _b !== void 0 ? _b : currentApproverCollaboratorId) !== null && _c !== void 0 ? _c : collaborator.supervisorId) !== null && _d !== void 0 ? _d : null;
|
|
5106
|
+
if (!approverId) {
|
|
5107
|
+
throw new common_1.BadRequestException('An approver is required before submitting a timesheet.');
|
|
5108
|
+
}
|
|
5109
|
+
const activeEntries = await client.$queryRawUnsafe(`SELECT EXISTS (
|
|
5110
|
+
SELECT 1
|
|
5111
|
+
FROM operations_timesheet_entry
|
|
5112
|
+
WHERE timesheet_id = $1
|
|
5113
|
+
AND deleted_at IS NULL
|
|
5114
|
+
) AS exists`, timesheetId);
|
|
5115
|
+
if (!((_e = activeEntries[0]) === null || _e === void 0 ? void 0 : _e.exists)) {
|
|
5116
|
+
throw new common_1.BadRequestException('Cannot submit a timesheet without active entries.');
|
|
5117
|
+
}
|
|
5118
|
+
await client.$executeRawUnsafe(`UPDATE operations_timesheet
|
|
5119
|
+
SET status = 'submitted',
|
|
5120
|
+
approver_collaborator_id = $1,
|
|
5121
|
+
submitted_at = NOW(),
|
|
5122
|
+
updated_at = NOW()
|
|
5123
|
+
WHERE id = $2
|
|
5124
|
+
AND deleted_at IS NULL`, approverId, timesheetId);
|
|
5125
|
+
await this.upsertApproval(client, {
|
|
5126
|
+
targetType: 'timesheet',
|
|
5127
|
+
targetId: timesheetId,
|
|
5128
|
+
requesterCollaboratorId: collaboratorId,
|
|
5129
|
+
approverCollaboratorId: approverId,
|
|
5130
|
+
});
|
|
4977
5131
|
}
|
|
4978
5132
|
async upsertApproval(client, input) {
|
|
4979
5133
|
var _a, _b;
|
|
@@ -5486,6 +5640,40 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
5486
5640
|
$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()
|
|
5487
5641
|
)`, contractId, documentType, (_a = document.fileId) !== null && _a !== void 0 ? _a : null, document.fileName, document.mimeType, (_b = document.fileContentBase64) !== null && _b !== void 0 ? _b : null, (_c = document.extractionStatus) !== null && _c !== void 0 ? _c : 'skipped', (_d = document.extractionSummary) !== null && _d !== void 0 ? _d : null, (_e = document.notes) !== null && _e !== void 0 ? _e : null);
|
|
5488
5642
|
}
|
|
5643
|
+
async appendContractDocuments(client, contractId, documentType, documents) {
|
|
5644
|
+
var _a, _b, _c, _d, _e;
|
|
5645
|
+
for (const document of documents) {
|
|
5646
|
+
await client.$executeRawUnsafe(`INSERT INTO operations_contract_document (
|
|
5647
|
+
contract_id,
|
|
5648
|
+
document_type,
|
|
5649
|
+
file_id,
|
|
5650
|
+
file_name,
|
|
5651
|
+
mime_type,
|
|
5652
|
+
file_content_base64,
|
|
5653
|
+
is_current,
|
|
5654
|
+
extraction_status,
|
|
5655
|
+
extraction_summary,
|
|
5656
|
+
notes,
|
|
5657
|
+
created_at,
|
|
5658
|
+
updated_at
|
|
5659
|
+
) VALUES (
|
|
5660
|
+
$1, $2::operations_contract_document_document_type_15ebaff0c9_enum, $3, $4, $5, $6, false, $7::operations_contract_document_extraction_status_6d94c231f3_enum, $8, $9, NOW(), NOW()
|
|
5661
|
+
)`, contractId, documentType, (_a = document.fileId) !== null && _a !== void 0 ? _a : null, document.fileName, document.mimeType, (_b = document.fileContentBase64) !== null && _b !== void 0 ? _b : null, (_c = document.extractionStatus) !== null && _c !== void 0 ? _c : 'skipped', (_d = document.extractionSummary) !== null && _d !== void 0 ? _d : null, (_e = document.notes) !== null && _e !== void 0 ? _e : null);
|
|
5662
|
+
}
|
|
5663
|
+
}
|
|
5664
|
+
async softDeleteContractDocuments(client, contractId, documentIds) {
|
|
5665
|
+
const normalizedIds = Array.from(new Set(documentIds.filter((value) => Number.isInteger(value) && Number(value) > 0)));
|
|
5666
|
+
if (!normalizedIds.length) {
|
|
5667
|
+
return;
|
|
5668
|
+
}
|
|
5669
|
+
await client.$executeRawUnsafe(`UPDATE operations_contract_document
|
|
5670
|
+
SET deleted_at = NOW(),
|
|
5671
|
+
updated_at = NOW(),
|
|
5672
|
+
is_current = false
|
|
5673
|
+
WHERE contract_id = $1
|
|
5674
|
+
AND id = ANY($2::int[])
|
|
5675
|
+
AND deleted_at IS NULL`, contractId, normalizedIds);
|
|
5676
|
+
}
|
|
5489
5677
|
async resolveContractExtractionFile(userId, data) {
|
|
5490
5678
|
if (data.contractId && data.contractId > 0) {
|
|
5491
5679
|
const contract = await this.getContractById(userId, data.contractId);
|
|
@@ -5788,6 +5976,17 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
5788
5976
|
$1, $2, $3, $4, $5, NOW()
|
|
5789
5977
|
)`, contractId, actorUserId, action, note, metadataJson !== null && metadataJson !== void 0 ? metadataJson : null);
|
|
5790
5978
|
}
|
|
5979
|
+
async insertCollaboratorCompensationHistory(client, collaboratorId, amount, actorUserId, notes) {
|
|
5980
|
+
await client.$executeRawUnsafe(`INSERT INTO operations_collaborator_compensation_history (
|
|
5981
|
+
collaborator_id,
|
|
5982
|
+
amount,
|
|
5983
|
+
actor_user_id,
|
|
5984
|
+
notes,
|
|
5985
|
+
created_at
|
|
5986
|
+
) VALUES (
|
|
5987
|
+
$1, $2, $3, $4, NOW()
|
|
5988
|
+
)`, collaboratorId, amount, actorUserId, notes !== null && notes !== void 0 ? notes : null);
|
|
5989
|
+
}
|
|
5791
5990
|
async generateCollaboratorCode(client) {
|
|
5792
5991
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
5793
5992
|
const candidate = `COL-${Date.now().toString(36).toUpperCase()}${Math.random()
|
|
@@ -6279,16 +6478,9 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
6279
6478
|
return value.toISOString().slice(0, 10);
|
|
6280
6479
|
}
|
|
6281
6480
|
getWorkWeekRange(workDate) {
|
|
6282
|
-
const date = this.parseDateOnly(workDate);
|
|
6283
|
-
const day = date.getUTCDay();
|
|
6284
|
-
const diffToMonday = day === 0 ? -6 : 1 - day;
|
|
6285
|
-
const weekStart = new Date(date);
|
|
6286
|
-
weekStart.setUTCDate(weekStart.getUTCDate() + diffToMonday);
|
|
6287
|
-
const weekEnd = new Date(weekStart);
|
|
6288
|
-
weekEnd.setUTCDate(weekStart.getUTCDate() + 6);
|
|
6289
6481
|
return {
|
|
6290
|
-
weekStartDate:
|
|
6291
|
-
weekEndDate:
|
|
6482
|
+
weekStartDate: workDate,
|
|
6483
|
+
weekEndDate: workDate,
|
|
6292
6484
|
};
|
|
6293
6485
|
}
|
|
6294
6486
|
normalizePaginationParams(input = {}, options) {
|
|
@@ -6417,22 +6609,39 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
6417
6609
|
}
|
|
6418
6610
|
}
|
|
6419
6611
|
buildIdFilter(ids, column, allowAll) {
|
|
6612
|
+
const normalizedIds = this.uniqueNumbers(ids);
|
|
6420
6613
|
if (allowAll) {
|
|
6421
6614
|
return { clause: '1 = 1', params: [] };
|
|
6422
6615
|
}
|
|
6423
|
-
if (!
|
|
6616
|
+
if (!normalizedIds.length) {
|
|
6424
6617
|
return { clause: '1 = 0', params: [] };
|
|
6425
6618
|
}
|
|
6426
6619
|
return {
|
|
6427
6620
|
clause: `${column} = ANY($1::int[])`,
|
|
6428
|
-
params: [
|
|
6621
|
+
params: [normalizedIds],
|
|
6429
6622
|
};
|
|
6430
6623
|
}
|
|
6431
6624
|
uniqueNumbers(values) {
|
|
6432
6625
|
return [
|
|
6433
|
-
...new Set(values
|
|
6626
|
+
...new Set(values
|
|
6627
|
+
.map((value) => this.normalizeIntegerId(value))
|
|
6628
|
+
.filter((value) => value !== null)),
|
|
6434
6629
|
];
|
|
6435
6630
|
}
|
|
6631
|
+
normalizeIntegerId(value) {
|
|
6632
|
+
if (typeof value === 'number' && Number.isInteger(value)) {
|
|
6633
|
+
return value;
|
|
6634
|
+
}
|
|
6635
|
+
if (typeof value === 'bigint') {
|
|
6636
|
+
const normalized = Number(value);
|
|
6637
|
+
return Number.isSafeInteger(normalized) ? normalized : null;
|
|
6638
|
+
}
|
|
6639
|
+
if (typeof value === 'string') {
|
|
6640
|
+
const normalized = Number(value);
|
|
6641
|
+
return Number.isInteger(normalized) ? normalized : null;
|
|
6642
|
+
}
|
|
6643
|
+
return null;
|
|
6644
|
+
}
|
|
6436
6645
|
pushUpdate(updates, params, column, value, castType) {
|
|
6437
6646
|
if (value === undefined)
|
|
6438
6647
|
return;
|
|
@@ -6466,12 +6675,1040 @@ let OperationsService = OperationsService_1 = class OperationsService {
|
|
|
6466
6675
|
async execute(sql, params = []) {
|
|
6467
6676
|
return this.prisma.$executeRawUnsafe(sql, ...params);
|
|
6468
6677
|
}
|
|
6678
|
+
shiftSqlPlaceholders(sql, offset) {
|
|
6679
|
+
if (offset <= 0) {
|
|
6680
|
+
return sql;
|
|
6681
|
+
}
|
|
6682
|
+
return sql.replace(/\$(\d+)/g, (_match, index) => {
|
|
6683
|
+
return `$${Number(index) + offset}`;
|
|
6684
|
+
});
|
|
6685
|
+
}
|
|
6469
6686
|
async getNextCollaboratorTypeSortOrder(client = this.prisma) {
|
|
6470
6687
|
var _a, _b;
|
|
6471
6688
|
const row = (await client.$queryRawUnsafe(`SELECT COALESCE(MAX(sort_order), 0) + 1 AS "nextSortOrder"
|
|
6472
6689
|
FROM operations_collaborator_type`));
|
|
6473
6690
|
return Number((_b = (_a = row === null || row === void 0 ? void 0 : row[0]) === null || _a === void 0 ? void 0 : _a.nextSortOrder) !== null && _b !== void 0 ? _b : 1);
|
|
6474
6691
|
}
|
|
6692
|
+
// ─── Collaborator-scoped endpoints (no scope manipulation) ─────────────────
|
|
6693
|
+
async listMyProjects(userId, filters = {}) {
|
|
6694
|
+
var _a, _b;
|
|
6695
|
+
const actor = await this.getActorContext(userId);
|
|
6696
|
+
const filter = this.buildIdFilter(actor.collaboratorId
|
|
6697
|
+
? await this.getAssignedProjectIds([actor.collaboratorId])
|
|
6698
|
+
: [], 'p.id', false);
|
|
6699
|
+
const assignmentParams = [];
|
|
6700
|
+
const ownAssignmentSelect = actor.collaboratorId
|
|
6701
|
+
? `MAX(CASE WHEN pa.collaborator_id = ${this.param(assignmentParams, actor.collaboratorId)} THEN pa.id END)::int AS "myAssignmentId",
|
|
6702
|
+
MAX(CASE WHEN pa.collaborator_id = ${this.param(assignmentParams, actor.collaboratorId)} THEN pa.role_label END) AS "myRoleLabel",`
|
|
6703
|
+
: `NULL::int AS "myAssignmentId",
|
|
6704
|
+
NULL::varchar AS "myRoleLabel",`;
|
|
6705
|
+
const pagination = this.shouldPaginate(filters)
|
|
6706
|
+
? this.normalizePaginationParams(filters, {
|
|
6707
|
+
defaultSortField: 'name',
|
|
6708
|
+
defaultSortOrder: 'asc',
|
|
6709
|
+
allowedSortFields: ['name', 'code', 'clientName', 'startDate', 'endDate', 'status'],
|
|
6710
|
+
})
|
|
6711
|
+
: null;
|
|
6712
|
+
const params = [...assignmentParams, ...filter.params];
|
|
6713
|
+
const totalParams = [...filter.params];
|
|
6714
|
+
const where = [
|
|
6715
|
+
'p.deleted_at IS NULL',
|
|
6716
|
+
this.shiftSqlPlaceholders(filter.clause, assignmentParams.length),
|
|
6717
|
+
];
|
|
6718
|
+
const totalWhere = ['p.deleted_at IS NULL', filter.clause];
|
|
6719
|
+
if (filters.status && filters.status !== 'all') {
|
|
6720
|
+
where.push(`p.status::text = ${this.param(params, filters.status)}`);
|
|
6721
|
+
totalWhere.push(`p.status::text = ${this.param(totalParams, filters.status)}`);
|
|
6722
|
+
}
|
|
6723
|
+
if (pagination === null || pagination === void 0 ? void 0 : pagination.search) {
|
|
6724
|
+
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
6725
|
+
const totalSearchPlaceholder = this.param(totalParams, `%${pagination.search}%`);
|
|
6726
|
+
where.push(`(
|
|
6727
|
+
COALESCE(p.name, '') ILIKE ${searchPlaceholder}
|
|
6728
|
+
OR COALESCE(p.code, '') ILIKE ${searchPlaceholder}
|
|
6729
|
+
OR COALESCE(p.client_name, '') ILIKE ${searchPlaceholder}
|
|
6730
|
+
OR COALESCE(c.name, '') ILIKE ${searchPlaceholder}
|
|
6731
|
+
OR COALESCE(m.display_name, '') ILIKE ${searchPlaceholder}
|
|
6732
|
+
)`);
|
|
6733
|
+
totalWhere.push(`(
|
|
6734
|
+
COALESCE(p.name, '') ILIKE ${totalSearchPlaceholder}
|
|
6735
|
+
OR COALESCE(p.code, '') ILIKE ${totalSearchPlaceholder}
|
|
6736
|
+
OR COALESCE(p.client_name, '') ILIKE ${totalSearchPlaceholder}
|
|
6737
|
+
OR COALESCE(c.name, '') ILIKE ${totalSearchPlaceholder}
|
|
6738
|
+
OR COALESCE(m.display_name, '') ILIKE ${totalSearchPlaceholder}
|
|
6739
|
+
)`);
|
|
6740
|
+
}
|
|
6741
|
+
const whereClause = where.join(' AND ');
|
|
6742
|
+
const totalWhereClause = totalWhere.join(' AND ');
|
|
6743
|
+
const baseQuery = `SELECT p.id,
|
|
6744
|
+
p.contract_id AS "contractId",
|
|
6745
|
+
p.manager_collaborator_id AS "managerCollaboratorId",
|
|
6746
|
+
p.code,
|
|
6747
|
+
p.name,
|
|
6748
|
+
p.client_name AS "clientName",
|
|
6749
|
+
p.summary,
|
|
6750
|
+
p.status,
|
|
6751
|
+
p.progress_percent AS "progressPercent",
|
|
6752
|
+
p.delivery_model AS "deliveryModel",
|
|
6753
|
+
TO_CHAR(p.start_date, 'YYYY-MM-DD') AS "startDate",
|
|
6754
|
+
TO_CHAR(p.end_date, 'YYYY-MM-DD') AS "endDate",
|
|
6755
|
+
m.display_name AS "managerName",
|
|
6756
|
+
${ownAssignmentSelect}
|
|
6757
|
+
COUNT(DISTINCT pa.id)::int AS "teamSize"
|
|
6758
|
+
FROM operations_project p
|
|
6759
|
+
LEFT JOIN operations_contract c ON c.id = p.contract_id
|
|
6760
|
+
LEFT JOIN operations_collaborator m ON m.id = p.manager_collaborator_id
|
|
6761
|
+
LEFT JOIN operations_project_assignment pa
|
|
6762
|
+
ON pa.project_id = p.id
|
|
6763
|
+
AND pa.deleted_at IS NULL
|
|
6764
|
+
AND pa.status IN ('planned', 'active')
|
|
6765
|
+
WHERE ${whereClause}
|
|
6766
|
+
GROUP BY p.id, c.id, m.id`;
|
|
6767
|
+
if (!pagination) {
|
|
6768
|
+
return this.queryRows(`${baseQuery} ORDER BY p.name ASC`, params);
|
|
6769
|
+
}
|
|
6770
|
+
const totalRow = await this.querySingle(`SELECT COUNT(*)::text AS total
|
|
6771
|
+
FROM operations_project p
|
|
6772
|
+
LEFT JOIN operations_contract c ON c.id = p.contract_id
|
|
6773
|
+
LEFT JOIN operations_collaborator m ON m.id = p.manager_collaborator_id
|
|
6774
|
+
WHERE ${totalWhereClause}`, totalParams);
|
|
6775
|
+
const sortColumn = (_a = {
|
|
6776
|
+
name: 'p.name',
|
|
6777
|
+
code: 'p.code',
|
|
6778
|
+
clientName: 'p.client_name',
|
|
6779
|
+
startDate: 'p.start_date',
|
|
6780
|
+
endDate: 'p.end_date',
|
|
6781
|
+
status: 'p.status',
|
|
6782
|
+
}[pagination.sortField]) !== null && _a !== void 0 ? _a : 'p.name';
|
|
6783
|
+
const queryParams = [...params];
|
|
6784
|
+
const limitPlaceholder = this.param(queryParams, pagination.pageSize);
|
|
6785
|
+
const offsetPlaceholder = this.param(queryParams, pagination.offset);
|
|
6786
|
+
const rows = await this.queryRows(`${baseQuery}
|
|
6787
|
+
ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, p.id ASC
|
|
6788
|
+
LIMIT ${limitPlaceholder}
|
|
6789
|
+
OFFSET ${offsetPlaceholder}`, queryParams);
|
|
6790
|
+
return this.buildPaginationResult(rows, Number((_b = totalRow === null || totalRow === void 0 ? void 0 : totalRow.total) !== null && _b !== void 0 ? _b : 0), pagination.page, pagination.pageSize);
|
|
6791
|
+
}
|
|
6792
|
+
async getMyProjectSummary(userId, projectId) {
|
|
6793
|
+
var _a;
|
|
6794
|
+
const actor = await this.getActorContext(userId);
|
|
6795
|
+
await this.assertProjectAccess(actor, projectId);
|
|
6796
|
+
const projectParams = [];
|
|
6797
|
+
const myCollaboratorId = (_a = actor.collaboratorId) !== null && _a !== void 0 ? _a : null;
|
|
6798
|
+
const collaboratorPlaceholder = this.param(projectParams, myCollaboratorId);
|
|
6799
|
+
const project = await this.querySingle(`SELECT p.id,
|
|
6800
|
+
p.code,
|
|
6801
|
+
p.name,
|
|
6802
|
+
p.status,
|
|
6803
|
+
p.summary,
|
|
6804
|
+
TO_CHAR(p.start_date, 'YYYY-MM-DD') AS "startDate",
|
|
6805
|
+
TO_CHAR(p.end_date, 'YYYY-MM-DD') AS "endDate",
|
|
6806
|
+
MAX(CASE WHEN pa.collaborator_id = ${collaboratorPlaceholder} THEN pa.id END)::int AS "myAssignmentId",
|
|
6807
|
+
MAX(CASE WHEN pa.collaborator_id = ${collaboratorPlaceholder} THEN COALESCE(project_role_locale.name, pa.role_label) END) AS "myRoleLabel"
|
|
6808
|
+
FROM operations_project p
|
|
6809
|
+
LEFT JOIN operations_project_assignment pa
|
|
6810
|
+
ON pa.project_id = p.id
|
|
6811
|
+
AND pa.deleted_at IS NULL
|
|
6812
|
+
LEFT JOIN operations_project_role project_role
|
|
6813
|
+
ON project_role.id = pa.project_role_id
|
|
6814
|
+
AND project_role.deleted_at IS NULL
|
|
6815
|
+
LEFT JOIN LATERAL (
|
|
6816
|
+
SELECT prl.name
|
|
6817
|
+
FROM operations_project_role_locale prl
|
|
6818
|
+
WHERE prl.operations_project_role_id = project_role.id
|
|
6819
|
+
ORDER BY prl.id ASC
|
|
6820
|
+
LIMIT 1
|
|
6821
|
+
) project_role_locale ON TRUE
|
|
6822
|
+
WHERE p.id = ${this.param(projectParams, projectId)}
|
|
6823
|
+
AND p.deleted_at IS NULL
|
|
6824
|
+
GROUP BY p.id`, projectParams);
|
|
6825
|
+
if (!project) {
|
|
6826
|
+
throw new common_1.NotFoundException('Project not found.');
|
|
6827
|
+
}
|
|
6828
|
+
const [assignments, tasks] = await Promise.all([
|
|
6829
|
+
this.queryRows(`SELECT pa.id,
|
|
6830
|
+
pa.collaborator_id AS "collaboratorId",
|
|
6831
|
+
c.display_name AS "collaboratorName",
|
|
6832
|
+
COALESCE(project_role_locale.name, pa.role_label) AS "roleLabel",
|
|
6833
|
+
pa.status,
|
|
6834
|
+
person_record.avatar_id AS "avatarId",
|
|
6835
|
+
collaborator_user.photo_id AS "userPhotoId"
|
|
6836
|
+
FROM operations_project_assignment pa
|
|
6837
|
+
JOIN operations_collaborator c ON c.id = pa.collaborator_id
|
|
6838
|
+
LEFT JOIN person person_record ON person_record.id = c.person_id
|
|
6839
|
+
LEFT JOIN "user" collaborator_user ON collaborator_user.id = c.user_id
|
|
6840
|
+
LEFT JOIN operations_project_role project_role
|
|
6841
|
+
ON project_role.id = pa.project_role_id
|
|
6842
|
+
AND project_role.deleted_at IS NULL
|
|
6843
|
+
LEFT JOIN LATERAL (
|
|
6844
|
+
SELECT prl.name
|
|
6845
|
+
FROM operations_project_role_locale prl
|
|
6846
|
+
WHERE prl.operations_project_role_id = project_role.id
|
|
6847
|
+
ORDER BY prl.id ASC
|
|
6848
|
+
LIMIT 1
|
|
6849
|
+
) project_role_locale ON TRUE
|
|
6850
|
+
WHERE pa.project_id = $1
|
|
6851
|
+
AND pa.deleted_at IS NULL
|
|
6852
|
+
ORDER BY c.display_name ASC`, [projectId]),
|
|
6853
|
+
this.queryRows(`SELECT t.id,
|
|
6854
|
+
t.name,
|
|
6855
|
+
t.description,
|
|
6856
|
+
t.priority,
|
|
6857
|
+
t.status,
|
|
6858
|
+
t.due_date AS "dueDate",
|
|
6859
|
+
t.estimate_hours AS "estimateHours",
|
|
6860
|
+
t.position,
|
|
6861
|
+
t.tags,
|
|
6862
|
+
t.assignee_collaborator_id AS "assigneeCollaboratorId",
|
|
6863
|
+
ac.display_name AS "assigneeName",
|
|
6864
|
+
au.photo_id AS "assigneeUserPhotoId",
|
|
6865
|
+
ap.avatar_id AS "assigneePersonAvatarId",
|
|
6866
|
+
t.project_assignment_id AS "projectAssignmentId",
|
|
6867
|
+
t.created_at AS "createdAt"
|
|
6868
|
+
FROM operations_task t
|
|
6869
|
+
LEFT JOIN operations_collaborator ac
|
|
6870
|
+
ON ac.id = t.assignee_collaborator_id AND ac.deleted_at IS NULL
|
|
6871
|
+
LEFT JOIN "user" au ON au.id = ac.user_id
|
|
6872
|
+
LEFT JOIN person ap ON ap.id = ac.person_id
|
|
6873
|
+
WHERE COALESCE(t.project_id, (
|
|
6874
|
+
SELECT pa.project_id FROM operations_project_assignment pa
|
|
6875
|
+
WHERE pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
|
|
6876
|
+
LIMIT 1
|
|
6877
|
+
)) = $1
|
|
6878
|
+
AND t.deleted_at IS NULL
|
|
6879
|
+
ORDER BY t.status ASC, t.position ASC, t.id ASC`, [projectId]),
|
|
6880
|
+
]);
|
|
6881
|
+
return Object.assign(Object.assign({}, project), { assignments,
|
|
6882
|
+
tasks });
|
|
6883
|
+
}
|
|
6884
|
+
async listMyTasks(userId, paginationParams = {}) {
|
|
6885
|
+
var _a, _b;
|
|
6886
|
+
const actor = await this.getActorContext(userId);
|
|
6887
|
+
this.ensureCollaborator(actor);
|
|
6888
|
+
const pagination = this.normalizePaginationParams(paginationParams, {
|
|
6889
|
+
defaultSortField: 'name',
|
|
6890
|
+
defaultSortOrder: 'asc',
|
|
6891
|
+
allowedSortFields: ['name', 'projectName', 'status', 'createdAt'],
|
|
6892
|
+
});
|
|
6893
|
+
const projectFilter = this.buildIdFilter(actor.collaboratorId
|
|
6894
|
+
? await this.getAssignedProjectIds([actor.collaboratorId])
|
|
6895
|
+
: [], 'COALESCE(t.project_id, pa.project_id)', false);
|
|
6896
|
+
const params = [...projectFilter.params];
|
|
6897
|
+
const archivedOnly = paginationParams.archived === true;
|
|
6898
|
+
const filters = [
|
|
6899
|
+
archivedOnly ? 't.deleted_at IS NOT NULL' : 't.deleted_at IS NULL',
|
|
6900
|
+
'p.deleted_at IS NULL',
|
|
6901
|
+
projectFilter.clause,
|
|
6902
|
+
`(
|
|
6903
|
+
t.project_id IS NOT NULL
|
|
6904
|
+
OR (
|
|
6905
|
+
pa.id IS NOT NULL
|
|
6906
|
+
AND pa.deleted_at IS NULL
|
|
6907
|
+
AND pa.status IN ('planned', 'active')
|
|
6908
|
+
)
|
|
6909
|
+
)`,
|
|
6910
|
+
];
|
|
6911
|
+
if (actor.collaboratorId) {
|
|
6912
|
+
filters.push(`pa.collaborator_id = ${this.param(params, actor.collaboratorId)}`);
|
|
6913
|
+
}
|
|
6914
|
+
if (pagination.search) {
|
|
6915
|
+
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
6916
|
+
filters.push(`(
|
|
6917
|
+
COALESCE(t.name, '') ILIKE ${searchPlaceholder}
|
|
6918
|
+
OR COALESCE(t.description, '') ILIKE ${searchPlaceholder}
|
|
6919
|
+
OR COALESCE(p.name, '') ILIKE ${searchPlaceholder}
|
|
6920
|
+
OR COALESCE(p.code, '') ILIKE ${searchPlaceholder}
|
|
6921
|
+
)`);
|
|
6922
|
+
}
|
|
6923
|
+
if (paginationParams.status) {
|
|
6924
|
+
filters.push(`t.status::text = ${this.param(params, paginationParams.status)}`);
|
|
6925
|
+
}
|
|
6926
|
+
const whereClause = filters.join(' AND ');
|
|
6927
|
+
const totalRow = await this.querySingle(`SELECT COUNT(*)::text AS total
|
|
6928
|
+
FROM operations_task t
|
|
6929
|
+
LEFT JOIN operations_project_assignment pa
|
|
6930
|
+
ON pa.id = t.project_assignment_id
|
|
6931
|
+
JOIN operations_project p
|
|
6932
|
+
ON p.id = COALESCE(t.project_id, pa.project_id)
|
|
6933
|
+
WHERE ${whereClause}`, params);
|
|
6934
|
+
const sortColumn = (_a = {
|
|
6935
|
+
name: 't.name',
|
|
6936
|
+
projectName: 'p.name',
|
|
6937
|
+
status: 't.status',
|
|
6938
|
+
createdAt: 't.created_at',
|
|
6939
|
+
}[pagination.sortField]) !== null && _a !== void 0 ? _a : 't.name';
|
|
6940
|
+
const queryParams = [...params];
|
|
6941
|
+
const limitPlaceholder = this.param(queryParams, pagination.pageSize);
|
|
6942
|
+
const offsetPlaceholder = this.param(queryParams, pagination.offset);
|
|
6943
|
+
const rows = await this.queryRows(`SELECT t.id,
|
|
6944
|
+
t.name,
|
|
6945
|
+
t.description,
|
|
6946
|
+
t.status,
|
|
6947
|
+
t.priority,
|
|
6948
|
+
COALESCE(t.project_id, pa.project_id) AS "projectId",
|
|
6949
|
+
pa.id AS "projectAssignmentId",
|
|
6950
|
+
p.name AS "projectName",
|
|
6951
|
+
p.code AS "projectCode",
|
|
6952
|
+
t.due_date AS "dueDate",
|
|
6953
|
+
t.estimate_hours AS "estimateHours",
|
|
6954
|
+
t.tags,
|
|
6955
|
+
ac.display_name AS "assigneeName",
|
|
6956
|
+
au.photo_id AS "assigneeUserPhotoId",
|
|
6957
|
+
ap.avatar_id AS "assigneePersonAvatarId",
|
|
6958
|
+
t.created_at AS "createdAt",
|
|
6959
|
+
t.deleted_at AS "deletedAt"
|
|
6960
|
+
FROM operations_task t
|
|
6961
|
+
LEFT JOIN operations_project_assignment pa
|
|
6962
|
+
ON pa.id = t.project_assignment_id
|
|
6963
|
+
LEFT JOIN operations_collaborator ac
|
|
6964
|
+
ON ac.id = t.assignee_collaborator_id
|
|
6965
|
+
AND ac.deleted_at IS NULL
|
|
6966
|
+
LEFT JOIN "user" au
|
|
6967
|
+
ON au.id = ac.user_id
|
|
6968
|
+
LEFT JOIN person ap
|
|
6969
|
+
ON ap.id = ac.person_id
|
|
6970
|
+
JOIN operations_project p
|
|
6971
|
+
ON p.id = COALESCE(t.project_id, pa.project_id)
|
|
6972
|
+
WHERE ${whereClause}
|
|
6973
|
+
ORDER BY ${sortColumn} ${pagination.sortOrder.toUpperCase()}, t.id ASC
|
|
6974
|
+
LIMIT ${limitPlaceholder}
|
|
6975
|
+
OFFSET ${offsetPlaceholder}`, queryParams);
|
|
6976
|
+
return this.buildPaginationResult(rows.map((row) => (Object.assign(Object.assign({}, row), { label: [row.name, row.projectName].filter(Boolean).join(' • ') }))), Number((_b = totalRow === null || totalRow === void 0 ? void 0 : totalRow.total) !== null && _b !== void 0 ? _b : 0), pagination.page, pagination.pageSize);
|
|
6977
|
+
}
|
|
6978
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
6979
|
+
// Cost Types
|
|
6980
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
6981
|
+
async listCostTypes(userId, filters = {}) {
|
|
6982
|
+
var _a;
|
|
6983
|
+
await this.getActorContext(userId);
|
|
6984
|
+
const params = [];
|
|
6985
|
+
const where = [];
|
|
6986
|
+
if (filters.active === true) {
|
|
6987
|
+
where.push('ct.is_active = true');
|
|
6988
|
+
}
|
|
6989
|
+
if ((_a = filters.search) === null || _a === void 0 ? void 0 : _a.trim()) {
|
|
6990
|
+
const p = this.param(params, `%${filters.search.trim()}%`);
|
|
6991
|
+
where.push(`(COALESCE(ct.name, '') ILIKE ${p} OR COALESCE(ct.slug, '') ILIKE ${p} OR COALESCE(ct.code, '') ILIKE ${p} OR COALESCE(ct.category, '') ILIKE ${p})`);
|
|
6992
|
+
}
|
|
6993
|
+
const whereClause = where.length ? `WHERE ${where.join(' AND ')}` : '';
|
|
6994
|
+
const rows = await this.queryRows(`SELECT ct.id,
|
|
6995
|
+
ct.slug,
|
|
6996
|
+
ct.name,
|
|
6997
|
+
ct.code,
|
|
6998
|
+
ct.category,
|
|
6999
|
+
ct.description,
|
|
7000
|
+
ct.default_recurrence AS "defaultRecurrence",
|
|
7001
|
+
ct.is_allocatable AS "isAllocatable",
|
|
7002
|
+
ct.is_depreciable AS "isDepreciable",
|
|
7003
|
+
ct.is_active AS "isActive",
|
|
7004
|
+
ct.created_at AS "createdAt"
|
|
7005
|
+
FROM operations_cost_type ct
|
|
7006
|
+
${whereClause}
|
|
7007
|
+
ORDER BY ct.name ASC`, params);
|
|
7008
|
+
return rows;
|
|
7009
|
+
}
|
|
7010
|
+
async createCostType(userId, data) {
|
|
7011
|
+
var _a, _b, _c, _d, _e;
|
|
7012
|
+
const actor = await this.getActorContext(userId);
|
|
7013
|
+
this.ensureDirector(actor);
|
|
7014
|
+
const name = this.normalizeOptionalText(data.name);
|
|
7015
|
+
if (!name) {
|
|
7016
|
+
throw new common_1.BadRequestException('Cost type name is required.');
|
|
7017
|
+
}
|
|
7018
|
+
const baseSlug = ((_a = data.slug) === null || _a === void 0 ? void 0 : _a.trim()) || name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
|
7019
|
+
const existing = await this.querySingle(`SELECT id FROM operations_cost_type WHERE slug = $1 LIMIT 1`, [baseSlug]);
|
|
7020
|
+
const finalSlug = existing
|
|
7021
|
+
? `${baseSlug}-${Date.now()}`
|
|
7022
|
+
: baseSlug;
|
|
7023
|
+
const created = await this.querySingle(`INSERT INTO operations_cost_type (slug, name, code, category, description, default_recurrence, is_allocatable, is_depreciable, is_active, created_at, updated_at)
|
|
7024
|
+
VALUES ($1, $2, $3, $4, $5, $6::operations_cost_type_default_recurrence_82bb00323e_enum, $7, $8, $9, NOW(), NOW())
|
|
7025
|
+
RETURNING id`, [
|
|
7026
|
+
finalSlug,
|
|
7027
|
+
name,
|
|
7028
|
+
this.normalizeOptionalText(data.code),
|
|
7029
|
+
this.normalizeOptionalText(data.category),
|
|
7030
|
+
this.normalizeOptionalText(data.description),
|
|
7031
|
+
(_b = data.defaultRecurrence) !== null && _b !== void 0 ? _b : null,
|
|
7032
|
+
(_c = data.isAllocatable) !== null && _c !== void 0 ? _c : null,
|
|
7033
|
+
(_d = data.isDepreciable) !== null && _d !== void 0 ? _d : null,
|
|
7034
|
+
(_e = data.isActive) !== null && _e !== void 0 ? _e : true,
|
|
7035
|
+
]);
|
|
7036
|
+
if (!(created === null || created === void 0 ? void 0 : created.id)) {
|
|
7037
|
+
throw new common_1.BadRequestException('Unable to create cost type.');
|
|
7038
|
+
}
|
|
7039
|
+
return this.querySingle(`SELECT id, slug, name, code, category, description, default_recurrence AS "defaultRecurrence", is_allocatable AS "isAllocatable", is_depreciable AS "isDepreciable", is_active AS "isActive" FROM operations_cost_type WHERE id = $1`, [created.id]);
|
|
7040
|
+
}
|
|
7041
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
7042
|
+
// Collaborator Costs
|
|
7043
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
7044
|
+
async listCollaboratorCosts(userId, collaboratorId) {
|
|
7045
|
+
await this.getActorContext(userId);
|
|
7046
|
+
return this.queryRows(`SELECT cc.id,
|
|
7047
|
+
cc.collaborator_id AS "collaboratorId",
|
|
7048
|
+
cc.cost_type_id AS "costTypeId",
|
|
7049
|
+
ct.name AS "costTypeName",
|
|
7050
|
+
ct.slug AS "costTypeSlug",
|
|
7051
|
+
cc.amount::text AS amount,
|
|
7052
|
+
cc.currency,
|
|
7053
|
+
cc.recurrence,
|
|
7054
|
+
cc.allocatable,
|
|
7055
|
+
cc.reference_date::text AS "referenceDate",
|
|
7056
|
+
cc.start_date::text AS "startDate",
|
|
7057
|
+
cc.end_date::text AS "endDate",
|
|
7058
|
+
cc.depreciation_months AS "depreciationMonths",
|
|
7059
|
+
cc.description,
|
|
7060
|
+
cc.notes,
|
|
7061
|
+
cc.created_at AS "createdAt"
|
|
7062
|
+
FROM operations_collaborator_cost cc
|
|
7063
|
+
JOIN operations_cost_type ct ON ct.id = cc.cost_type_id
|
|
7064
|
+
WHERE cc.collaborator_id = $1
|
|
7065
|
+
ORDER BY cc.created_at DESC`, [collaboratorId]);
|
|
7066
|
+
}
|
|
7067
|
+
async getCollaboratorCostsSummary(userId, collaboratorId) {
|
|
7068
|
+
await this.getActorContext(userId);
|
|
7069
|
+
const costs = await this.listCollaboratorCosts(userId, collaboratorId);
|
|
7070
|
+
let monthlyTotal = 0;
|
|
7071
|
+
let allocatableTotal = 0;
|
|
7072
|
+
for (const c of costs) {
|
|
7073
|
+
const amount = parseFloat(c.amount);
|
|
7074
|
+
let monthly = 0;
|
|
7075
|
+
if (c.recurrence === 'one_time') {
|
|
7076
|
+
monthly = c.depreciationMonths && c.depreciationMonths > 0 ? amount / c.depreciationMonths : 0;
|
|
7077
|
+
}
|
|
7078
|
+
else if (c.recurrence === 'monthly') {
|
|
7079
|
+
monthly = amount;
|
|
7080
|
+
}
|
|
7081
|
+
else if (c.recurrence === 'yearly') {
|
|
7082
|
+
monthly = amount / 12;
|
|
7083
|
+
}
|
|
7084
|
+
else if (c.recurrence === 'quarterly') {
|
|
7085
|
+
monthly = amount / 3;
|
|
7086
|
+
}
|
|
7087
|
+
else {
|
|
7088
|
+
monthly = amount;
|
|
7089
|
+
}
|
|
7090
|
+
monthlyTotal += monthly;
|
|
7091
|
+
if (c.allocatable !== false) {
|
|
7092
|
+
allocatableTotal += monthly;
|
|
7093
|
+
}
|
|
7094
|
+
}
|
|
7095
|
+
return {
|
|
7096
|
+
monthlyTotal: Math.round(monthlyTotal * 100) / 100,
|
|
7097
|
+
allocatableTotal: Math.round(allocatableTotal * 100) / 100,
|
|
7098
|
+
nonAllocatableTotal: Math.round((monthlyTotal - allocatableTotal) * 100) / 100,
|
|
7099
|
+
count: costs.length,
|
|
7100
|
+
};
|
|
7101
|
+
}
|
|
7102
|
+
async getProjectsReport(userId, filters = {}) {
|
|
7103
|
+
var _a, _b, _c, _d;
|
|
7104
|
+
const actor = await this.getActorContext(userId);
|
|
7105
|
+
this.ensureDirector(actor);
|
|
7106
|
+
const currentYear = new Date().getFullYear();
|
|
7107
|
+
const from = /^\d{4}-\d{2}-\d{2}$/.test(String((_a = filters.from) !== null && _a !== void 0 ? _a : ''))
|
|
7108
|
+
? String(filters.from)
|
|
7109
|
+
: `${currentYear}-01-01`;
|
|
7110
|
+
const to = /^\d{4}-\d{2}-\d{2}$/.test(String((_b = filters.to) !== null && _b !== void 0 ? _b : ''))
|
|
7111
|
+
? String(filters.to)
|
|
7112
|
+
: `${currentYear}-12-31`;
|
|
7113
|
+
const scenario = filters.scenario === 'growth' || filters.scenario === 'conservative'
|
|
7114
|
+
? filters.scenario
|
|
7115
|
+
: 'base';
|
|
7116
|
+
const multiplier = scenario === 'growth'
|
|
7117
|
+
? { revenue: 1.16, cost: 1.09, backlog: 1.22 }
|
|
7118
|
+
: scenario === 'conservative'
|
|
7119
|
+
? { revenue: 0.9, cost: 0.96, backlog: 0.82 }
|
|
7120
|
+
: { revenue: 1, cost: 1, backlog: 1 };
|
|
7121
|
+
const params = [from, to];
|
|
7122
|
+
const where = [
|
|
7123
|
+
'p.deleted_at IS NULL',
|
|
7124
|
+
'(p.end_date IS NULL OR p.end_date >= $1::date)',
|
|
7125
|
+
'(p.start_date IS NULL OR p.start_date <= $2::date)',
|
|
7126
|
+
];
|
|
7127
|
+
if (filters.client && filters.client !== 'all') {
|
|
7128
|
+
where.push(`COALESCE(NULLIF(p.client_name, ''), NULLIF(contract_record.client_name, ''), '-') = ${this.param(params, filters.client)}`);
|
|
7129
|
+
}
|
|
7130
|
+
const dbRows = await this.queryRows(`SELECT p.id,
|
|
7131
|
+
p.name,
|
|
7132
|
+
COALESCE(NULLIF(p.client_name, ''), NULLIF(contract_record.client_name, ''), '-') AS client,
|
|
7133
|
+
COALESCE(NULLIF(manager_record.display_name, ''), '-') AS manager,
|
|
7134
|
+
COALESCE(NULLIF(p.delivery_model::text, ''), '-') AS squad,
|
|
7135
|
+
p.status::text AS status,
|
|
7136
|
+
COALESCE(contract_record.billing_model::text, p.delivery_model::text, 'fixed_price') AS "contractType",
|
|
7137
|
+
TO_CHAR(p.start_date, 'YYYY-MM-DD') AS "startDate",
|
|
7138
|
+
TO_CHAR(p.end_date, 'YYYY-MM-DD') AS "endDate",
|
|
7139
|
+
COALESCE(p.budget_amount, contract_record.budget_amount, 0)::text AS "contractedRevenue",
|
|
7140
|
+
COALESCE(p.progress_percent, 0)::text AS "progressPercent",
|
|
7141
|
+
COALESCE(assignment_stats.weekly_hours, 0)::text AS "weeklyHours",
|
|
7142
|
+
COALESCE(time_stats.actual_hours, 0)::text AS "actualHours",
|
|
7143
|
+
COALESCE(time_stats.billable_hours, 0)::text AS "billableHours",
|
|
7144
|
+
COALESCE(task_stats.open_tasks, 0)::text AS "openTasks",
|
|
7145
|
+
COALESCE(task_stats.backlog_hours, 0)::text AS "backlogHours",
|
|
7146
|
+
COALESCE(task_stats.future_deliveries, 0)::text AS "futureDeliveries"
|
|
7147
|
+
FROM operations_project p
|
|
7148
|
+
LEFT JOIN operations_contract contract_record
|
|
7149
|
+
ON contract_record.id = p.contract_id
|
|
7150
|
+
AND contract_record.deleted_at IS NULL
|
|
7151
|
+
LEFT JOIN operations_collaborator manager_record
|
|
7152
|
+
ON manager_record.id = p.manager_collaborator_id
|
|
7153
|
+
AND manager_record.deleted_at IS NULL
|
|
7154
|
+
LEFT JOIN LATERAL (
|
|
7155
|
+
SELECT COALESCE(SUM(pa.weekly_hours), 0) AS weekly_hours
|
|
7156
|
+
FROM operations_project_assignment pa
|
|
7157
|
+
WHERE pa.project_id = p.id
|
|
7158
|
+
AND pa.deleted_at IS NULL
|
|
7159
|
+
AND pa.status IN ('planned', 'active')
|
|
7160
|
+
) assignment_stats ON TRUE
|
|
7161
|
+
LEFT JOIN LATERAL (
|
|
7162
|
+
SELECT COALESCE(SUM(entry.hours), 0) AS actual_hours,
|
|
7163
|
+
COALESCE(SUM(entry.hours) FILTER (WHERE pa.is_billable = true), 0) AS billable_hours
|
|
7164
|
+
FROM operations_timesheet_entry entry
|
|
7165
|
+
JOIN operations_project_assignment pa
|
|
7166
|
+
ON pa.id = entry.project_assignment_id
|
|
7167
|
+
AND pa.deleted_at IS NULL
|
|
7168
|
+
WHERE pa.project_id = p.id
|
|
7169
|
+
AND entry.deleted_at IS NULL
|
|
7170
|
+
AND entry.work_date BETWEEN $1::date AND $2::date
|
|
7171
|
+
) time_stats ON TRUE
|
|
7172
|
+
LEFT JOIN LATERAL (
|
|
7173
|
+
SELECT COUNT(*) FILTER (WHERE task.status IN ('todo', 'doing', 'review')) AS open_tasks,
|
|
7174
|
+
COALESCE(SUM(task.estimate_hours) FILTER (WHERE task.status IN ('todo', 'doing', 'review')), 0) AS backlog_hours,
|
|
7175
|
+
COUNT(*) FILTER (WHERE task.due_date > $2::date AND task.status IN ('todo', 'doing', 'review')) AS future_deliveries
|
|
7176
|
+
FROM operations_task task
|
|
7177
|
+
WHERE task.project_id = p.id
|
|
7178
|
+
AND task.deleted_at IS NULL
|
|
7179
|
+
) task_stats ON TRUE
|
|
7180
|
+
WHERE ${where.join(' AND ')}
|
|
7181
|
+
ORDER BY p.name ASC`, params);
|
|
7182
|
+
const fromDate = new Date(`${from}T00:00:00`);
|
|
7183
|
+
const toDate = new Date(`${to}T00:00:00`);
|
|
7184
|
+
const periodWeeks = Math.max(1, Math.ceil((toDate.getTime() - fromDate.getTime()) / 604800000));
|
|
7185
|
+
const rows = dbRows
|
|
7186
|
+
.map((row) => {
|
|
7187
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
7188
|
+
const progress = Number((_a = row.progressPercent) !== null && _a !== void 0 ? _a : 0);
|
|
7189
|
+
const contractedRevenue = Number((_b = row.contractedRevenue) !== null && _b !== void 0 ? _b : 0);
|
|
7190
|
+
const recognizedRevenue = contractedRevenue * (progress / 100);
|
|
7191
|
+
const actualHours = Number((_c = row.actualHours) !== null && _c !== void 0 ? _c : 0);
|
|
7192
|
+
const plannedHours = Math.max(Number((_d = row.weeklyHours) !== null && _d !== void 0 ? _d : 0) * periodWeeks, actualHours);
|
|
7193
|
+
const realizedCost = 0;
|
|
7194
|
+
const reportStatus = row.status === 'paused'
|
|
7195
|
+
? 'paused'
|
|
7196
|
+
: row.status === 'at_risk'
|
|
7197
|
+
? 'attention'
|
|
7198
|
+
: row.endDate && new Date(`${row.endDate}T00:00:00`) < new Date() && progress < 100
|
|
7199
|
+
? 'late'
|
|
7200
|
+
: 'on_track';
|
|
7201
|
+
const risk = reportStatus === 'late' || (plannedHours && actualHours / plannedHours > 1)
|
|
7202
|
+
? 'alto'
|
|
7203
|
+
: reportStatus === 'attention'
|
|
7204
|
+
? 'médio'
|
|
7205
|
+
: 'baixo';
|
|
7206
|
+
return {
|
|
7207
|
+
id: Number(row.id),
|
|
7208
|
+
name: row.name,
|
|
7209
|
+
client: (_e = row.client) !== null && _e !== void 0 ? _e : '-',
|
|
7210
|
+
manager: (_f = row.manager) !== null && _f !== void 0 ? _f : '-',
|
|
7211
|
+
squad: String((_g = row.squad) !== null && _g !== void 0 ? _g : '-').replace(/_/g, ' '),
|
|
7212
|
+
status: reportStatus,
|
|
7213
|
+
contractType: row.contractType === 'monthly_retainer'
|
|
7214
|
+
? 'retainer'
|
|
7215
|
+
: row.contractType === 'time_and_material'
|
|
7216
|
+
? 'time_materials'
|
|
7217
|
+
: 'fixed_price',
|
|
7218
|
+
priority: risk === 'alto' ? 'alta' : risk === 'médio' ? 'média' : 'baixa',
|
|
7219
|
+
startDate: row.startDate,
|
|
7220
|
+
endDate: row.endDate,
|
|
7221
|
+
contractedRevenue,
|
|
7222
|
+
recognizedRevenue,
|
|
7223
|
+
realizedCost,
|
|
7224
|
+
forecastCost: realizedCost,
|
|
7225
|
+
teamCost: realizedCost,
|
|
7226
|
+
infraCost: 0,
|
|
7227
|
+
licenseCost: 0,
|
|
7228
|
+
thirdPartyCost: 0,
|
|
7229
|
+
reworkCost: 0,
|
|
7230
|
+
plannedHours,
|
|
7231
|
+
actualHours,
|
|
7232
|
+
billableHours: Number((_h = row.billableHours) !== null && _h !== void 0 ? _h : 0),
|
|
7233
|
+
reworkHours: 0,
|
|
7234
|
+
internalHours: Math.max(actualHours - Number((_j = row.billableHours) !== null && _j !== void 0 ? _j : 0), 0),
|
|
7235
|
+
allocatedCapacity: plannedHours ? (actualHours / plannedHours) * 100 : 0,
|
|
7236
|
+
physicalProgress: progress,
|
|
7237
|
+
financialProgress: contractedRevenue ? (recognizedRevenue / contractedRevenue) * 100 : 0,
|
|
7238
|
+
backlogValue: Math.max(contractedRevenue - recognizedRevenue, 0),
|
|
7239
|
+
futureDeliveries: Number((_k = row.futureDeliveries) !== null && _k !== void 0 ? _k : 0),
|
|
7240
|
+
risk,
|
|
7241
|
+
recommendation: risk === 'alto'
|
|
7242
|
+
? 'Revisar escopo, prazo ou capacidade alocada.'
|
|
7243
|
+
: risk === 'médio'
|
|
7244
|
+
? 'Acompanhar margem, entregas e consumo de horas.'
|
|
7245
|
+
: 'Manter ritmo e proteger capacidade planejada.',
|
|
7246
|
+
};
|
|
7247
|
+
})
|
|
7248
|
+
.filter((row) => !filters.status || filters.status === 'all' || row.status === filters.status);
|
|
7249
|
+
const summary = rows.reduce((acc, row) => {
|
|
7250
|
+
acc.contractedRevenue += row.contractedRevenue;
|
|
7251
|
+
acc.recognizedRevenue += row.recognizedRevenue;
|
|
7252
|
+
acc.realizedCost += row.realizedCost;
|
|
7253
|
+
acc.forecastCost += row.forecastCost;
|
|
7254
|
+
acc.plannedHours += row.plannedHours;
|
|
7255
|
+
acc.actualHours += row.actualHours;
|
|
7256
|
+
acc.billableHours += row.billableHours;
|
|
7257
|
+
acc.reworkHours += row.reworkHours;
|
|
7258
|
+
acc.backlogValue += row.backlogValue;
|
|
7259
|
+
acc.avgDeadline += row.physicalProgress;
|
|
7260
|
+
acc.avgAllocation += row.allocatedCapacity;
|
|
7261
|
+
acc.atRisk += row.risk === 'alto' ? 1 : 0;
|
|
7262
|
+
return acc;
|
|
7263
|
+
}, {
|
|
7264
|
+
contractedRevenue: 0,
|
|
7265
|
+
recognizedRevenue: 0,
|
|
7266
|
+
realizedCost: 0,
|
|
7267
|
+
forecastCost: 0,
|
|
7268
|
+
profit: 0,
|
|
7269
|
+
margin: 0,
|
|
7270
|
+
plannedHours: 0,
|
|
7271
|
+
actualHours: 0,
|
|
7272
|
+
billableHours: 0,
|
|
7273
|
+
reworkHours: 0,
|
|
7274
|
+
backlogValue: 0,
|
|
7275
|
+
avgDeadline: 0,
|
|
7276
|
+
avgAllocation: 0,
|
|
7277
|
+
atRisk: 0,
|
|
7278
|
+
burnRate: 0,
|
|
7279
|
+
});
|
|
7280
|
+
summary.profit = summary.recognizedRevenue - summary.realizedCost;
|
|
7281
|
+
summary.margin = summary.recognizedRevenue ? (summary.profit / summary.recognizedRevenue) * 100 : 0;
|
|
7282
|
+
summary.avgDeadline = rows.length ? summary.avgDeadline / rows.length : 0;
|
|
7283
|
+
summary.avgAllocation = rows.length ? summary.avgAllocation / rows.length : 0;
|
|
7284
|
+
summary.burnRate = summary.plannedHours ? (summary.actualHours / summary.plannedHours) * 100 : 0;
|
|
7285
|
+
const forecast = Array.from({ length: 12 }, (_, index) => {
|
|
7286
|
+
const monthDate = new Date(fromDate);
|
|
7287
|
+
monthDate.setMonth(fromDate.getMonth() + index);
|
|
7288
|
+
const revenue = Math.round((summary.recognizedRevenue / 12) * multiplier.revenue);
|
|
7289
|
+
const cost = Math.round((summary.realizedCost / 12) * multiplier.cost);
|
|
7290
|
+
const profit = revenue - cost;
|
|
7291
|
+
return {
|
|
7292
|
+
month: monthDate.toLocaleDateString('pt-BR', { month: 'short' }).replace('.', ''),
|
|
7293
|
+
revenue,
|
|
7294
|
+
cost,
|
|
7295
|
+
profit,
|
|
7296
|
+
backlog: Math.round(Math.max(profit, 0) * multiplier.backlog),
|
|
7297
|
+
planned: Math.round(summary.plannedHours / 12),
|
|
7298
|
+
actual: Math.round(summary.actualHours / 12),
|
|
7299
|
+
};
|
|
7300
|
+
});
|
|
7301
|
+
return {
|
|
7302
|
+
filters: {
|
|
7303
|
+
from,
|
|
7304
|
+
to,
|
|
7305
|
+
status: (_c = filters.status) !== null && _c !== void 0 ? _c : 'all',
|
|
7306
|
+
client: (_d = filters.client) !== null && _d !== void 0 ? _d : 'all',
|
|
7307
|
+
scenario,
|
|
7308
|
+
clients: [...new Set(dbRows.map((row) => { var _a; return (_a = row.client) !== null && _a !== void 0 ? _a : '-'; }))].sort(),
|
|
7309
|
+
},
|
|
7310
|
+
summary,
|
|
7311
|
+
forecast,
|
|
7312
|
+
costComposition: [
|
|
7313
|
+
{ name: 'Equipe', value: summary.realizedCost },
|
|
7314
|
+
{ name: 'Infraestrutura', value: 0 },
|
|
7315
|
+
{ name: 'Licenças', value: 0 },
|
|
7316
|
+
{ name: 'Terceiros', value: 0 },
|
|
7317
|
+
{ name: 'Retrabalho', value: 0 },
|
|
7318
|
+
],
|
|
7319
|
+
hoursByProject: rows.map((row) => ({
|
|
7320
|
+
project: row.name.split(' ').slice(0, 2).join(' '),
|
|
7321
|
+
Faturável: row.billableHours,
|
|
7322
|
+
Interno: row.internalHours,
|
|
7323
|
+
Retrabalho: row.reworkHours,
|
|
7324
|
+
Livre: Math.max(row.plannedHours - row.actualHours, 0),
|
|
7325
|
+
})),
|
|
7326
|
+
health: rows.map((row) => ({
|
|
7327
|
+
project: row.name.split(' ').slice(0, 2).join(' '),
|
|
7328
|
+
margem: Math.max(row.recognizedRevenue ? ((row.recognizedRevenue - row.realizedCost) / row.recognizedRevenue) * 100 : 0, 0),
|
|
7329
|
+
prazo: row.physicalProgress,
|
|
7330
|
+
alocacao: row.allocatedCapacity,
|
|
7331
|
+
saude: Math.max(100 - (row.risk === 'alto' ? 28 : row.risk === 'médio' ? 14 : 0), 35),
|
|
7332
|
+
})),
|
|
7333
|
+
ranking: rows
|
|
7334
|
+
.map((row) => ({
|
|
7335
|
+
name: row.name.split(' ').slice(0, 2).join(' '),
|
|
7336
|
+
Lucro: row.recognizedRevenue - row.realizedCost,
|
|
7337
|
+
Custo: row.realizedCost,
|
|
7338
|
+
}))
|
|
7339
|
+
.sort((a, b) => b.Lucro - a.Lucro),
|
|
7340
|
+
progress: forecast.map((row) => ({
|
|
7341
|
+
month: row.month,
|
|
7342
|
+
Planejado: row.planned,
|
|
7343
|
+
Realizado: row.actual,
|
|
7344
|
+
})),
|
|
7345
|
+
planningCards: [
|
|
7346
|
+
{ title: 'Acelerar críticas', value: String(summary.atRisk), description: 'Projetos com risco alto devem receber revisão de prazo e capacidade.' },
|
|
7347
|
+
{ title: 'Renegociar escopo', value: Math.round(summary.backlogValue).toString(), description: 'Backlog financeiro ainda não reconhecido no período.' },
|
|
7348
|
+
{ title: 'Realocar equipe', value: Math.round(Math.max(summary.plannedHours - summary.actualHours, 0)).toString(), description: 'Horas planejadas ainda não consumidas.' },
|
|
7349
|
+
{ title: 'Priorizar margem', value: rows.filter((row) => row.recognizedRevenue > row.realizedCost).length.toString(), description: 'Projetos com contribuição positiva no recorte.' },
|
|
7350
|
+
],
|
|
7351
|
+
rows,
|
|
7352
|
+
};
|
|
7353
|
+
}
|
|
7354
|
+
async getCollaboratorsReport(userId, filters = {}) {
|
|
7355
|
+
var _a, _b, _c, _d;
|
|
7356
|
+
const actor = await this.getActorContext(userId);
|
|
7357
|
+
this.ensureDirector(actor);
|
|
7358
|
+
const currentYear = new Date().getFullYear();
|
|
7359
|
+
const from = /^\d{4}-\d{2}-\d{2}$/.test(String((_a = filters.from) !== null && _a !== void 0 ? _a : ''))
|
|
7360
|
+
? String(filters.from)
|
|
7361
|
+
: `${currentYear}-01-01`;
|
|
7362
|
+
const to = /^\d{4}-\d{2}-\d{2}$/.test(String((_b = filters.to) !== null && _b !== void 0 ? _b : ''))
|
|
7363
|
+
? String(filters.to)
|
|
7364
|
+
: `${currentYear}-12-31`;
|
|
7365
|
+
const scenario = filters.scenario === 'growth' || filters.scenario === 'conservative'
|
|
7366
|
+
? filters.scenario
|
|
7367
|
+
: 'base';
|
|
7368
|
+
const multiplier = scenario === 'growth'
|
|
7369
|
+
? { revenue: 1.18, cost: 1.11, capacity: 1.08 }
|
|
7370
|
+
: scenario === 'conservative'
|
|
7371
|
+
? { revenue: 0.9, cost: 0.96, capacity: 0.94 }
|
|
7372
|
+
: { revenue: 1, cost: 1, capacity: 1 };
|
|
7373
|
+
const params = [from, to];
|
|
7374
|
+
const where = [
|
|
7375
|
+
'c.deleted_at IS NULL',
|
|
7376
|
+
'(c.joined_at IS NULL OR c.joined_at <= $2::date)',
|
|
7377
|
+
'(c.left_at IS NULL OR c.left_at >= $1::date)',
|
|
7378
|
+
];
|
|
7379
|
+
if (filters.department && filters.department !== 'all') {
|
|
7380
|
+
where.push(`COALESCE(department_record.name, '-') = ${this.param(params, filters.department)}`);
|
|
7381
|
+
}
|
|
7382
|
+
if (filters.contractType && filters.contractType !== 'all') {
|
|
7383
|
+
where.push(`COALESCE(collaborator_type.name, collaborator_type.slug, '-') = ${this.param(params, filters.contractType)}`);
|
|
7384
|
+
}
|
|
7385
|
+
const dbRows = await this.queryRows(`SELECT c.id,
|
|
7386
|
+
COALESCE(NULLIF(c.display_name, ''), person_record.name, c.code) AS name,
|
|
7387
|
+
COALESCE(job_title_record.name, c.title, '-') AS role,
|
|
7388
|
+
COALESCE(c.level_label, '-') AS seniority,
|
|
7389
|
+
COALESCE(department_record.name, '-') AS department,
|
|
7390
|
+
COALESCE(collaborator_type.name, collaborator_type.slug, '-') AS "contractType",
|
|
7391
|
+
TO_CHAR(c.joined_at, 'YYYY-MM-DD') AS "startDate",
|
|
7392
|
+
TO_CHAR(c.left_at, 'YYYY-MM-DD') AS "endDate",
|
|
7393
|
+
COALESCE(c.weekly_capacity_hours, 40)::text AS "weeklyCapacityHours",
|
|
7394
|
+
COALESCE(cost_stats.salary_cost, 0)::text AS "salaryCost",
|
|
7395
|
+
COALESCE(cost_stats.benefits_cost, 0)::text AS "benefitsCost",
|
|
7396
|
+
COALESCE(cost_stats.taxes_cost, 0)::text AS "taxesCost",
|
|
7397
|
+
COALESCE(cost_stats.tools_cost, 0)::text AS "toolsCost",
|
|
7398
|
+
COALESCE(value_stats.billable_value, 0)::text AS "billableValue",
|
|
7399
|
+
COALESCE(value_stats.allocated_hours, 0)::text AS "allocatedHours",
|
|
7400
|
+
COALESCE(value_stats.billable_hours, 0)::text AS "billableHours",
|
|
7401
|
+
COALESCE(project_stats.projects, 0)::text AS projects
|
|
7402
|
+
FROM operations_collaborator c
|
|
7403
|
+
LEFT JOIN person person_record ON person_record.id = c.person_id
|
|
7404
|
+
LEFT JOIN operations_department department_record
|
|
7405
|
+
ON department_record.id = c.department_id
|
|
7406
|
+
AND department_record.deleted_at IS NULL
|
|
7407
|
+
LEFT JOIN operations_job_title job_title_record
|
|
7408
|
+
ON job_title_record.id = c.job_title_id
|
|
7409
|
+
AND job_title_record.deleted_at IS NULL
|
|
7410
|
+
LEFT JOIN operations_collaborator_type collaborator_type
|
|
7411
|
+
ON collaborator_type.id = c.collaborator_type_id
|
|
7412
|
+
AND collaborator_type.deleted_at IS NULL
|
|
7413
|
+
LEFT JOIN LATERAL (
|
|
7414
|
+
SELECT COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('salario-base', 'pro-labore')), 0) AS salary_cost,
|
|
7415
|
+
COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('vale-refeicao', 'vale-alimentacao', 'vale-transporte', 'plano-saude', 'plano-odontologico', 'seguro-vida')), 0) AS benefits_cost,
|
|
7416
|
+
COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('inss-patronal', 'fgts', 'rat-fap', 'terceiros-sistema-s', 'provisao-decimo-terceiro', 'provisao-ferias')), 0) AS taxes_cost,
|
|
7417
|
+
COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('software-licenca', 'equipamento')), 0) AS tools_cost
|
|
7418
|
+
FROM operations_collaborator_cost cost
|
|
7419
|
+
LEFT JOIN operations_cost_type cost_type ON cost_type.id = cost.cost_type_id
|
|
7420
|
+
WHERE cost.collaborator_id = c.id
|
|
7421
|
+
AND (cost.start_date IS NULL OR cost.start_date <= $2::date)
|
|
7422
|
+
AND (cost.end_date IS NULL OR cost.end_date >= $1::date)
|
|
7423
|
+
) cost_stats ON TRUE
|
|
7424
|
+
LEFT JOIN LATERAL (
|
|
7425
|
+
SELECT COALESCE(SUM(entry.hours), 0) AS allocated_hours,
|
|
7426
|
+
COALESCE(SUM(entry.hours) FILTER (WHERE pa.is_billable = true), 0) AS billable_hours,
|
|
7427
|
+
COALESCE(SUM(entry.hours) FILTER (WHERE pa.is_billable = true) * 120, 0) AS billable_value
|
|
7428
|
+
FROM operations_timesheet_entry entry
|
|
7429
|
+
JOIN operations_project_assignment pa
|
|
7430
|
+
ON pa.id = entry.project_assignment_id
|
|
7431
|
+
AND pa.deleted_at IS NULL
|
|
7432
|
+
WHERE pa.collaborator_id = c.id
|
|
7433
|
+
AND entry.deleted_at IS NULL
|
|
7434
|
+
AND entry.work_date BETWEEN $1::date AND $2::date
|
|
7435
|
+
) value_stats ON TRUE
|
|
7436
|
+
LEFT JOIN LATERAL (
|
|
7437
|
+
SELECT COUNT(DISTINCT pa.project_id) AS projects
|
|
7438
|
+
FROM operations_project_assignment pa
|
|
7439
|
+
WHERE pa.collaborator_id = c.id
|
|
7440
|
+
AND pa.deleted_at IS NULL
|
|
7441
|
+
AND pa.status IN ('planned', 'active')
|
|
7442
|
+
) project_stats ON TRUE
|
|
7443
|
+
WHERE ${where.join(' AND ')}
|
|
7444
|
+
ORDER BY name ASC`, params);
|
|
7445
|
+
const fromDate = new Date(`${from}T00:00:00`);
|
|
7446
|
+
const toDate = new Date(`${to}T00:00:00`);
|
|
7447
|
+
const periodWeeks = Math.max(1, Math.ceil((toDate.getTime() - fromDate.getTime()) / 604800000));
|
|
7448
|
+
const rows = dbRows.map((row) => {
|
|
7449
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
7450
|
+
const salaryCost = Number((_a = row.salaryCost) !== null && _a !== void 0 ? _a : 0);
|
|
7451
|
+
const benefitsCost = Number((_b = row.benefitsCost) !== null && _b !== void 0 ? _b : 0);
|
|
7452
|
+
const taxesCost = Number((_c = row.taxesCost) !== null && _c !== void 0 ? _c : 0);
|
|
7453
|
+
const toolsCost = Number((_d = row.toolsCost) !== null && _d !== void 0 ? _d : 0);
|
|
7454
|
+
const availableHours = Number((_e = row.weeklyCapacityHours) !== null && _e !== void 0 ? _e : 40) * periodWeeks;
|
|
7455
|
+
const allocatedHours = Number((_f = row.allocatedHours) !== null && _f !== void 0 ? _f : 0);
|
|
7456
|
+
const billableHours = Number((_g = row.billableHours) !== null && _g !== void 0 ? _g : 0);
|
|
7457
|
+
const allocation = availableHours ? (allocatedHours / availableHours) * 100 : 0;
|
|
7458
|
+
const risk = allocation >= 98 ? 'alto' : allocation < 75 ? 'médio' : 'baixo';
|
|
7459
|
+
return {
|
|
7460
|
+
id: Number(row.id),
|
|
7461
|
+
name: row.name,
|
|
7462
|
+
role: (_h = row.role) !== null && _h !== void 0 ? _h : '-',
|
|
7463
|
+
seniority: (_j = row.seniority) !== null && _j !== void 0 ? _j : '-',
|
|
7464
|
+
department: (_k = row.department) !== null && _k !== void 0 ? _k : '-',
|
|
7465
|
+
contractType: (_l = row.contractType) !== null && _l !== void 0 ? _l : '-',
|
|
7466
|
+
startDate: row.startDate,
|
|
7467
|
+
endDate: row.endDate,
|
|
7468
|
+
salaryCost,
|
|
7469
|
+
benefitsCost,
|
|
7470
|
+
taxesCost,
|
|
7471
|
+
toolsCost,
|
|
7472
|
+
billableValue: Number((_m = row.billableValue) !== null && _m !== void 0 ? _m : 0),
|
|
7473
|
+
availableHours,
|
|
7474
|
+
allocatedHours,
|
|
7475
|
+
billableHours,
|
|
7476
|
+
internalHours: Math.max(allocatedHours - billableHours, 0),
|
|
7477
|
+
overtimeHours: Math.max(allocatedHours - availableHours, 0),
|
|
7478
|
+
projects: Number((_o = row.projects) !== null && _o !== void 0 ? _o : 0),
|
|
7479
|
+
risk,
|
|
7480
|
+
recommendation: risk === 'alto'
|
|
7481
|
+
? 'Reduzir sobrecarga ou redistribuir entregas.'
|
|
7482
|
+
: risk === 'médio'
|
|
7483
|
+
? 'Acompanhar alocação e buscar maior aproveitamento faturável.'
|
|
7484
|
+
: 'Manter alocação e proteger margem.',
|
|
7485
|
+
};
|
|
7486
|
+
});
|
|
7487
|
+
const summary = rows.reduce((acc, row) => {
|
|
7488
|
+
acc.cost += row.salaryCost + row.benefitsCost + row.taxesCost + row.toolsCost;
|
|
7489
|
+
acc.salary += row.salaryCost;
|
|
7490
|
+
acc.benefits += row.benefitsCost;
|
|
7491
|
+
acc.taxes += row.taxesCost;
|
|
7492
|
+
acc.tools += row.toolsCost;
|
|
7493
|
+
acc.billableValue += row.billableValue;
|
|
7494
|
+
acc.availableHours += row.availableHours;
|
|
7495
|
+
acc.allocatedHours += row.allocatedHours;
|
|
7496
|
+
acc.billableHours += row.billableHours;
|
|
7497
|
+
acc.internalHours += row.internalHours;
|
|
7498
|
+
acc.overtimeHours += row.overtimeHours;
|
|
7499
|
+
acc.overloadCount += row.risk === 'alto' ? 1 : 0;
|
|
7500
|
+
return acc;
|
|
7501
|
+
}, {
|
|
7502
|
+
cost: 0,
|
|
7503
|
+
salary: 0,
|
|
7504
|
+
benefits: 0,
|
|
7505
|
+
taxes: 0,
|
|
7506
|
+
tools: 0,
|
|
7507
|
+
billableValue: 0,
|
|
7508
|
+
profit: 0,
|
|
7509
|
+
margin: 0,
|
|
7510
|
+
availableHours: 0,
|
|
7511
|
+
allocatedHours: 0,
|
|
7512
|
+
billableHours: 0,
|
|
7513
|
+
internalHours: 0,
|
|
7514
|
+
overtimeHours: 0,
|
|
7515
|
+
freeHours: 0,
|
|
7516
|
+
allocation: 0,
|
|
7517
|
+
utilization: 0,
|
|
7518
|
+
overloadCount: 0,
|
|
7519
|
+
hourlyCost: 0,
|
|
7520
|
+
});
|
|
7521
|
+
summary.profit = summary.billableValue - summary.cost;
|
|
7522
|
+
summary.margin = summary.billableValue ? (summary.profit / summary.billableValue) * 100 : 0;
|
|
7523
|
+
summary.freeHours = Math.max(summary.availableHours - summary.allocatedHours, 0);
|
|
7524
|
+
summary.allocation = summary.availableHours ? (summary.allocatedHours / summary.availableHours) * 100 : 0;
|
|
7525
|
+
summary.utilization = summary.availableHours ? (summary.billableHours / summary.availableHours) * 100 : 0;
|
|
7526
|
+
summary.hourlyCost = summary.allocatedHours ? summary.cost / summary.allocatedHours : 0;
|
|
7527
|
+
const forecast = Array.from({ length: 12 }, (_, index) => {
|
|
7528
|
+
const monthDate = new Date(fromDate);
|
|
7529
|
+
monthDate.setMonth(fromDate.getMonth() + index);
|
|
7530
|
+
const revenue = Math.round((summary.billableValue / 12) * multiplier.revenue);
|
|
7531
|
+
const cost = Math.round((summary.cost / 12) * multiplier.cost);
|
|
7532
|
+
const profit = revenue - cost;
|
|
7533
|
+
return {
|
|
7534
|
+
month: monthDate.toLocaleDateString('pt-BR', { month: 'short' }).replace('.', ''),
|
|
7535
|
+
revenue,
|
|
7536
|
+
cost,
|
|
7537
|
+
profit,
|
|
7538
|
+
margin: revenue ? Math.round((profit / revenue) * 100) : 0,
|
|
7539
|
+
capacity: Math.round((summary.allocatedHours / 12) * multiplier.capacity),
|
|
7540
|
+
};
|
|
7541
|
+
});
|
|
7542
|
+
const departments = [...new Set(dbRows.map((row) => { var _a; return (_a = row.department) !== null && _a !== void 0 ? _a : '-'; }))].sort();
|
|
7543
|
+
return {
|
|
7544
|
+
filters: {
|
|
7545
|
+
from,
|
|
7546
|
+
to,
|
|
7547
|
+
department: (_c = filters.department) !== null && _c !== void 0 ? _c : 'all',
|
|
7548
|
+
contractType: (_d = filters.contractType) !== null && _d !== void 0 ? _d : 'all',
|
|
7549
|
+
scenario,
|
|
7550
|
+
departments,
|
|
7551
|
+
contractTypes: [...new Set(dbRows.map((row) => { var _a; return (_a = row.contractType) !== null && _a !== void 0 ? _a : '-'; }))].sort(),
|
|
7552
|
+
},
|
|
7553
|
+
summary,
|
|
7554
|
+
forecast,
|
|
7555
|
+
costComposition: [
|
|
7556
|
+
{ name: 'Salários / Contratos', value: summary.salary },
|
|
7557
|
+
{ name: 'Benefícios', value: summary.benefits },
|
|
7558
|
+
{ name: 'Encargos', value: summary.taxes },
|
|
7559
|
+
{ name: 'Ferramentas', value: summary.tools },
|
|
7560
|
+
],
|
|
7561
|
+
capacityByDepartment: departments.map((department) => {
|
|
7562
|
+
const departmentRows = rows.filter((row) => row.department === department);
|
|
7563
|
+
const available = departmentRows.reduce((sum, row) => sum + row.availableHours, 0);
|
|
7564
|
+
const allocated = departmentRows.reduce((sum, row) => sum + row.allocatedHours, 0);
|
|
7565
|
+
return {
|
|
7566
|
+
department,
|
|
7567
|
+
Faturável: departmentRows.reduce((sum, row) => sum + row.billableHours, 0),
|
|
7568
|
+
Interno: departmentRows.reduce((sum, row) => sum + row.internalHours, 0),
|
|
7569
|
+
Livre: Math.max(available - allocated, 0),
|
|
7570
|
+
Sobrecarga: Math.max(allocated - available, 0),
|
|
7571
|
+
};
|
|
7572
|
+
}),
|
|
7573
|
+
health: departments.map((department) => {
|
|
7574
|
+
const departmentRows = rows.filter((row) => row.department === department);
|
|
7575
|
+
const value = departmentRows.reduce((sum, row) => sum + row.billableValue, 0);
|
|
7576
|
+
const cost = departmentRows.reduce((sum, row) => sum + row.salaryCost + row.benefitsCost + row.taxesCost + row.toolsCost, 0);
|
|
7577
|
+
const available = departmentRows.reduce((sum, row) => sum + row.availableHours, 0);
|
|
7578
|
+
const allocated = departmentRows.reduce((sum, row) => sum + row.allocatedHours, 0);
|
|
7579
|
+
const billable = departmentRows.reduce((sum, row) => sum + row.billableHours, 0);
|
|
7580
|
+
return {
|
|
7581
|
+
department,
|
|
7582
|
+
margem: Math.max(value ? ((value - cost) / value) * 100 : 0, 0),
|
|
7583
|
+
alocacao: available ? (allocated / available) * 100 : 0,
|
|
7584
|
+
utilizacao: available ? (billable / available) * 100 : 0,
|
|
7585
|
+
saude: Math.max(100 - departmentRows.filter((row) => row.risk === 'alto').length * 12, 45),
|
|
7586
|
+
};
|
|
7587
|
+
}),
|
|
7588
|
+
ranking: rows
|
|
7589
|
+
.map((row) => ({
|
|
7590
|
+
name: row.name.split(' ')[0],
|
|
7591
|
+
Custo: row.salaryCost + row.benefitsCost + row.taxesCost + row.toolsCost,
|
|
7592
|
+
Lucro: row.billableValue - (row.salaryCost + row.benefitsCost + row.taxesCost + row.toolsCost),
|
|
7593
|
+
}))
|
|
7594
|
+
.sort((a, b) => b.Lucro - a.Lucro),
|
|
7595
|
+
planningCards: [
|
|
7596
|
+
{ title: 'Contratar', value: String(summary.overloadCount), description: 'Pessoas em sobrecarga no recorte.' },
|
|
7597
|
+
{ title: 'Realocar', value: Math.round(summary.freeHours).toString(), description: 'Horas livres estimadas no período.' },
|
|
7598
|
+
{ title: 'Reduzir sobrecarga', value: Math.round(summary.overtimeHours).toString(), description: 'Horas acima da capacidade cadastrada.' },
|
|
7599
|
+
{ title: 'Aumentar venda', value: Math.round(summary.utilization).toString(), description: 'Percentual de utilização faturável.' },
|
|
7600
|
+
],
|
|
7601
|
+
rows,
|
|
7602
|
+
};
|
|
7603
|
+
}
|
|
7604
|
+
async deleteCollaboratorCostById(userId, costId) {
|
|
7605
|
+
const actor = await this.getActorContext(userId);
|
|
7606
|
+
this.ensureDirector(actor);
|
|
7607
|
+
const cost = await this.querySingle(`SELECT id, collaborator_id AS "collaboratorId" FROM operations_collaborator_cost WHERE id = $1 LIMIT 1`, [costId]);
|
|
7608
|
+
if (!cost) {
|
|
7609
|
+
throw new common_1.NotFoundException('Collaborator cost not found.');
|
|
7610
|
+
}
|
|
7611
|
+
await this.prisma.$queryRawUnsafe(`DELETE FROM operations_collaborator_cost WHERE id = $1`, costId);
|
|
7612
|
+
return { success: true };
|
|
7613
|
+
}
|
|
7614
|
+
async createCollaboratorCost(userId, collaboratorId, data) {
|
|
7615
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
7616
|
+
const actor = await this.getActorContext(userId);
|
|
7617
|
+
this.ensureDirector(actor);
|
|
7618
|
+
const collaborator = await this.querySingle(`SELECT id FROM operations_collaborator WHERE id = $1 AND deleted_at IS NULL LIMIT 1`, [collaboratorId]);
|
|
7619
|
+
if (!collaborator) {
|
|
7620
|
+
throw new common_1.NotFoundException('Collaborator not found.');
|
|
7621
|
+
}
|
|
7622
|
+
const costType = await this.querySingle(`SELECT id FROM operations_cost_type WHERE id = $1 AND is_active = true LIMIT 1`, [data.costTypeId]);
|
|
7623
|
+
if (!costType) {
|
|
7624
|
+
throw new common_1.BadRequestException('Cost type not found or inactive.');
|
|
7625
|
+
}
|
|
7626
|
+
const created = await this.querySingle(`INSERT INTO operations_collaborator_cost
|
|
7627
|
+
(collaborator_id, cost_type_id, amount, currency, recurrence, allocatable, reference_date, start_date, end_date, depreciation_months, description, notes, created_at, updated_at)
|
|
7628
|
+
VALUES ($1, $2, $3, $4, $5::operations_collaborator_cost_recurrence_56fbe60170_enum, $6, $7::date, $8::date, $9::date, $10, $11, $12, NOW(), NOW())
|
|
7629
|
+
RETURNING id`, [
|
|
7630
|
+
collaboratorId,
|
|
7631
|
+
data.costTypeId,
|
|
7632
|
+
data.amount,
|
|
7633
|
+
(_a = data.currency) !== null && _a !== void 0 ? _a : 'BRL',
|
|
7634
|
+
(_b = data.recurrence) !== null && _b !== void 0 ? _b : 'monthly',
|
|
7635
|
+
(_c = data.allocatable) !== null && _c !== void 0 ? _c : true,
|
|
7636
|
+
(_e = (_d = data.referenceDate) !== null && _d !== void 0 ? _d : data.startDate) !== null && _e !== void 0 ? _e : null,
|
|
7637
|
+
(_f = data.startDate) !== null && _f !== void 0 ? _f : null,
|
|
7638
|
+
(_g = data.endDate) !== null && _g !== void 0 ? _g : null,
|
|
7639
|
+
(_h = data.depreciationMonths) !== null && _h !== void 0 ? _h : null,
|
|
7640
|
+
(_j = data.description) !== null && _j !== void 0 ? _j : null,
|
|
7641
|
+
(_k = data.notes) !== null && _k !== void 0 ? _k : null,
|
|
7642
|
+
]);
|
|
7643
|
+
if (!(created === null || created === void 0 ? void 0 : created.id)) {
|
|
7644
|
+
throw new common_1.BadRequestException('Unable to create collaborator cost.');
|
|
7645
|
+
}
|
|
7646
|
+
const rows = await this.listCollaboratorCosts(userId, collaboratorId);
|
|
7647
|
+
return (_l = rows.find((r) => r.id === created.id)) !== null && _l !== void 0 ? _l : null;
|
|
7648
|
+
}
|
|
7649
|
+
async updateCollaboratorCost(userId, collaboratorId, costId, data) {
|
|
7650
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
7651
|
+
const actor = await this.getActorContext(userId);
|
|
7652
|
+
this.ensureDirector(actor);
|
|
7653
|
+
const cost = await this.querySingle(`SELECT id, collaborator_id AS "collaboratorId" FROM operations_collaborator_cost WHERE id = $1 ${collaboratorId ? 'AND collaborator_id = $2' : ''} LIMIT 1`, collaboratorId ? [costId, collaboratorId] : [costId]);
|
|
7654
|
+
if (!cost) {
|
|
7655
|
+
throw new common_1.NotFoundException('Collaborator cost not found.');
|
|
7656
|
+
}
|
|
7657
|
+
const resolvedCollaboratorId = collaboratorId || cost.collaboratorId;
|
|
7658
|
+
const sets = [];
|
|
7659
|
+
const params = [];
|
|
7660
|
+
if (data.costTypeId !== undefined) {
|
|
7661
|
+
sets.push(`cost_type_id = ${this.param(params, data.costTypeId)}`);
|
|
7662
|
+
}
|
|
7663
|
+
if (data.amount !== undefined) {
|
|
7664
|
+
sets.push(`amount = ${this.param(params, data.amount)}`);
|
|
7665
|
+
}
|
|
7666
|
+
if (data.currency !== undefined) {
|
|
7667
|
+
sets.push(`currency = ${this.param(params, data.currency)}`);
|
|
7668
|
+
}
|
|
7669
|
+
if (data.recurrence !== undefined) {
|
|
7670
|
+
sets.push(`recurrence = ${this.param(params, data.recurrence)}::operations_collaborator_cost_recurrence_56fbe60170_enum`);
|
|
7671
|
+
}
|
|
7672
|
+
if (data.allocatable !== undefined) {
|
|
7673
|
+
sets.push(`allocatable = ${this.param(params, data.allocatable)}`);
|
|
7674
|
+
}
|
|
7675
|
+
if ('referenceDate' in data) {
|
|
7676
|
+
sets.push(`reference_date = ${this.param(params, (_a = data.referenceDate) !== null && _a !== void 0 ? _a : null)}::date`);
|
|
7677
|
+
}
|
|
7678
|
+
if ('startDate' in data) {
|
|
7679
|
+
sets.push(`start_date = ${this.param(params, (_b = data.startDate) !== null && _b !== void 0 ? _b : null)}::date`);
|
|
7680
|
+
}
|
|
7681
|
+
if ('endDate' in data) {
|
|
7682
|
+
sets.push(`end_date = ${this.param(params, (_c = data.endDate) !== null && _c !== void 0 ? _c : null)}::date`);
|
|
7683
|
+
}
|
|
7684
|
+
if ('depreciationMonths' in data) {
|
|
7685
|
+
sets.push(`depreciation_months = ${this.param(params, (_d = data.depreciationMonths) !== null && _d !== void 0 ? _d : null)}`);
|
|
7686
|
+
}
|
|
7687
|
+
if ('description' in data) {
|
|
7688
|
+
sets.push(`description = ${this.param(params, (_e = data.description) !== null && _e !== void 0 ? _e : null)}`);
|
|
7689
|
+
}
|
|
7690
|
+
if ('notes' in data) {
|
|
7691
|
+
sets.push(`notes = ${this.param(params, (_f = data.notes) !== null && _f !== void 0 ? _f : null)}`);
|
|
7692
|
+
}
|
|
7693
|
+
if (sets.length === 0) {
|
|
7694
|
+
const rows = await this.listCollaboratorCosts(userId, resolvedCollaboratorId);
|
|
7695
|
+
return (_g = rows.find((r) => r.id === costId)) !== null && _g !== void 0 ? _g : null;
|
|
7696
|
+
}
|
|
7697
|
+
sets.push(`updated_at = NOW()`);
|
|
7698
|
+
await this.prisma.$queryRawUnsafe(`UPDATE operations_collaborator_cost SET ${sets.join(', ')} WHERE id = ${this.param(params, costId)}`, ...params);
|
|
7699
|
+
const rows = await this.listCollaboratorCosts(userId, resolvedCollaboratorId);
|
|
7700
|
+
return (_h = rows.find((r) => r.id === costId)) !== null && _h !== void 0 ? _h : null;
|
|
7701
|
+
}
|
|
7702
|
+
async deleteCollaboratorCost(userId, collaboratorId, costId) {
|
|
7703
|
+
const actor = await this.getActorContext(userId);
|
|
7704
|
+
this.ensureDirector(actor);
|
|
7705
|
+
const cost = await this.querySingle(`SELECT id FROM operations_collaborator_cost WHERE id = $1 AND collaborator_id = $2 LIMIT 1`, [costId, collaboratorId]);
|
|
7706
|
+
if (!cost) {
|
|
7707
|
+
throw new common_1.NotFoundException('Collaborator cost not found.');
|
|
7708
|
+
}
|
|
7709
|
+
await this.prisma.$queryRawUnsafe(`DELETE FROM operations_collaborator_cost WHERE id = $1`, costId);
|
|
7710
|
+
return { success: true };
|
|
7711
|
+
}
|
|
6475
7712
|
};
|
|
6476
7713
|
exports.OperationsService = OperationsService;
|
|
6477
7714
|
exports.OperationsService = OperationsService = OperationsService_1 = __decorate([
|