@hed-hog/operations 0.0.322 → 0.0.326
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-collaborators.controller.d.ts +14 -0
- package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
- package/dist/controllers/operations-collaborators.controller.js +25 -0
- package/dist/controllers/operations-collaborators.controller.js.map +1 -1
- package/dist/controllers/operations-project-costs.controller.d.ts +422 -0
- package/dist/controllers/operations-project-costs.controller.d.ts.map +1 -0
- package/dist/controllers/operations-project-costs.controller.js +250 -0
- package/dist/controllers/operations-project-costs.controller.js.map +1 -0
- package/dist/controllers/operations-reports.controller.d.ts +9 -0
- package/dist/controllers/operations-reports.controller.d.ts.map +1 -1
- package/dist/controllers/operations-tasks.controller.d.ts +42 -0
- package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
- package/dist/controllers/operations-tasks.controller.js +48 -0
- package/dist/controllers/operations-tasks.controller.js.map +1 -1
- package/dist/controllers/operations-timesheets.controller.d.ts +1 -0
- package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
- package/dist/dto/create-collaborator-project-assignment.dto.d.ts +5 -0
- package/dist/dto/create-collaborator-project-assignment.dto.d.ts.map +1 -0
- package/dist/dto/create-collaborator-project-assignment.dto.js +30 -0
- package/dist/dto/create-collaborator-project-assignment.dto.js.map +1 -0
- package/dist/dto/create-project-cost-category.dto.d.ts +10 -0
- package/dist/dto/create-project-cost-category.dto.d.ts.map +1 -0
- package/dist/dto/create-project-cost-category.dto.js +59 -0
- package/dist/dto/create-project-cost-category.dto.js.map +1 -0
- package/dist/dto/create-project-cost-type.dto.d.ts +14 -0
- package/dist/dto/create-project-cost-type.dto.d.ts.map +1 -0
- package/dist/dto/create-project-cost-type.dto.js +87 -0
- package/dist/dto/create-project-cost-type.dto.js.map +1 -0
- package/dist/dto/create-project-cost.dto.d.ts +22 -0
- package/dist/dto/create-project-cost.dto.d.ts.map +1 -0
- package/dist/dto/create-project-cost.dto.js +135 -0
- package/dist/dto/create-project-cost.dto.js.map +1 -0
- package/dist/dto/get-project-cost-report.dto.d.ts +10 -0
- package/dist/dto/get-project-cost-report.dto.d.ts.map +1 -0
- package/dist/dto/get-project-cost-report.dto.js +65 -0
- package/dist/dto/get-project-cost-report.dto.js.map +1 -0
- package/dist/dto/list-project-cost-categories.dto.d.ts +6 -0
- package/dist/dto/list-project-cost-categories.dto.d.ts.map +1 -0
- package/dist/dto/list-project-cost-categories.dto.js +34 -0
- package/dist/dto/list-project-cost-categories.dto.js.map +1 -0
- package/dist/dto/list-project-cost-types.dto.d.ts +8 -0
- package/dist/dto/list-project-cost-types.dto.d.ts.map +1 -0
- package/dist/dto/list-project-cost-types.dto.js +45 -0
- package/dist/dto/list-project-cost-types.dto.js.map +1 -0
- package/dist/dto/list-project-costs.dto.d.ts +14 -0
- package/dist/dto/list-project-costs.dto.d.ts.map +1 -0
- package/dist/dto/list-project-costs.dto.js +81 -0
- package/dist/dto/list-project-costs.dto.js.map +1 -0
- package/dist/dto/list-tasks.dto.d.ts +1 -0
- package/dist/dto/list-tasks.dto.d.ts.map +1 -1
- package/dist/dto/list-tasks.dto.js +6 -0
- package/dist/dto/list-tasks.dto.js.map +1 -1
- package/dist/dto/list-timesheets.dto.d.ts +1 -0
- package/dist/dto/list-timesheets.dto.d.ts.map +1 -1
- package/dist/dto/list-timesheets.dto.js +7 -0
- package/dist/dto/list-timesheets.dto.js.map +1 -1
- package/dist/dto/update-collaborator-project-assignment.dto.d.ts +11 -0
- package/dist/dto/update-collaborator-project-assignment.dto.d.ts.map +1 -0
- package/dist/dto/update-collaborator-project-assignment.dto.js +65 -0
- package/dist/dto/update-collaborator-project-assignment.dto.js.map +1 -0
- package/dist/dto/update-project-cost-category.dto.d.ts +6 -0
- package/dist/dto/update-project-cost-category.dto.d.ts.map +1 -0
- package/dist/dto/update-project-cost-category.dto.js +9 -0
- package/dist/dto/update-project-cost-category.dto.js.map +1 -0
- package/dist/dto/update-project-cost-type.dto.d.ts +6 -0
- package/dist/dto/update-project-cost-type.dto.d.ts.map +1 -0
- package/dist/dto/update-project-cost-type.dto.js +9 -0
- package/dist/dto/update-project-cost-type.dto.js.map +1 -0
- package/dist/dto/update-project-cost.dto.d.ts +6 -0
- package/dist/dto/update-project-cost.dto.d.ts.map +1 -0
- package/dist/dto/update-project-cost.dto.js +9 -0
- package/dist/dto/update-project-cost.dto.js.map +1 -0
- package/dist/operations.module.d.ts.map +1 -1
- package/dist/operations.module.js +2 -0
- package/dist/operations.module.js.map +1 -1
- package/dist/operations.service.d.ts +571 -1
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +1793 -69
- package/dist/operations.service.js.map +1 -1
- package/hedhog/data/integration_event_catalog.yaml +313 -0
- package/hedhog/data/menu.yaml +52 -0
- package/hedhog/data/operations_project_cost_category.yaml +80 -0
- package/hedhog/data/operations_project_cost_type.yaml +503 -0
- package/hedhog/data/route.yaml +274 -0
- package/hedhog/data/setting_group.yaml +21 -0
- package/hedhog/frontend/app/_components/collaborator-costs-section.tsx.ejs +2 -18
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +593 -297
- package/hedhog/frontend/app/_components/collaborator-tasks-tab.tsx.ejs +358 -0
- package/hedhog/frontend/app/_components/collaborator-timesheets-tab.tsx.ejs +242 -0
- package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +533 -296
- package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +1 -853
- package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +450 -0
- package/hedhog/frontend/app/_components/project-cost-report-screen.tsx.ejs +602 -0
- package/hedhog/frontend/app/_components/project-costs-section.tsx.ejs +1401 -0
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +2248 -2063
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +56 -11
- package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +454 -96
- package/hedhog/frontend/app/_components/task-form-sheet.tsx.ejs +784 -0
- package/hedhog/frontend/app/_lib/api.ts.ejs +256 -0
- package/hedhog/frontend/app/_lib/hooks/use-mention-items.ts.ejs +28 -0
- package/hedhog/frontend/app/_lib/types.ts.ejs +190 -0
- package/hedhog/frontend/app/_lib/utils/format.ts.ejs +9 -3
- package/hedhog/frontend/app/collaborators/page.tsx.ejs +18 -7
- package/hedhog/frontend/app/my-tasks/page.tsx.ejs +536 -328
- package/hedhog/frontend/app/project-cost-categories/page.tsx.ejs +674 -0
- package/hedhog/frontend/app/project-cost-types/page.tsx.ejs +845 -0
- package/hedhog/frontend/app/projects/[id]/costs-report/page.tsx.ejs +10 -0
- package/hedhog/frontend/app/reports/collaborators/page.tsx.ejs +20 -349
- package/hedhog/frontend/app/reports/projects/page.tsx.ejs +217 -485
- package/hedhog/frontend/messages/en.json +257 -5
- package/hedhog/frontend/messages/en.json.ejs +2060 -0
- package/hedhog/frontend/messages/operations/en.json +2068 -0
- package/hedhog/frontend/messages/operations/operations/en.json +2102 -0
- package/hedhog/frontend/messages/operations/operations/pt.json +2111 -0
- package/hedhog/frontend/messages/operations/pt.json +2072 -0
- package/hedhog/frontend/messages/pt.json +256 -4
- package/hedhog/frontend/messages/pt.json.ejs +2067 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.d.ts +29 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.js +95 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.tsx +233 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.d.ts +10 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.js +577 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.tsx +868 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.d.ts +4 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.js +337 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.tsx +476 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.d.ts +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.js +1348 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.tsx +2233 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.d.ts +12 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.js +162 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.tsx +261 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.d.ts +18 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.js +145 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.tsx +258 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.d.ts +4 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.js +223 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.tsx +342 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.d.ts +58 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.js +438 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.tsx +698 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.d.ts +20 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.js +233 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.tsx +392 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.d.ts +4 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.js +814 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.tsx +1288 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.d.ts +21 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.js +174 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.tsx +306 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.d.ts +10 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.js +12 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.tsx +29 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.d.ts +15 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.js +501 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.tsx +853 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.js +847 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.tsx +1340 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.d.ts +4 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.js +2930 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.tsx +4378 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.d.ts +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.js +1013 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.tsx +1745 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.d.ts +13 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.js +38 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.tsx +74 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.d.ts +7 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.js +11 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.tsx +15 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.d.ts +18 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.js +406 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.tsx +660 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.d.ts +26 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.js +332 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.tsx +518 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.js +255 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.tsx +388 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.d.ts +10 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.js +131 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.tsx +214 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.d.ts +108 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.js +162 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.ts +428 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.d.ts +8 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.js +36 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.ts +44 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.d.ts +837 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.js +3 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.ts +861 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.d.ts +16 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.js +182 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.ts +250 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.d.ts +4 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.js +51 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.ts +61 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.js +954 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.tsx +1277 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.js +488 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.tsx +805 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.js +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.tsx +11 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.js +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.tsx +11 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.js +8 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.tsx +5 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.js +612 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.tsx +939 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.js +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.tsx +11 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.js +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.tsx +11 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.js +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.tsx +17 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.js +348 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.tsx +536 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.js +401 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.tsx +607 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.d.ts +5 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.js +7 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.tsx +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.js +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.tsx +11 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.js +321 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.tsx +440 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.js +939 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.tsx +1499 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.d.ts +29 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.js +95 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.tsx +233 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.d.ts +10 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.js +577 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.tsx +868 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.d.ts +4 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.js +337 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.tsx +476 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.d.ts +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.js +1348 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.tsx +2233 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.d.ts +12 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.js +162 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.tsx +261 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.d.ts +18 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.js +145 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.tsx +258 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.d.ts +4 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.js +223 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.tsx +342 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.d.ts +58 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.js +438 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.tsx +698 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.d.ts +20 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.js +233 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.tsx +392 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.d.ts +4 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.js +814 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.tsx +1288 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.d.ts +21 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.js +174 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.tsx +306 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.d.ts +10 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.js +12 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.tsx +29 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.d.ts +15 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.js +501 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.tsx +853 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.js +459 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.tsx +598 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.js +876 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.tsx +1368 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.d.ts +4 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.js +2930 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.tsx +4378 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.d.ts +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.js +1013 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.tsx +1745 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.d.ts +13 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.js +38 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.tsx +74 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.d.ts +7 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.js +11 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.tsx +15 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.d.ts +18 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.js +406 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.tsx +660 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.d.ts +26 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.js +332 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.tsx +518 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.js +255 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.tsx +388 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.d.ts +10 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.js +131 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.tsx +214 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.d.ts +108 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.js +162 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.ts +428 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.d.ts +8 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.js +36 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.ts +44 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.d.ts +837 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.js +3 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.ts +861 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.d.ts +16 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.js +182 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.ts +250 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.d.ts +4 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.js +51 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.ts +61 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.js +954 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.tsx +1277 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.js +488 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.tsx +805 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.js +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.tsx +11 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.js +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.tsx +11 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.js +8 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.tsx +5 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.js +612 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.tsx +939 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.js +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.tsx +11 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.js +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.tsx +11 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.js +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.tsx +17 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.js +348 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.tsx +536 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.js +401 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.tsx +607 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.d.ts +5 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.js +7 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.tsx +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.js +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.tsx +11 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.js +321 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.tsx +440 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.js +939 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.tsx +1499 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.js +8 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.tsx +5 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.js +436 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.tsx +675 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.js +563 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.tsx +846 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.js +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.tsx +10 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.js +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.tsx +11 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.js +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.tsx +11 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.js +8 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.tsx +5 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.js +492 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.tsx +757 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.js +342 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.tsx +430 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.js +338 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.tsx +428 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.js +660 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.tsx +992 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.js +515 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.tsx +707 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.js +1141 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.tsx +1705 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.js +8 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.tsx +5 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.js +436 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.tsx +675 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.js +563 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.tsx +846 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.js +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.tsx +11 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.d.ts +6 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.js +9 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.tsx +11 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.js +8 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.tsx +5 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.js +492 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.tsx +757 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.js +342 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.tsx +430 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.js +338 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.tsx +428 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.js +660 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.tsx +992 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.js +515 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.tsx +707 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.d.ts +2 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.d.ts.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.js +1141 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.js.map +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.tsx +1705 -0
- package/hedhog/table/operations_collaborator.yaml +5 -0
- package/hedhog/table/operations_collaborator_compensation_history.yaml +4 -0
- package/hedhog/table/operations_project_assignment.yaml +1 -0
- package/hedhog/table/operations_project_cost.yaml +93 -0
- package/hedhog/table/operations_project_cost_category.yaml +37 -0
- package/hedhog/table/operations_project_cost_type.yaml +55 -0
- package/hedhog/table/operations_task_comment.yaml +26 -0
- package/package.json +6 -6
- package/src/controllers/operations-collaborators.controller.ts +26 -0
- package/src/controllers/operations-project-costs.controller.ts +249 -0
- package/src/controllers/operations-tasks.controller.ts +49 -0
- package/src/dto/create-collaborator-project-assignment.dto.ts +14 -0
- package/src/dto/create-project-cost-category.dto.ts +37 -0
- package/src/dto/create-project-cost-type.dto.ts +64 -0
- package/src/dto/create-project-cost.dto.ts +126 -0
- package/src/dto/get-project-cost-report.dto.ts +46 -0
- package/src/dto/list-project-cost-categories.dto.ts +17 -0
- package/src/dto/list-project-cost-types.dto.ts +28 -0
- package/src/dto/list-project-costs.dto.ts +59 -0
- package/src/dto/list-tasks.dto.ts +7 -0
- package/src/dto/list-timesheets.dto.ts +7 -1
- package/src/dto/update-collaborator-project-assignment.dto.ts +58 -0
- package/src/dto/update-project-cost-category.dto.ts +4 -0
- package/src/dto/update-project-cost-type.dto.ts +4 -0
- package/src/dto/update-project-cost.dto.ts +4 -0
- package/src/operations.module.ts +2 -0
- package/src/operations.service.ts +2472 -61
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from '@hed-hog/core';
|
|
10
10
|
import {
|
|
11
11
|
BadRequestException,
|
|
12
|
+
ConflictException,
|
|
12
13
|
ForbiddenException,
|
|
13
14
|
Inject,
|
|
14
15
|
Injectable,
|
|
@@ -142,6 +143,9 @@ type CollaboratorPayload = {
|
|
|
142
143
|
joinedAt?: string | null;
|
|
143
144
|
leftAt?: string | null;
|
|
144
145
|
compensationAmount?: number | null;
|
|
146
|
+
hourlyRate?: number | null;
|
|
147
|
+
compensationEffectiveDate?: string | null;
|
|
148
|
+
compensationNotes?: string | null;
|
|
145
149
|
contractDescription?: string | null;
|
|
146
150
|
autoGenerateContractDraft?: boolean;
|
|
147
151
|
weeklySchedule?: Array<{
|
|
@@ -544,6 +548,18 @@ type TaskPayload = {
|
|
|
544
548
|
archived?: boolean;
|
|
545
549
|
};
|
|
546
550
|
|
|
551
|
+
type TaskCommentRecord = {
|
|
552
|
+
id: number;
|
|
553
|
+
taskId: number;
|
|
554
|
+
content: string;
|
|
555
|
+
actorCollaboratorId: number | null;
|
|
556
|
+
actorName: string | null;
|
|
557
|
+
actorUserPhotoId: number | null;
|
|
558
|
+
actorPersonAvatarId: number | null;
|
|
559
|
+
createdAt: string;
|
|
560
|
+
updatedAt: string | null;
|
|
561
|
+
};
|
|
562
|
+
|
|
547
563
|
type QuickTimesheetEntryPayload = {
|
|
548
564
|
projectId?: number | null;
|
|
549
565
|
projectAssignmentId?: number | null;
|
|
@@ -1967,14 +1983,42 @@ export class OperationsService {
|
|
|
1967
1983
|
createdCollaboratorId,
|
|
1968
1984
|
Number(data.compensationAmount),
|
|
1969
1985
|
actor.userId,
|
|
1970
|
-
null
|
|
1986
|
+
data.compensationNotes ?? null,
|
|
1987
|
+
data.compensationEffectiveDate ?? null
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
if (data.hourlyRate != null) {
|
|
1992
|
+
await (tx as any).$executeRawUnsafe(
|
|
1993
|
+
`UPDATE operations_collaborator SET hourly_rate = $1 WHERE id = $2`,
|
|
1994
|
+
Number(data.hourlyRate),
|
|
1995
|
+
createdCollaboratorId
|
|
1996
|
+
);
|
|
1997
|
+
await this.insertCollaboratorCompensationHistory(
|
|
1998
|
+
tx as any,
|
|
1999
|
+
createdCollaboratorId,
|
|
2000
|
+
Number(data.hourlyRate),
|
|
2001
|
+
actor.userId,
|
|
2002
|
+
data.compensationNotes ?? null,
|
|
2003
|
+
data.compensationEffectiveDate ?? null,
|
|
2004
|
+
'hourly_rate'
|
|
1971
2005
|
);
|
|
1972
2006
|
}
|
|
1973
2007
|
|
|
1974
2008
|
return createdCollaboratorId;
|
|
1975
2009
|
});
|
|
1976
2010
|
|
|
1977
|
-
|
|
2011
|
+
const result = await this.getCollaboratorByIdForUser(userId, collaboratorId);
|
|
2012
|
+
|
|
2013
|
+
await this.integrationApi.publishEvent({
|
|
2014
|
+
eventName: 'operations.collaborator.created',
|
|
2015
|
+
sourceModule: 'operations',
|
|
2016
|
+
aggregateType: 'collaborator',
|
|
2017
|
+
aggregateId: String(collaboratorId),
|
|
2018
|
+
payload: { id: collaboratorId, displayName: resolvedDisplayName, status: normalizedStatus },
|
|
2019
|
+
}).catch(() => null);
|
|
2020
|
+
|
|
2021
|
+
return result;
|
|
1978
2022
|
}
|
|
1979
2023
|
|
|
1980
2024
|
async updateCollaborator(
|
|
@@ -2010,6 +2054,7 @@ export class OperationsService {
|
|
|
2010
2054
|
}
|
|
2011
2055
|
this.pushUpdate(updates, params, 'level_label', data.levelLabel);
|
|
2012
2056
|
this.pushUpdate(updates, params, 'weekly_capacity_hours', data.weeklyCapacityHours);
|
|
2057
|
+
this.pushUpdate(updates, params, 'hourly_rate', data.hourlyRate);
|
|
2013
2058
|
this.pushUpdate(
|
|
2014
2059
|
updates,
|
|
2015
2060
|
params,
|
|
@@ -2021,7 +2066,18 @@ export class OperationsService {
|
|
|
2021
2066
|
this.pushUpdate(updates, params, 'left_at', data.leftAt, 'date');
|
|
2022
2067
|
this.pushUpdate(updates, params, 'notes', data.notes);
|
|
2023
2068
|
|
|
2069
|
+
let currentHourlyRate: number | null = null;
|
|
2070
|
+
|
|
2024
2071
|
await this.prisma.$transaction(async (tx) => {
|
|
2072
|
+
if (data.hourlyRate !== undefined && data.hourlyRate !== null) {
|
|
2073
|
+
const curr = (await (tx as any).$queryRawUnsafe(
|
|
2074
|
+
`SELECT hourly_rate AS "hourlyRate" FROM operations_collaborator WHERE id = $1`,
|
|
2075
|
+
collaboratorId
|
|
2076
|
+
)) as { hourlyRate: string | null }[];
|
|
2077
|
+
currentHourlyRate =
|
|
2078
|
+
curr[0]?.hourlyRate != null ? Number(curr[0].hourlyRate) : null;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2025
2081
|
if (
|
|
2026
2082
|
data.collaboratorType !== undefined ||
|
|
2027
2083
|
data.collaboratorTypeId !== undefined ||
|
|
@@ -2114,6 +2170,7 @@ export class OperationsService {
|
|
|
2114
2170
|
data.compensationAmount !== undefined ||
|
|
2115
2171
|
data.contractDescription !== undefined ||
|
|
2116
2172
|
data.autoGenerateContractDraft !== undefined ||
|
|
2173
|
+
data.hourlyRate !== undefined ||
|
|
2117
2174
|
data.joinedAt !== undefined ||
|
|
2118
2175
|
data.weeklyCapacityHours !== undefined ||
|
|
2119
2176
|
data.supervisorCollaboratorId !== undefined ||
|
|
@@ -2124,6 +2181,29 @@ export class OperationsService {
|
|
|
2124
2181
|
data.personId !== undefined ||
|
|
2125
2182
|
data.displayName !== undefined
|
|
2126
2183
|
) {
|
|
2184
|
+
let currentBudgetAmount: number | null = null;
|
|
2185
|
+
|
|
2186
|
+
if (
|
|
2187
|
+
data.compensationAmount !== undefined &&
|
|
2188
|
+
data.compensationAmount !== null
|
|
2189
|
+
) {
|
|
2190
|
+
const hiringContracts = (await (tx as any).$queryRawUnsafe(
|
|
2191
|
+
`SELECT budget_amount AS "budgetAmount"
|
|
2192
|
+
FROM operations_contract
|
|
2193
|
+
WHERE related_collaborator_id = $1
|
|
2194
|
+
AND origin_type = 'employee_hiring'
|
|
2195
|
+
AND deleted_at IS NULL
|
|
2196
|
+
ORDER BY created_at DESC
|
|
2197
|
+
LIMIT 1`,
|
|
2198
|
+
collaboratorId
|
|
2199
|
+
)) as { budgetAmount: string | null }[];
|
|
2200
|
+
|
|
2201
|
+
currentBudgetAmount =
|
|
2202
|
+
hiringContracts[0]?.budgetAmount != null
|
|
2203
|
+
? Number(hiringContracts[0].budgetAmount)
|
|
2204
|
+
: null;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2127
2207
|
await this.syncHiringContractDraft(
|
|
2128
2208
|
tx as any,
|
|
2129
2209
|
actor.userId,
|
|
@@ -2135,18 +2215,140 @@ export class OperationsService {
|
|
|
2135
2215
|
data.compensationAmount !== undefined &&
|
|
2136
2216
|
data.compensationAmount !== null
|
|
2137
2217
|
) {
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2218
|
+
const newAmount = Number(data.compensationAmount);
|
|
2219
|
+
|
|
2220
|
+
if (
|
|
2221
|
+
currentBudgetAmount === null ||
|
|
2222
|
+
newAmount !== currentBudgetAmount
|
|
2223
|
+
) {
|
|
2224
|
+
await this.insertCollaboratorCompensationHistory(
|
|
2225
|
+
tx as any,
|
|
2226
|
+
collaboratorId,
|
|
2227
|
+
newAmount,
|
|
2228
|
+
actor.userId,
|
|
2229
|
+
data.compensationNotes ?? null,
|
|
2230
|
+
data.compensationEffectiveDate ?? null
|
|
2231
|
+
);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
if (data.hourlyRate !== undefined && data.hourlyRate !== null) {
|
|
2236
|
+
const newRate = Number(data.hourlyRate);
|
|
2237
|
+
if (currentHourlyRate === null || newRate !== currentHourlyRate) {
|
|
2238
|
+
await this.insertCollaboratorCompensationHistory(
|
|
2239
|
+
tx as any,
|
|
2240
|
+
collaboratorId,
|
|
2241
|
+
newRate,
|
|
2242
|
+
actor.userId,
|
|
2243
|
+
data.compensationNotes ?? null,
|
|
2244
|
+
data.compensationEffectiveDate ?? null,
|
|
2245
|
+
'hourly_rate'
|
|
2246
|
+
);
|
|
2247
|
+
}
|
|
2145
2248
|
}
|
|
2146
2249
|
}
|
|
2147
2250
|
});
|
|
2148
2251
|
|
|
2149
|
-
|
|
2252
|
+
const collaboratorResult = await this.getCollaboratorByIdForUser(userId, collaboratorId);
|
|
2253
|
+
|
|
2254
|
+
await this.integrationApi.publishEvent({
|
|
2255
|
+
eventName: 'operations.collaborator.updated',
|
|
2256
|
+
sourceModule: 'operations',
|
|
2257
|
+
aggregateType: 'collaborator',
|
|
2258
|
+
aggregateId: String(collaboratorId),
|
|
2259
|
+
payload: { id: collaboratorId, displayName: data.displayName, status: data.status },
|
|
2260
|
+
}).catch(() => null);
|
|
2261
|
+
|
|
2262
|
+
return collaboratorResult;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
async updateCollaboratorProjectAssignment(
|
|
2266
|
+
collaboratorId: number,
|
|
2267
|
+
projectId: number,
|
|
2268
|
+
data: {
|
|
2269
|
+
projectRoleId?: number | null;
|
|
2270
|
+
roleLabel?: string | null;
|
|
2271
|
+
allocationPercent?: number | null;
|
|
2272
|
+
weeklyHours?: number | null;
|
|
2273
|
+
startDate?: string | null;
|
|
2274
|
+
endDate?: string | null;
|
|
2275
|
+
status?: string;
|
|
2276
|
+
}
|
|
2277
|
+
) {
|
|
2278
|
+
const sets: string[] = [];
|
|
2279
|
+
const params: unknown[] = [collaboratorId, projectId];
|
|
2280
|
+
let idx = 3;
|
|
2281
|
+
|
|
2282
|
+
if ('projectRoleId' in data) {
|
|
2283
|
+
sets.push(`project_role_id = $${idx++}`);
|
|
2284
|
+
params.push(data.projectRoleId ?? null);
|
|
2285
|
+
}
|
|
2286
|
+
if ('roleLabel' in data) {
|
|
2287
|
+
sets.push(`role_label = $${idx++}`);
|
|
2288
|
+
params.push(data.roleLabel ?? null);
|
|
2289
|
+
}
|
|
2290
|
+
if ('allocationPercent' in data) {
|
|
2291
|
+
sets.push(`allocation_percent = $${idx++}`);
|
|
2292
|
+
params.push(data.allocationPercent ?? null);
|
|
2293
|
+
}
|
|
2294
|
+
if ('weeklyHours' in data) {
|
|
2295
|
+
sets.push(`weekly_hours = $${idx++}`);
|
|
2296
|
+
params.push(data.weeklyHours ?? null);
|
|
2297
|
+
}
|
|
2298
|
+
if ('startDate' in data) {
|
|
2299
|
+
sets.push(`start_date = $${idx++}::date`);
|
|
2300
|
+
params.push(data.startDate ?? null);
|
|
2301
|
+
}
|
|
2302
|
+
if ('endDate' in data) {
|
|
2303
|
+
sets.push(`end_date = $${idx++}::date`);
|
|
2304
|
+
params.push(data.endDate ?? null);
|
|
2305
|
+
}
|
|
2306
|
+
if ('status' in data) {
|
|
2307
|
+
sets.push(
|
|
2308
|
+
`status = $${idx++}::operations_project_assignment_status_155b459bbf_enum`
|
|
2309
|
+
);
|
|
2310
|
+
params.push(data.status);
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
if (!sets.length) return { updated: false };
|
|
2314
|
+
|
|
2315
|
+
sets.push(`updated_at = NOW()`);
|
|
2316
|
+
|
|
2317
|
+
await this.prisma.$executeRawUnsafe(
|
|
2318
|
+
`UPDATE operations_project_assignment
|
|
2319
|
+
SET ${sets.join(', ')}
|
|
2320
|
+
WHERE collaborator_id = $1
|
|
2321
|
+
AND project_id = $2
|
|
2322
|
+
AND deleted_at IS NULL`,
|
|
2323
|
+
...params
|
|
2324
|
+
);
|
|
2325
|
+
|
|
2326
|
+
return { updated: true };
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
async addCollaboratorProjectAssignment(
|
|
2330
|
+
collaboratorId: number,
|
|
2331
|
+
data: { projectId: number; roleLabel?: string }
|
|
2332
|
+
) {
|
|
2333
|
+
const existing = await this.querySingle<{ id: number }>(
|
|
2334
|
+
`SELECT id FROM operations_project_assignment
|
|
2335
|
+
WHERE collaborator_id = $1 AND project_id = $2 AND deleted_at IS NULL`,
|
|
2336
|
+
[collaboratorId, data.projectId]
|
|
2337
|
+
);
|
|
2338
|
+
|
|
2339
|
+
if (existing) {
|
|
2340
|
+
return { id: existing.id, created: false };
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
const row = await this.querySingle<{ id: number }>(
|
|
2344
|
+
`INSERT INTO operations_project_assignment
|
|
2345
|
+
(collaborator_id, project_id, role_label, status)
|
|
2346
|
+
VALUES ($1, $2, $3, 'active')
|
|
2347
|
+
RETURNING id`,
|
|
2348
|
+
[collaboratorId, data.projectId, data.roleLabel ?? '']
|
|
2349
|
+
);
|
|
2350
|
+
|
|
2351
|
+
return { id: row!.id, created: true };
|
|
2150
2352
|
}
|
|
2151
2353
|
|
|
2152
2354
|
async getCollaboratorCompensationHistory(
|
|
@@ -2165,6 +2367,7 @@ export class OperationsService {
|
|
|
2165
2367
|
actorUserId: number | null;
|
|
2166
2368
|
actorName: string | null;
|
|
2167
2369
|
notes: string | null;
|
|
2370
|
+
amountType: string;
|
|
2168
2371
|
createdAt: string;
|
|
2169
2372
|
}>(
|
|
2170
2373
|
`SELECT h.id,
|
|
@@ -2174,6 +2377,7 @@ export class OperationsService {
|
|
|
2174
2377
|
h.actor_user_id AS "actorUserId",
|
|
2175
2378
|
u.name AS "actorName",
|
|
2176
2379
|
h.notes,
|
|
2380
|
+
h.amount_type AS "amountType",
|
|
2177
2381
|
h.created_at AS "createdAt"
|
|
2178
2382
|
FROM operations_collaborator_compensation_history h
|
|
2179
2383
|
LEFT JOIN "user" u ON u.id = h.actor_user_id
|
|
@@ -2755,6 +2959,7 @@ export class OperationsService {
|
|
|
2755
2959
|
status?: string;
|
|
2756
2960
|
myOnly?: boolean;
|
|
2757
2961
|
archived?: boolean;
|
|
2962
|
+
collaboratorId?: number;
|
|
2758
2963
|
}
|
|
2759
2964
|
) {
|
|
2760
2965
|
const actor = await this.getActorContext(userId);
|
|
@@ -2828,6 +3033,13 @@ export class OperationsService {
|
|
|
2828
3033
|
filters.push(`t.status::text = ${this.param(params, paginationParams.status)}`);
|
|
2829
3034
|
}
|
|
2830
3035
|
|
|
3036
|
+
if (paginationParams.collaboratorId) {
|
|
3037
|
+
const colId = paginationParams.collaboratorId;
|
|
3038
|
+
filters.push(
|
|
3039
|
+
`(pa.collaborator_id = ${this.param(params, colId)} OR t.assignee_collaborator_id = ${this.param(params, colId)})`
|
|
3040
|
+
);
|
|
3041
|
+
}
|
|
3042
|
+
|
|
2831
3043
|
const whereClause = filters.join(' AND ');
|
|
2832
3044
|
const totalRow = await this.querySingle<{ total: string }>(
|
|
2833
3045
|
`SELECT COUNT(*)::text AS total
|
|
@@ -2868,6 +3080,8 @@ export class OperationsService {
|
|
|
2868
3080
|
assigneeName: string | null;
|
|
2869
3081
|
assigneeUserPhotoId: number | null;
|
|
2870
3082
|
assigneePersonAvatarId: number | null;
|
|
3083
|
+
commentCount: number;
|
|
3084
|
+
fileCount: number;
|
|
2871
3085
|
createdAt: string;
|
|
2872
3086
|
deletedAt: string | null;
|
|
2873
3087
|
}>(
|
|
@@ -2886,6 +3100,8 @@ export class OperationsService {
|
|
|
2886
3100
|
ac.display_name AS "assigneeName",
|
|
2887
3101
|
au.photo_id AS "assigneeUserPhotoId",
|
|
2888
3102
|
ap.avatar_id AS "assigneePersonAvatarId",
|
|
3103
|
+
COALESCE(task_comments.count, 0)::int AS "commentCount",
|
|
3104
|
+
COALESCE(task_files.count, 0)::int AS "fileCount",
|
|
2889
3105
|
t.created_at AS "createdAt",
|
|
2890
3106
|
t.deleted_at AS "deletedAt"
|
|
2891
3107
|
FROM operations_task t
|
|
@@ -2898,6 +3114,16 @@ export class OperationsService {
|
|
|
2898
3114
|
ON au.id = ac.user_id
|
|
2899
3115
|
LEFT JOIN person ap
|
|
2900
3116
|
ON ap.id = ac.person_id
|
|
3117
|
+
LEFT JOIN LATERAL (
|
|
3118
|
+
SELECT COUNT(*) AS count
|
|
3119
|
+
FROM operations_task_comment tc
|
|
3120
|
+
WHERE tc.task_id = t.id
|
|
3121
|
+
) task_comments ON TRUE
|
|
3122
|
+
LEFT JOIN LATERAL (
|
|
3123
|
+
SELECT COUNT(*) AS count
|
|
3124
|
+
FROM operations_task_file tf
|
|
3125
|
+
WHERE tf.operations_task_id = t.id
|
|
3126
|
+
) task_files ON TRUE
|
|
2901
3127
|
JOIN operations_project p
|
|
2902
3128
|
ON p.id = COALESCE(t.project_id, pa.project_id)
|
|
2903
3129
|
WHERE ${whereClause}
|
|
@@ -3015,14 +3241,20 @@ export class OperationsService {
|
|
|
3015
3241
|
]
|
|
3016
3242
|
);
|
|
3017
3243
|
|
|
3018
|
-
|
|
3244
|
+
const task = await this.getProjectBoardTask(created?.id ?? 0);
|
|
3245
|
+
|
|
3246
|
+
await this.integrationApi.publishEvent({
|
|
3247
|
+
eventName: 'operations.task.created',
|
|
3248
|
+
sourceModule: 'operations',
|
|
3249
|
+
aggregateType: 'task',
|
|
3250
|
+
aggregateId: String(created?.id ?? 0),
|
|
3251
|
+
payload: { id: created?.id, projectId, name, status: data.status ?? 'todo', priority: data.priority ?? 'medium' },
|
|
3252
|
+
}).catch(() => null);
|
|
3253
|
+
|
|
3254
|
+
return task;
|
|
3019
3255
|
}
|
|
3020
3256
|
|
|
3021
|
-
async updateTask(
|
|
3022
|
-
userId: number,
|
|
3023
|
-
taskId: number,
|
|
3024
|
-
data: Partial<TaskPayload>
|
|
3025
|
-
) {
|
|
3257
|
+
async updateTask(userId: number, taskId: number, data: TaskPayload) {
|
|
3026
3258
|
const actor = await this.getActorContext(userId);
|
|
3027
3259
|
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
3028
3260
|
throw new ForbiddenException(
|
|
@@ -3113,7 +3345,17 @@ export class OperationsService {
|
|
|
3113
3345
|
);
|
|
3114
3346
|
});
|
|
3115
3347
|
|
|
3116
|
-
|
|
3348
|
+
const taskResult = await this.getProjectBoardTask(taskId);
|
|
3349
|
+
|
|
3350
|
+
await this.integrationApi.publishEvent({
|
|
3351
|
+
eventName: 'operations.task.updated',
|
|
3352
|
+
sourceModule: 'operations',
|
|
3353
|
+
aggregateType: 'task',
|
|
3354
|
+
aggregateId: String(taskId),
|
|
3355
|
+
payload: { id: taskId, name: data.name, status: data.status },
|
|
3356
|
+
}).catch(() => null);
|
|
3357
|
+
|
|
3358
|
+
return taskResult;
|
|
3117
3359
|
}
|
|
3118
3360
|
|
|
3119
3361
|
async removeTask(userId: number, taskId: number, permanent = false) {
|
|
@@ -3151,6 +3393,14 @@ export class OperationsService {
|
|
|
3151
3393
|
);
|
|
3152
3394
|
});
|
|
3153
3395
|
|
|
3396
|
+
await this.integrationApi.publishEvent({
|
|
3397
|
+
eventName: 'operations.task.deleted',
|
|
3398
|
+
sourceModule: 'operations',
|
|
3399
|
+
aggregateType: 'task',
|
|
3400
|
+
aggregateId: String(taskId),
|
|
3401
|
+
payload: { id: taskId, projectId: current.projectId, permanent },
|
|
3402
|
+
}).catch(() => null);
|
|
3403
|
+
|
|
3154
3404
|
return { success: true };
|
|
3155
3405
|
}
|
|
3156
3406
|
|
|
@@ -3262,6 +3512,205 @@ export class OperationsService {
|
|
|
3262
3512
|
return { success: true };
|
|
3263
3513
|
}
|
|
3264
3514
|
|
|
3515
|
+
async listTaskComments(userId: number, taskId: number) {
|
|
3516
|
+
const actor = await this.getActorContext(userId);
|
|
3517
|
+
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
3518
|
+
throw new ForbiddenException(
|
|
3519
|
+
'Operations collaborator access is required.'
|
|
3520
|
+
);
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
const current = await this.getTaskRecordForActor(
|
|
3524
|
+
this.prisma,
|
|
3525
|
+
actor,
|
|
3526
|
+
taskId
|
|
3527
|
+
);
|
|
3528
|
+
await this.assertProjectAccess(actor, current.projectId);
|
|
3529
|
+
|
|
3530
|
+
return this.queryRows<TaskCommentRecord>(
|
|
3531
|
+
`SELECT tc.id,
|
|
3532
|
+
tc.task_id AS "taskId",
|
|
3533
|
+
tc.content,
|
|
3534
|
+
tc.actor_collaborator_id AS "actorCollaboratorId",
|
|
3535
|
+
actor.display_name AS "actorName",
|
|
3536
|
+
actor_user.photo_id AS "actorUserPhotoId",
|
|
3537
|
+
actor_person.avatar_id AS "actorPersonAvatarId",
|
|
3538
|
+
tc.created_at AS "createdAt",
|
|
3539
|
+
tc.updated_at AS "updatedAt"
|
|
3540
|
+
FROM operations_task_comment tc
|
|
3541
|
+
LEFT JOIN operations_collaborator actor
|
|
3542
|
+
ON actor.id = tc.actor_collaborator_id
|
|
3543
|
+
AND actor.deleted_at IS NULL
|
|
3544
|
+
LEFT JOIN "user" actor_user
|
|
3545
|
+
ON actor_user.id = actor.user_id
|
|
3546
|
+
LEFT JOIN person actor_person
|
|
3547
|
+
ON actor_person.id = actor.person_id
|
|
3548
|
+
WHERE tc.task_id = $1
|
|
3549
|
+
ORDER BY tc.created_at ASC, tc.id ASC`,
|
|
3550
|
+
[taskId]
|
|
3551
|
+
);
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
async addTaskComment(userId: number, taskId: number, content: string) {
|
|
3555
|
+
const actor = await this.getActorContext(userId);
|
|
3556
|
+
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
3557
|
+
throw new ForbiddenException(
|
|
3558
|
+
'Operations collaborator access is required.'
|
|
3559
|
+
);
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
const current = await this.getTaskRecordForActor(
|
|
3563
|
+
this.prisma,
|
|
3564
|
+
actor,
|
|
3565
|
+
taskId
|
|
3566
|
+
);
|
|
3567
|
+
await this.assertProjectAccess(actor, current.projectId);
|
|
3568
|
+
|
|
3569
|
+
const normalizedContent = this.normalizeOptionalText(content);
|
|
3570
|
+
if (!normalizedContent) {
|
|
3571
|
+
throw new BadRequestException('Comment content is required.');
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
const inserted = await this.queryRows<{ id: number }>(
|
|
3575
|
+
`INSERT INTO operations_task_comment (
|
|
3576
|
+
task_id,
|
|
3577
|
+
actor_collaborator_id,
|
|
3578
|
+
content,
|
|
3579
|
+
created_at,
|
|
3580
|
+
updated_at
|
|
3581
|
+
) VALUES ($1, $2, $3, NOW(), NOW())
|
|
3582
|
+
RETURNING id`,
|
|
3583
|
+
[taskId, actor.collaboratorId ?? null, normalizedContent]
|
|
3584
|
+
);
|
|
3585
|
+
|
|
3586
|
+
const commentId = inserted[0]?.id;
|
|
3587
|
+
const comments = await this.listTaskComments(userId, taskId);
|
|
3588
|
+
const createdComment = comments.find((comment) => comment.id === commentId);
|
|
3589
|
+
|
|
3590
|
+
if (!createdComment) {
|
|
3591
|
+
throw new NotFoundException('Task comment could not be loaded.');
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
return createdComment;
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
async updateTaskComment(
|
|
3598
|
+
userId: number,
|
|
3599
|
+
taskId: number,
|
|
3600
|
+
commentId: number,
|
|
3601
|
+
content: string
|
|
3602
|
+
) {
|
|
3603
|
+
const actor = await this.getActorContext(userId);
|
|
3604
|
+
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
3605
|
+
throw new ForbiddenException(
|
|
3606
|
+
'Operations collaborator access is required.'
|
|
3607
|
+
);
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
const current = await this.getTaskRecordForActor(
|
|
3611
|
+
this.prisma,
|
|
3612
|
+
actor,
|
|
3613
|
+
taskId
|
|
3614
|
+
);
|
|
3615
|
+
await this.assertProjectAccess(actor, current.projectId);
|
|
3616
|
+
|
|
3617
|
+
const normalizedContent = this.normalizeOptionalText(content);
|
|
3618
|
+
if (!normalizedContent) {
|
|
3619
|
+
throw new BadRequestException('Comment content is required.');
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
const rows = await this.queryRows<{ id: number; actorCollaboratorId: number | null; createdAt: Date }>(
|
|
3623
|
+
`SELECT id, actor_collaborator_id AS "actorCollaboratorId", created_at AS "createdAt"
|
|
3624
|
+
FROM operations_task_comment
|
|
3625
|
+
WHERE id = $1 AND task_id = $2`,
|
|
3626
|
+
[commentId, taskId]
|
|
3627
|
+
);
|
|
3628
|
+
|
|
3629
|
+
const row = rows[0];
|
|
3630
|
+
if (!row) {
|
|
3631
|
+
throw new NotFoundException('Comment not found.');
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3634
|
+
if (row.actorCollaboratorId !== actor.collaboratorId) {
|
|
3635
|
+
throw new ForbiddenException('You can only edit your own comments.');
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
const editSettings = await this.settingService.getSettingValues(['operations.comment-edit-window']);
|
|
3639
|
+
const editWindowMinutes = Number(editSettings['operations.comment-edit-window'] ?? 5);
|
|
3640
|
+
if (editWindowMinutes > 0) {
|
|
3641
|
+
const diffMinutes = (Date.now() - new Date(row.createdAt).getTime()) / 60000;
|
|
3642
|
+
if (diffMinutes > editWindowMinutes) {
|
|
3643
|
+
throw new ForbiddenException(
|
|
3644
|
+
`Comments can only be edited within ${editWindowMinutes} minute(s) of posting.`
|
|
3645
|
+
);
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
await this.queryRows(
|
|
3650
|
+
`UPDATE operations_task_comment
|
|
3651
|
+
SET content = $1, updated_at = NOW()
|
|
3652
|
+
WHERE id = $2`,
|
|
3653
|
+
[normalizedContent, commentId]
|
|
3654
|
+
);
|
|
3655
|
+
|
|
3656
|
+
const comments = await this.listTaskComments(userId, taskId);
|
|
3657
|
+
return comments.find((c) => c.id === commentId) ?? null;
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
async removeTaskComment(
|
|
3661
|
+
userId: number,
|
|
3662
|
+
taskId: number,
|
|
3663
|
+
commentId: number
|
|
3664
|
+
) {
|
|
3665
|
+
const actor = await this.getActorContext(userId);
|
|
3666
|
+
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
3667
|
+
throw new ForbiddenException(
|
|
3668
|
+
'Operations collaborator access is required.'
|
|
3669
|
+
);
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
const current = await this.getTaskRecordForActor(
|
|
3673
|
+
this.prisma,
|
|
3674
|
+
actor,
|
|
3675
|
+
taskId
|
|
3676
|
+
);
|
|
3677
|
+
await this.assertProjectAccess(actor, current.projectId);
|
|
3678
|
+
|
|
3679
|
+
const rows = await this.queryRows<{ id: number; actorCollaboratorId: number | null; createdAt: Date }>(
|
|
3680
|
+
`SELECT id, actor_collaborator_id AS "actorCollaboratorId", created_at AS "createdAt"
|
|
3681
|
+
FROM operations_task_comment
|
|
3682
|
+
WHERE id = $1 AND task_id = $2`,
|
|
3683
|
+
[commentId, taskId]
|
|
3684
|
+
);
|
|
3685
|
+
|
|
3686
|
+
const row = rows[0];
|
|
3687
|
+
if (!row) {
|
|
3688
|
+
throw new NotFoundException('Comment not found.');
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3691
|
+
if (row.actorCollaboratorId !== actor.collaboratorId) {
|
|
3692
|
+
throw new ForbiddenException('You can only delete your own comments.');
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
const deleteSettings = await this.settingService.getSettingValues(['operations.comment-edit-window']);
|
|
3696
|
+
const deleteWindowMinutes = Number(deleteSettings['operations.comment-edit-window'] ?? 5);
|
|
3697
|
+
if (deleteWindowMinutes > 0) {
|
|
3698
|
+
const diffMinutes = (Date.now() - new Date(row.createdAt).getTime()) / 60000;
|
|
3699
|
+
if (diffMinutes > deleteWindowMinutes) {
|
|
3700
|
+
throw new ForbiddenException(
|
|
3701
|
+
`Comments can only be deleted within ${deleteWindowMinutes} minute(s) of posting.`
|
|
3702
|
+
);
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
await this.queryRows(
|
|
3707
|
+
`DELETE FROM operations_task_comment WHERE id = $1`,
|
|
3708
|
+
[commentId]
|
|
3709
|
+
);
|
|
3710
|
+
|
|
3711
|
+
return { success: true };
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3265
3714
|
async listTimesheetEntries(
|
|
3266
3715
|
userId: number,
|
|
3267
3716
|
paginationParams: {
|
|
@@ -3761,7 +4210,17 @@ export class OperationsService {
|
|
|
3761
4210
|
return projectId;
|
|
3762
4211
|
});
|
|
3763
4212
|
|
|
3764
|
-
|
|
4213
|
+
const result = await this.getProjectById(userId, createdProjectId);
|
|
4214
|
+
|
|
4215
|
+
await this.integrationApi.publishEvent({
|
|
4216
|
+
eventName: 'operations.project.created',
|
|
4217
|
+
sourceModule: 'operations',
|
|
4218
|
+
aggregateType: 'project',
|
|
4219
|
+
aggregateId: String(createdProjectId),
|
|
4220
|
+
payload: { id: createdProjectId, code: data.code, name: data.name, status: data.status ?? 'planning' },
|
|
4221
|
+
}).catch(() => null);
|
|
4222
|
+
|
|
4223
|
+
return result;
|
|
3765
4224
|
}
|
|
3766
4225
|
|
|
3767
4226
|
async updateProject(userId: number, projectId: number, data: Partial<ProjectPayload>) {
|
|
@@ -3858,7 +4317,17 @@ export class OperationsService {
|
|
|
3858
4317
|
}
|
|
3859
4318
|
});
|
|
3860
4319
|
|
|
3861
|
-
|
|
4320
|
+
const projectResult = await this.getProjectById(userId, projectId);
|
|
4321
|
+
|
|
4322
|
+
await this.integrationApi.publishEvent({
|
|
4323
|
+
eventName: 'operations.project.updated',
|
|
4324
|
+
sourceModule: 'operations',
|
|
4325
|
+
aggregateType: 'project',
|
|
4326
|
+
aggregateId: String(projectId),
|
|
4327
|
+
payload: { id: projectId, name: data.name, status: data.status },
|
|
4328
|
+
}).catch(() => null);
|
|
4329
|
+
|
|
4330
|
+
return projectResult;
|
|
3862
4331
|
}
|
|
3863
4332
|
|
|
3864
4333
|
async listContracts(
|
|
@@ -5099,6 +5568,7 @@ export class OperationsService {
|
|
|
5099
5568
|
status?: string;
|
|
5100
5569
|
dateFrom?: string;
|
|
5101
5570
|
dateTo?: string;
|
|
5571
|
+
collaboratorId?: number;
|
|
5102
5572
|
} = {}
|
|
5103
5573
|
) {
|
|
5104
5574
|
const actor = await this.getActorContext(userId);
|
|
@@ -5125,6 +5595,10 @@ export class OperationsService {
|
|
|
5125
5595
|
where.push(`t.week_start_date <= ${this.param(params, filters.dateTo)}::date`);
|
|
5126
5596
|
}
|
|
5127
5597
|
|
|
5598
|
+
if (filters.collaboratorId) {
|
|
5599
|
+
where.push(`t.collaborator_id = ${this.param(params, filters.collaboratorId)}`);
|
|
5600
|
+
}
|
|
5601
|
+
|
|
5128
5602
|
if (pagination?.search) {
|
|
5129
5603
|
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
5130
5604
|
where.push(`(
|
|
@@ -5164,6 +5638,7 @@ export class OperationsService {
|
|
|
5164
5638
|
reviewedAt: string | null;
|
|
5165
5639
|
notes: string | null;
|
|
5166
5640
|
decisionNote: string | null;
|
|
5641
|
+
approvalId: number | null;
|
|
5167
5642
|
}>(
|
|
5168
5643
|
`SELECT t.id,
|
|
5169
5644
|
t.collaborator_id AS "collaboratorId",
|
|
@@ -5177,7 +5652,8 @@ export class OperationsService {
|
|
|
5177
5652
|
t.submitted_at AS "submittedAt",
|
|
5178
5653
|
t.reviewed_at AS "reviewedAt",
|
|
5179
5654
|
t.notes,
|
|
5180
|
-
approval.decision_note AS "decisionNote"
|
|
5655
|
+
approval.decision_note AS "decisionNote",
|
|
5656
|
+
approval.id AS "approvalId"
|
|
5181
5657
|
FROM operations_timesheet t
|
|
5182
5658
|
JOIN operations_collaborator c ON c.id = t.collaborator_id
|
|
5183
5659
|
LEFT JOIN operations_collaborator a ON a.id = t.approver_collaborator_id
|
|
@@ -7545,6 +8021,7 @@ export class OperationsService {
|
|
|
7545
8021
|
title: string | null;
|
|
7546
8022
|
levelLabel: string | null;
|
|
7547
8023
|
weeklyCapacityHours: number | null;
|
|
8024
|
+
hourlyRate: number | null;
|
|
7548
8025
|
status: string;
|
|
7549
8026
|
joinedAt: string | null;
|
|
7550
8027
|
leftAt: string | null;
|
|
@@ -7572,6 +8049,7 @@ export class OperationsService {
|
|
|
7572
8049
|
COALESCE(NULLIF(job_title_record.name, ''), NULLIF(c.title, '')) AS "title",
|
|
7573
8050
|
c.level_label AS "levelLabel",
|
|
7574
8051
|
c.weekly_capacity_hours AS "weeklyCapacityHours",
|
|
8052
|
+
c.hourly_rate AS "hourlyRate",
|
|
7575
8053
|
c.status,
|
|
7576
8054
|
c.joined_at AS "joinedAt",
|
|
7577
8055
|
c.left_at AS "leftAt",
|
|
@@ -8026,6 +8504,7 @@ export class OperationsService {
|
|
|
8026
8504
|
assigneeUserPhotoId: number | null;
|
|
8027
8505
|
assigneePersonAvatarId: number | null;
|
|
8028
8506
|
projectAssignmentId: number | null;
|
|
8507
|
+
commentCount: number;
|
|
8029
8508
|
createdAt: string;
|
|
8030
8509
|
}>(
|
|
8031
8510
|
`SELECT t.id,
|
|
@@ -8042,6 +8521,7 @@ export class OperationsService {
|
|
|
8042
8521
|
au.photo_id AS "assigneeUserPhotoId",
|
|
8043
8522
|
ap.avatar_id AS "assigneePersonAvatarId",
|
|
8044
8523
|
t.project_assignment_id AS "projectAssignmentId",
|
|
8524
|
+
COALESCE(task_comments.count, 0)::int AS "commentCount",
|
|
8045
8525
|
t.created_at AS "createdAt"
|
|
8046
8526
|
FROM operations_task t
|
|
8047
8527
|
LEFT JOIN operations_collaborator ac
|
|
@@ -8050,6 +8530,11 @@ export class OperationsService {
|
|
|
8050
8530
|
ON au.id = ac.user_id
|
|
8051
8531
|
LEFT JOIN person ap
|
|
8052
8532
|
ON ap.id = ac.person_id
|
|
8533
|
+
LEFT JOIN LATERAL (
|
|
8534
|
+
SELECT COUNT(*) AS count
|
|
8535
|
+
FROM operations_task_comment tc
|
|
8536
|
+
WHERE tc.task_id = t.id
|
|
8537
|
+
) task_comments ON TRUE
|
|
8053
8538
|
WHERE COALESCE(t.project_id, (
|
|
8054
8539
|
SELECT pa.project_id FROM operations_project_assignment pa
|
|
8055
8540
|
WHERE pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
|
|
@@ -8080,6 +8565,7 @@ export class OperationsService {
|
|
|
8080
8565
|
assigneePersonAvatarId: number | null;
|
|
8081
8566
|
projectAssignmentId: number | null;
|
|
8082
8567
|
projectId: number | null;
|
|
8568
|
+
commentCount: number;
|
|
8083
8569
|
createdAt: string;
|
|
8084
8570
|
deletedAt: string | null;
|
|
8085
8571
|
}>(
|
|
@@ -8098,6 +8584,7 @@ export class OperationsService {
|
|
|
8098
8584
|
ap.avatar_id AS "assigneePersonAvatarId",
|
|
8099
8585
|
t.project_assignment_id AS "projectAssignmentId",
|
|
8100
8586
|
COALESCE(t.project_id, pa.project_id) AS "projectId",
|
|
8587
|
+
COALESCE(task_comments.count, 0)::int AS "commentCount",
|
|
8101
8588
|
t.created_at AS "createdAt",
|
|
8102
8589
|
t.deleted_at AS "deletedAt"
|
|
8103
8590
|
FROM operations_task t
|
|
@@ -8107,6 +8594,11 @@ export class OperationsService {
|
|
|
8107
8594
|
LEFT JOIN person ap ON ap.id = ac.person_id
|
|
8108
8595
|
LEFT JOIN operations_project_assignment pa
|
|
8109
8596
|
ON pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
|
|
8597
|
+
LEFT JOIN LATERAL (
|
|
8598
|
+
SELECT COUNT(*) AS count
|
|
8599
|
+
FROM operations_task_comment tc
|
|
8600
|
+
WHERE tc.task_id = t.id
|
|
8601
|
+
) task_comments ON TRUE
|
|
8110
8602
|
WHERE t.id = $1`,
|
|
8111
8603
|
[taskId]
|
|
8112
8604
|
);
|
|
@@ -9824,7 +10316,9 @@ export class OperationsService {
|
|
|
9824
10316
|
collaboratorId: number,
|
|
9825
10317
|
amount: number,
|
|
9826
10318
|
actorUserId: number | null,
|
|
9827
|
-
notes: string | null
|
|
10319
|
+
notes: string | null,
|
|
10320
|
+
effectiveDate?: string | null,
|
|
10321
|
+
amountType: 'salary' | 'hourly_rate' = 'salary'
|
|
9828
10322
|
) {
|
|
9829
10323
|
await client.$executeRawUnsafe(
|
|
9830
10324
|
`INSERT INTO operations_collaborator_compensation_history (
|
|
@@ -9832,14 +10326,18 @@ export class OperationsService {
|
|
|
9832
10326
|
amount,
|
|
9833
10327
|
actor_user_id,
|
|
9834
10328
|
notes,
|
|
10329
|
+
effective_date,
|
|
10330
|
+
amount_type,
|
|
9835
10331
|
created_at
|
|
9836
10332
|
) VALUES (
|
|
9837
|
-
$1, $2, $3, $4, NOW()
|
|
10333
|
+
$1, $2, $3, $4, $5::date, $6::operations_collaborator_compensation_history_am_f803c4196e_enum, NOW()
|
|
9838
10334
|
)`,
|
|
9839
10335
|
collaboratorId,
|
|
9840
10336
|
amount,
|
|
9841
10337
|
actorUserId,
|
|
9842
|
-
notes ?? null
|
|
10338
|
+
notes ?? null,
|
|
10339
|
+
effectiveDate ?? null,
|
|
10340
|
+
amountType
|
|
9843
10341
|
);
|
|
9844
10342
|
}
|
|
9845
10343
|
|
|
@@ -11052,12 +11550,18 @@ export class OperationsService {
|
|
|
11052
11550
|
au.photo_id AS "assigneeUserPhotoId",
|
|
11053
11551
|
ap.avatar_id AS "assigneePersonAvatarId",
|
|
11054
11552
|
t.project_assignment_id AS "projectAssignmentId",
|
|
11553
|
+
COALESCE(task_comments.count, 0)::int AS "commentCount",
|
|
11055
11554
|
t.created_at AS "createdAt"
|
|
11056
11555
|
FROM operations_task t
|
|
11057
11556
|
LEFT JOIN operations_collaborator ac
|
|
11058
11557
|
ON ac.id = t.assignee_collaborator_id AND ac.deleted_at IS NULL
|
|
11059
11558
|
LEFT JOIN "user" au ON au.id = ac.user_id
|
|
11060
11559
|
LEFT JOIN person ap ON ap.id = ac.person_id
|
|
11560
|
+
LEFT JOIN LATERAL (
|
|
11561
|
+
SELECT COUNT(*) AS count
|
|
11562
|
+
FROM operations_task_comment tc
|
|
11563
|
+
WHERE tc.task_id = t.id
|
|
11564
|
+
) task_comments ON TRUE
|
|
11061
11565
|
WHERE COALESCE(t.project_id, (
|
|
11062
11566
|
SELECT pa.project_id FROM operations_project_assignment pa
|
|
11063
11567
|
WHERE pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
|
|
@@ -11121,7 +11625,9 @@ export class OperationsService {
|
|
|
11121
11625
|
];
|
|
11122
11626
|
|
|
11123
11627
|
if (actor.collaboratorId) {
|
|
11124
|
-
|
|
11628
|
+
const p1 = this.param(params, actor.collaboratorId);
|
|
11629
|
+
const p2 = this.param(params, actor.collaboratorId);
|
|
11630
|
+
filters.push(`(pa.collaborator_id = ${p1} OR t.assignee_collaborator_id = ${p2})`);
|
|
11125
11631
|
}
|
|
11126
11632
|
|
|
11127
11633
|
if (pagination.search) {
|
|
@@ -11178,6 +11684,7 @@ export class OperationsService {
|
|
|
11178
11684
|
assigneeName: string | null;
|
|
11179
11685
|
assigneeUserPhotoId: number | null;
|
|
11180
11686
|
assigneePersonAvatarId: number | null;
|
|
11687
|
+
commentCount: number;
|
|
11181
11688
|
createdAt: string;
|
|
11182
11689
|
deletedAt: string | null;
|
|
11183
11690
|
}>(
|
|
@@ -11196,6 +11703,7 @@ export class OperationsService {
|
|
|
11196
11703
|
ac.display_name AS "assigneeName",
|
|
11197
11704
|
au.photo_id AS "assigneeUserPhotoId",
|
|
11198
11705
|
ap.avatar_id AS "assigneePersonAvatarId",
|
|
11706
|
+
COALESCE(task_comments.count, 0)::int AS "commentCount",
|
|
11199
11707
|
t.created_at AS "createdAt",
|
|
11200
11708
|
t.deleted_at AS "deletedAt"
|
|
11201
11709
|
FROM operations_task t
|
|
@@ -11208,6 +11716,11 @@ export class OperationsService {
|
|
|
11208
11716
|
ON au.id = ac.user_id
|
|
11209
11717
|
LEFT JOIN person ap
|
|
11210
11718
|
ON ap.id = ac.person_id
|
|
11719
|
+
LEFT JOIN LATERAL (
|
|
11720
|
+
SELECT COUNT(*) AS count
|
|
11721
|
+
FROM operations_task_comment tc
|
|
11722
|
+
WHERE tc.task_id = t.id
|
|
11723
|
+
) task_comments ON TRUE
|
|
11211
11724
|
JOIN operations_project p
|
|
11212
11725
|
ON p.id = COALESCE(t.project_id, pa.project_id)
|
|
11213
11726
|
WHERE ${whereClause}
|
|
@@ -11442,7 +11955,16 @@ export class OperationsService {
|
|
|
11442
11955
|
? { revenue: 0.9, cost: 0.96, backlog: 0.82 }
|
|
11443
11956
|
: { revenue: 1, cost: 1, backlog: 1 };
|
|
11444
11957
|
|
|
11445
|
-
const
|
|
11958
|
+
const fromDate = new Date(`${from}T00:00:00`);
|
|
11959
|
+
const toDate = new Date(`${to}T00:00:00`);
|
|
11960
|
+
const periodDays = Math.max(
|
|
11961
|
+
1,
|
|
11962
|
+
Math.floor((toDate.getTime() - fromDate.getTime()) / 86400000) + 1
|
|
11963
|
+
);
|
|
11964
|
+
const periodWeeks = Math.max(1, Math.ceil(periodDays / 7));
|
|
11965
|
+
const periodMonths = periodDays / 30.4375;
|
|
11966
|
+
|
|
11967
|
+
const params: unknown[] = [from, to, periodMonths, periodWeeks];
|
|
11446
11968
|
const where = [
|
|
11447
11969
|
'p.deleted_at IS NULL',
|
|
11448
11970
|
'(p.end_date IS NULL OR p.end_date >= $1::date)',
|
|
@@ -11469,6 +11991,8 @@ export class OperationsService {
|
|
|
11469
11991
|
weeklyHours: string | null;
|
|
11470
11992
|
actualHours: string | null;
|
|
11471
11993
|
billableHours: string | null;
|
|
11994
|
+
realizedCost: string | null;
|
|
11995
|
+
allocatedCost: string | null;
|
|
11472
11996
|
openTasks: string | null;
|
|
11473
11997
|
backlogHours: string | null;
|
|
11474
11998
|
futureDeliveries: string | null;
|
|
@@ -11487,6 +12011,8 @@ export class OperationsService {
|
|
|
11487
12011
|
COALESCE(assignment_stats.weekly_hours, 0)::text AS "weeklyHours",
|
|
11488
12012
|
COALESCE(time_stats.actual_hours, 0)::text AS "actualHours",
|
|
11489
12013
|
COALESCE(time_stats.billable_hours, 0)::text AS "billableHours",
|
|
12014
|
+
COALESCE(cost_stats.realized_cost, 0)::text AS "realizedCost",
|
|
12015
|
+
COALESCE(alloc_cost_stats.allocated_cost, 0)::text AS "allocatedCost",
|
|
11490
12016
|
COALESCE(task_stats.open_tasks, 0)::text AS "openTasks",
|
|
11491
12017
|
COALESCE(task_stats.backlog_hours, 0)::text AS "backlogHours",
|
|
11492
12018
|
COALESCE(task_stats.future_deliveries, 0)::text AS "futureDeliveries"
|
|
@@ -11515,6 +12041,146 @@ export class OperationsService {
|
|
|
11515
12041
|
AND entry.deleted_at IS NULL
|
|
11516
12042
|
AND entry.work_date BETWEEN $1::date AND $2::date
|
|
11517
12043
|
) time_stats ON TRUE
|
|
12044
|
+
LEFT JOIN LATERAL (
|
|
12045
|
+
SELECT COALESCE(
|
|
12046
|
+
SUM(
|
|
12047
|
+
entry.hours
|
|
12048
|
+
* (
|
|
12049
|
+
(
|
|
12050
|
+
COALESCE(collaborator_costs.salary_cost, 0)
|
|
12051
|
+
+ COALESCE(collaborator_costs.benefits_cost, 0)
|
|
12052
|
+
+ COALESCE(collaborator_costs.taxes_cost, 0)
|
|
12053
|
+
+ COALESCE(collaborator_costs.tools_cost, 0)
|
|
12054
|
+
)
|
|
12055
|
+
* $3::numeric
|
|
12056
|
+
/ GREATEST(
|
|
12057
|
+
COALESCE(collaborator_record.weekly_capacity_hours, 40)::numeric * $4::numeric,
|
|
12058
|
+
COALESCE(collaborator_hours.total_hours, 0),
|
|
12059
|
+
1
|
|
12060
|
+
)
|
|
12061
|
+
)
|
|
12062
|
+
),
|
|
12063
|
+
0
|
|
12064
|
+
) AS realized_cost
|
|
12065
|
+
FROM operations_timesheet_entry entry
|
|
12066
|
+
JOIN operations_project_assignment pa
|
|
12067
|
+
ON pa.id = entry.project_assignment_id
|
|
12068
|
+
AND pa.deleted_at IS NULL
|
|
12069
|
+
JOIN operations_collaborator collaborator_record
|
|
12070
|
+
ON collaborator_record.id = pa.collaborator_id
|
|
12071
|
+
AND collaborator_record.deleted_at IS NULL
|
|
12072
|
+
LEFT JOIN LATERAL (
|
|
12073
|
+
SELECT COALESCE(NULLIF(cost_totals.salary_cost, 0), compensation_history.amount, hiring_contract.budget_amount, 0) AS salary_cost,
|
|
12074
|
+
cost_totals.benefits_cost,
|
|
12075
|
+
cost_totals.taxes_cost,
|
|
12076
|
+
cost_totals.tools_cost
|
|
12077
|
+
FROM (
|
|
12078
|
+
SELECT COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('salario-base', 'pro-labore')), 0) AS salary_cost,
|
|
12079
|
+
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,
|
|
12080
|
+
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,
|
|
12081
|
+
COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('software-licenca', 'equipamento')), 0) AS tools_cost
|
|
12082
|
+
FROM operations_collaborator_cost cost
|
|
12083
|
+
LEFT JOIN operations_cost_type cost_type
|
|
12084
|
+
ON cost_type.id = cost.cost_type_id
|
|
12085
|
+
WHERE cost.collaborator_id = collaborator_record.id
|
|
12086
|
+
AND (cost.start_date IS NULL OR cost.start_date <= $2::date)
|
|
12087
|
+
AND (cost.end_date IS NULL OR cost.end_date >= $1::date)
|
|
12088
|
+
) cost_totals
|
|
12089
|
+
LEFT JOIN LATERAL (
|
|
12090
|
+
SELECT h.amount
|
|
12091
|
+
FROM operations_collaborator_compensation_history h
|
|
12092
|
+
WHERE h.collaborator_id = collaborator_record.id
|
|
12093
|
+
AND (h.effective_date IS NULL OR h.effective_date <= $2::date)
|
|
12094
|
+
ORDER BY h.effective_date DESC NULLS LAST, h.created_at DESC
|
|
12095
|
+
LIMIT 1
|
|
12096
|
+
) compensation_history ON TRUE
|
|
12097
|
+
LEFT JOIN LATERAL (
|
|
12098
|
+
SELECT oc.budget_amount
|
|
12099
|
+
FROM operations_contract oc
|
|
12100
|
+
WHERE oc.related_collaborator_id = collaborator_record.id
|
|
12101
|
+
AND oc.deleted_at IS NULL
|
|
12102
|
+
ORDER BY CASE WHEN oc.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
|
|
12103
|
+
oc.created_at DESC
|
|
12104
|
+
LIMIT 1
|
|
12105
|
+
) hiring_contract ON TRUE
|
|
12106
|
+
) collaborator_costs ON TRUE
|
|
12107
|
+
LEFT JOIN LATERAL (
|
|
12108
|
+
SELECT COALESCE(SUM(entry2.hours), 0) AS total_hours
|
|
12109
|
+
FROM operations_timesheet_entry entry2
|
|
12110
|
+
JOIN operations_project_assignment pa2
|
|
12111
|
+
ON pa2.id = entry2.project_assignment_id
|
|
12112
|
+
AND pa2.deleted_at IS NULL
|
|
12113
|
+
WHERE pa2.collaborator_id = collaborator_record.id
|
|
12114
|
+
AND entry2.deleted_at IS NULL
|
|
12115
|
+
AND entry2.work_date BETWEEN $1::date AND $2::date
|
|
12116
|
+
) collaborator_hours ON TRUE
|
|
12117
|
+
WHERE pa.project_id = p.id
|
|
12118
|
+
AND entry.deleted_at IS NULL
|
|
12119
|
+
AND entry.work_date BETWEEN $1::date AND $2::date
|
|
12120
|
+
) cost_stats ON TRUE
|
|
12121
|
+
LEFT JOIN LATERAL (
|
|
12122
|
+
SELECT COALESCE(
|
|
12123
|
+
SUM(
|
|
12124
|
+
pa.weekly_hours
|
|
12125
|
+
* (
|
|
12126
|
+
(
|
|
12127
|
+
COALESCE(alloc_costs.salary_cost, 0)
|
|
12128
|
+
+ COALESCE(alloc_costs.benefits_cost, 0)
|
|
12129
|
+
+ COALESCE(alloc_costs.taxes_cost, 0)
|
|
12130
|
+
+ COALESCE(alloc_costs.tools_cost, 0)
|
|
12131
|
+
)
|
|
12132
|
+
* $3::numeric
|
|
12133
|
+
/ GREATEST(
|
|
12134
|
+
COALESCE(alloc_col.weekly_capacity_hours, 40)::numeric,
|
|
12135
|
+
1
|
|
12136
|
+
)
|
|
12137
|
+
)
|
|
12138
|
+
),
|
|
12139
|
+
0
|
|
12140
|
+
) AS allocated_cost
|
|
12141
|
+
FROM operations_project_assignment pa
|
|
12142
|
+
JOIN operations_collaborator alloc_col
|
|
12143
|
+
ON alloc_col.id = pa.collaborator_id
|
|
12144
|
+
AND alloc_col.deleted_at IS NULL
|
|
12145
|
+
LEFT JOIN LATERAL (
|
|
12146
|
+
SELECT COALESCE(NULLIF(ct.salary_cost, 0), ch.amount, hc.budget_amount, 0) AS salary_cost,
|
|
12147
|
+
ct.benefits_cost,
|
|
12148
|
+
ct.taxes_cost,
|
|
12149
|
+
ct.tools_cost
|
|
12150
|
+
FROM (
|
|
12151
|
+
SELECT COALESCE(SUM(c.amount) FILTER (WHERE c.recurrence::text = 'monthly' AND ct2.slug IN ('salario-base', 'pro-labore')), 0) AS salary_cost,
|
|
12152
|
+
COALESCE(SUM(c.amount) FILTER (WHERE c.recurrence::text = 'monthly' AND ct2.slug IN ('vale-refeicao', 'vale-alimentacao', 'vale-transporte', 'plano-saude', 'plano-odontologico', 'seguro-vida')), 0) AS benefits_cost,
|
|
12153
|
+
COALESCE(SUM(c.amount) FILTER (WHERE c.recurrence::text = 'monthly' AND ct2.slug IN ('inss-patronal', 'fgts', 'rat-fap', 'terceiros-sistema-s', 'provisao-decimo-terceiro', 'provisao-ferias')), 0) AS taxes_cost,
|
|
12154
|
+
COALESCE(SUM(c.amount) FILTER (WHERE c.recurrence::text = 'monthly' AND ct2.slug IN ('software-licenca', 'equipamento')), 0) AS tools_cost
|
|
12155
|
+
FROM operations_collaborator_cost c
|
|
12156
|
+
LEFT JOIN operations_cost_type ct2
|
|
12157
|
+
ON ct2.id = c.cost_type_id
|
|
12158
|
+
WHERE c.collaborator_id = alloc_col.id
|
|
12159
|
+
AND (c.start_date IS NULL OR c.start_date <= $2::date)
|
|
12160
|
+
AND (c.end_date IS NULL OR c.end_date >= $1::date)
|
|
12161
|
+
) ct
|
|
12162
|
+
LEFT JOIN LATERAL (
|
|
12163
|
+
SELECT h.amount
|
|
12164
|
+
FROM operations_collaborator_compensation_history h
|
|
12165
|
+
WHERE h.collaborator_id = alloc_col.id
|
|
12166
|
+
AND (h.effective_date IS NULL OR h.effective_date <= $2::date)
|
|
12167
|
+
ORDER BY h.effective_date DESC NULLS LAST, h.created_at DESC
|
|
12168
|
+
LIMIT 1
|
|
12169
|
+
) ch ON TRUE
|
|
12170
|
+
LEFT JOIN LATERAL (
|
|
12171
|
+
SELECT oc.budget_amount
|
|
12172
|
+
FROM operations_contract oc
|
|
12173
|
+
WHERE oc.related_collaborator_id = alloc_col.id
|
|
12174
|
+
AND oc.deleted_at IS NULL
|
|
12175
|
+
ORDER BY CASE WHEN oc.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
|
|
12176
|
+
oc.created_at DESC
|
|
12177
|
+
LIMIT 1
|
|
12178
|
+
) hc ON TRUE
|
|
12179
|
+
) alloc_costs ON TRUE
|
|
12180
|
+
WHERE pa.project_id = p.id
|
|
12181
|
+
AND pa.deleted_at IS NULL
|
|
12182
|
+
AND pa.status IN ('planned', 'active')
|
|
12183
|
+
) alloc_cost_stats ON TRUE
|
|
11518
12184
|
LEFT JOIN LATERAL (
|
|
11519
12185
|
SELECT COUNT(*) FILTER (WHERE task.status IN ('todo', 'doing', 'review')) AS open_tasks,
|
|
11520
12186
|
COALESCE(SUM(task.estimate_hours) FILTER (WHERE task.status IN ('todo', 'doing', 'review')), 0) AS backlog_hours,
|
|
@@ -11528,12 +12194,6 @@ export class OperationsService {
|
|
|
11528
12194
|
params
|
|
11529
12195
|
);
|
|
11530
12196
|
|
|
11531
|
-
const fromDate = new Date(`${from}T00:00:00`);
|
|
11532
|
-
const toDate = new Date(`${to}T00:00:00`);
|
|
11533
|
-
const periodWeeks = Math.max(
|
|
11534
|
-
1,
|
|
11535
|
-
Math.ceil((toDate.getTime() - fromDate.getTime()) / 604800000)
|
|
11536
|
-
);
|
|
11537
12197
|
const rows = dbRows
|
|
11538
12198
|
.map((row) => {
|
|
11539
12199
|
const progress = Number(row.progressPercent ?? 0);
|
|
@@ -11541,7 +12201,12 @@ export class OperationsService {
|
|
|
11541
12201
|
const recognizedRevenue = contractedRevenue * (progress / 100);
|
|
11542
12202
|
const actualHours = Number(row.actualHours ?? 0);
|
|
11543
12203
|
const plannedHours = Math.max(Number(row.weeklyHours ?? 0) * periodWeeks, actualHours);
|
|
11544
|
-
const realizedCost = 0;
|
|
12204
|
+
const realizedCost = Number(row.realizedCost ?? 0);
|
|
12205
|
+
const allocatedCost = Number(row.allocatedCost ?? 0);
|
|
12206
|
+
const consumedHoursCost = realizedCost;
|
|
12207
|
+
const idlenessHours = Math.max(plannedHours - actualHours, 0);
|
|
12208
|
+
const idlenessRate = plannedHours > 0 ? (idlenessHours / plannedHours) * 100 : 0;
|
|
12209
|
+
const idlenessCost = Math.max(allocatedCost - consumedHoursCost, 0);
|
|
11545
12210
|
const reportStatus =
|
|
11546
12211
|
row.status === 'paused'
|
|
11547
12212
|
? 'paused'
|
|
@@ -11576,7 +12241,7 @@ export class OperationsService {
|
|
|
11576
12241
|
contractedRevenue,
|
|
11577
12242
|
recognizedRevenue,
|
|
11578
12243
|
realizedCost,
|
|
11579
|
-
forecastCost: realizedCost,
|
|
12244
|
+
forecastCost: realizedCost * multiplier.cost,
|
|
11580
12245
|
teamCost: realizedCost,
|
|
11581
12246
|
infraCost: 0,
|
|
11582
12247
|
licenseCost: 0,
|
|
@@ -11592,6 +12257,10 @@ export class OperationsService {
|
|
|
11592
12257
|
financialProgress: contractedRevenue ? (recognizedRevenue / contractedRevenue) * 100 : 0,
|
|
11593
12258
|
backlogValue: Math.max(contractedRevenue - recognizedRevenue, 0),
|
|
11594
12259
|
futureDeliveries: Number(row.futureDeliveries ?? 0),
|
|
12260
|
+
allocatedCost,
|
|
12261
|
+
consumedHoursCost,
|
|
12262
|
+
idlenessRate,
|
|
12263
|
+
idlenessCost,
|
|
11595
12264
|
risk,
|
|
11596
12265
|
recommendation:
|
|
11597
12266
|
risk === 'alto'
|
|
@@ -11617,6 +12286,9 @@ export class OperationsService {
|
|
|
11617
12286
|
acc.avgDeadline += row.physicalProgress;
|
|
11618
12287
|
acc.avgAllocation += row.allocatedCapacity;
|
|
11619
12288
|
acc.atRisk += row.risk === 'alto' ? 1 : 0;
|
|
12289
|
+
acc.allocatedCost += row.allocatedCost;
|
|
12290
|
+
acc.consumedHoursCost += row.consumedHoursCost;
|
|
12291
|
+
acc.idlenessCost += row.idlenessCost;
|
|
11620
12292
|
return acc;
|
|
11621
12293
|
},
|
|
11622
12294
|
{
|
|
@@ -11635,6 +12307,11 @@ export class OperationsService {
|
|
|
11635
12307
|
avgAllocation: 0,
|
|
11636
12308
|
atRisk: 0,
|
|
11637
12309
|
burnRate: 0,
|
|
12310
|
+
allocatedCost: 0,
|
|
12311
|
+
consumedHoursCost: 0,
|
|
12312
|
+
idlenessCost: 0,
|
|
12313
|
+
idlenessRate: 0,
|
|
12314
|
+
plannedProfit: 0,
|
|
11638
12315
|
}
|
|
11639
12316
|
);
|
|
11640
12317
|
summary.profit = summary.recognizedRevenue - summary.realizedCost;
|
|
@@ -11642,6 +12319,10 @@ export class OperationsService {
|
|
|
11642
12319
|
summary.avgDeadline = rows.length ? summary.avgDeadline / rows.length : 0;
|
|
11643
12320
|
summary.avgAllocation = rows.length ? summary.avgAllocation / rows.length : 0;
|
|
11644
12321
|
summary.burnRate = summary.plannedHours ? (summary.actualHours / summary.plannedHours) * 100 : 0;
|
|
12322
|
+
summary.plannedProfit = summary.contractedRevenue - summary.allocatedCost;
|
|
12323
|
+
summary.idlenessRate = summary.plannedHours > 0
|
|
12324
|
+
? Math.max(0, (summary.plannedHours - summary.actualHours) / summary.plannedHours * 100)
|
|
12325
|
+
: 0;
|
|
11645
12326
|
|
|
11646
12327
|
const forecast = Array.from({ length: 12 }, (_, index) => {
|
|
11647
12328
|
const monthDate = new Date(fromDate);
|
|
@@ -11744,6 +12425,15 @@ export class OperationsService {
|
|
|
11744
12425
|
: scenario === 'conservative'
|
|
11745
12426
|
? { revenue: 0.9, cost: 0.96, capacity: 0.94 }
|
|
11746
12427
|
: { revenue: 1, cost: 1, capacity: 1 };
|
|
12428
|
+
const fromDate = new Date(`${from}T00:00:00`);
|
|
12429
|
+
const toDate = new Date(`${to}T00:00:00`);
|
|
12430
|
+
const periodDays = Math.max(
|
|
12431
|
+
1,
|
|
12432
|
+
Math.floor((toDate.getTime() - fromDate.getTime()) / 86400000) + 1
|
|
12433
|
+
);
|
|
12434
|
+
const periodWeeks = Math.max(1, Math.ceil(periodDays / 7));
|
|
12435
|
+
const periodMonths = periodDays / 30.4375;
|
|
12436
|
+
|
|
11747
12437
|
const params: unknown[] = [from, to];
|
|
11748
12438
|
const where = [
|
|
11749
12439
|
'c.deleted_at IS NULL',
|
|
@@ -11774,6 +12464,11 @@ export class OperationsService {
|
|
|
11774
12464
|
taxesCost: string | null;
|
|
11775
12465
|
toolsCost: string | null;
|
|
11776
12466
|
billableValue: string | null;
|
|
12467
|
+
plannedAllocatedHours: string | null;
|
|
12468
|
+
plannedBillableHours: string | null;
|
|
12469
|
+
openTaskHours: string | null;
|
|
12470
|
+
openTaskBillableHours: string | null;
|
|
12471
|
+
openTasks: string | null;
|
|
11777
12472
|
allocatedHours: string | null;
|
|
11778
12473
|
billableHours: string | null;
|
|
11779
12474
|
projects: string | null;
|
|
@@ -11792,9 +12487,14 @@ export class OperationsService {
|
|
|
11792
12487
|
COALESCE(cost_stats.taxes_cost, 0)::text AS "taxesCost",
|
|
11793
12488
|
COALESCE(cost_stats.tools_cost, 0)::text AS "toolsCost",
|
|
11794
12489
|
COALESCE(value_stats.billable_value, 0)::text AS "billableValue",
|
|
12490
|
+
COALESCE(assignment_stats.planned_allocated_hours, 0)::text AS "plannedAllocatedHours",
|
|
12491
|
+
COALESCE(assignment_stats.planned_billable_hours, 0)::text AS "plannedBillableHours",
|
|
12492
|
+
COALESCE(task_stats.open_task_hours, 0)::text AS "openTaskHours",
|
|
12493
|
+
COALESCE(task_stats.open_task_billable_hours, 0)::text AS "openTaskBillableHours",
|
|
12494
|
+
COALESCE(task_stats.open_tasks, 0)::text AS "openTasks",
|
|
11795
12495
|
COALESCE(value_stats.allocated_hours, 0)::text AS "allocatedHours",
|
|
11796
12496
|
COALESCE(value_stats.billable_hours, 0)::text AS "billableHours",
|
|
11797
|
-
COALESCE(
|
|
12497
|
+
COALESCE(assignment_stats.projects, 0)::text AS projects
|
|
11798
12498
|
FROM operations_collaborator c
|
|
11799
12499
|
LEFT JOIN person person_record ON person_record.id = c.person_id
|
|
11800
12500
|
LEFT JOIN operations_department department_record
|
|
@@ -11807,16 +12507,85 @@ export class OperationsService {
|
|
|
11807
12507
|
ON collaborator_type.id = c.collaborator_type_id
|
|
11808
12508
|
AND collaborator_type.deleted_at IS NULL
|
|
11809
12509
|
LEFT JOIN LATERAL (
|
|
11810
|
-
SELECT COALESCE(
|
|
11811
|
-
|
|
11812
|
-
|
|
11813
|
-
|
|
11814
|
-
FROM
|
|
11815
|
-
|
|
11816
|
-
|
|
11817
|
-
|
|
11818
|
-
|
|
12510
|
+
SELECT COALESCE(NULLIF(cost_totals.salary_cost, 0), compensation_history.amount, hiring_contract.budget_amount, 0) AS salary_cost,
|
|
12511
|
+
cost_totals.benefits_cost,
|
|
12512
|
+
cost_totals.taxes_cost,
|
|
12513
|
+
cost_totals.tools_cost
|
|
12514
|
+
FROM (
|
|
12515
|
+
SELECT COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('salario-base', 'pro-labore')), 0) AS salary_cost,
|
|
12516
|
+
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,
|
|
12517
|
+
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,
|
|
12518
|
+
COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('software-licenca', 'equipamento')), 0) AS tools_cost
|
|
12519
|
+
FROM operations_collaborator_cost cost
|
|
12520
|
+
LEFT JOIN operations_cost_type cost_type
|
|
12521
|
+
ON cost_type.id = cost.cost_type_id
|
|
12522
|
+
WHERE cost.collaborator_id = c.id
|
|
12523
|
+
AND (cost.start_date IS NULL OR cost.start_date <= $2::date)
|
|
12524
|
+
AND (cost.end_date IS NULL OR cost.end_date >= $1::date)
|
|
12525
|
+
) cost_totals
|
|
12526
|
+
LEFT JOIN LATERAL (
|
|
12527
|
+
SELECT h.amount
|
|
12528
|
+
FROM operations_collaborator_compensation_history h
|
|
12529
|
+
WHERE h.collaborator_id = c.id
|
|
12530
|
+
AND (h.effective_date IS NULL OR h.effective_date <= $2::date)
|
|
12531
|
+
ORDER BY h.effective_date DESC NULLS LAST, h.created_at DESC
|
|
12532
|
+
LIMIT 1
|
|
12533
|
+
) compensation_history ON TRUE
|
|
12534
|
+
LEFT JOIN LATERAL (
|
|
12535
|
+
SELECT oc.budget_amount
|
|
12536
|
+
FROM operations_contract oc
|
|
12537
|
+
WHERE oc.related_collaborator_id = c.id
|
|
12538
|
+
AND oc.deleted_at IS NULL
|
|
12539
|
+
ORDER BY CASE WHEN oc.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
|
|
12540
|
+
oc.created_at DESC
|
|
12541
|
+
LIMIT 1
|
|
12542
|
+
) hiring_contract ON TRUE
|
|
11819
12543
|
) cost_stats ON TRUE
|
|
12544
|
+
LEFT JOIN LATERAL (
|
|
12545
|
+
SELECT COALESCE(
|
|
12546
|
+
SUM(
|
|
12547
|
+
COALESCE(
|
|
12548
|
+
pa.weekly_hours,
|
|
12549
|
+
COALESCE(c.weekly_capacity_hours, 40) * COALESCE(pa.allocation_percent, 0) / 100
|
|
12550
|
+
) * GREATEST(
|
|
12551
|
+
CEIL(
|
|
12552
|
+
(
|
|
12553
|
+
LEAST(COALESCE(pa.end_date, $2::date), $2::date)
|
|
12554
|
+
- GREATEST(COALESCE(pa.start_date, $1::date), $1::date)
|
|
12555
|
+
+ 1
|
|
12556
|
+
) / 7.0
|
|
12557
|
+
),
|
|
12558
|
+
0
|
|
12559
|
+
)
|
|
12560
|
+
),
|
|
12561
|
+
0
|
|
12562
|
+
) AS planned_allocated_hours,
|
|
12563
|
+
COALESCE(
|
|
12564
|
+
SUM(
|
|
12565
|
+
COALESCE(
|
|
12566
|
+
pa.weekly_hours,
|
|
12567
|
+
COALESCE(c.weekly_capacity_hours, 40) * COALESCE(pa.allocation_percent, 0) / 100
|
|
12568
|
+
) * GREATEST(
|
|
12569
|
+
CEIL(
|
|
12570
|
+
(
|
|
12571
|
+
LEAST(COALESCE(pa.end_date, $2::date), $2::date)
|
|
12572
|
+
- GREATEST(COALESCE(pa.start_date, $1::date), $1::date)
|
|
12573
|
+
+ 1
|
|
12574
|
+
) / 7.0
|
|
12575
|
+
),
|
|
12576
|
+
0
|
|
12577
|
+
)
|
|
12578
|
+
) FILTER (WHERE pa.is_billable = true),
|
|
12579
|
+
0
|
|
12580
|
+
) AS planned_billable_hours,
|
|
12581
|
+
COUNT(DISTINCT pa.project_id) AS projects
|
|
12582
|
+
FROM operations_project_assignment pa
|
|
12583
|
+
WHERE pa.collaborator_id = c.id
|
|
12584
|
+
AND pa.deleted_at IS NULL
|
|
12585
|
+
AND pa.status IN ('planned', 'active')
|
|
12586
|
+
AND (pa.start_date IS NULL OR pa.start_date <= $2::date)
|
|
12587
|
+
AND (pa.end_date IS NULL OR pa.end_date >= $1::date)
|
|
12588
|
+
) assignment_stats ON TRUE
|
|
11820
12589
|
LEFT JOIN LATERAL (
|
|
11821
12590
|
SELECT COALESCE(SUM(entry.hours), 0) AS allocated_hours,
|
|
11822
12591
|
COALESCE(SUM(entry.hours) FILTER (WHERE pa.is_billable = true), 0) AS billable_hours,
|
|
@@ -11830,31 +12599,50 @@ export class OperationsService {
|
|
|
11830
12599
|
AND entry.work_date BETWEEN $1::date AND $2::date
|
|
11831
12600
|
) value_stats ON TRUE
|
|
11832
12601
|
LEFT JOIN LATERAL (
|
|
11833
|
-
SELECT COUNT(
|
|
11834
|
-
|
|
11835
|
-
|
|
12602
|
+
SELECT COUNT(*) AS open_tasks,
|
|
12603
|
+
COALESCE(SUM(COALESCE(task.estimate_hours, 0)), 0) AS open_task_hours,
|
|
12604
|
+
COALESCE(
|
|
12605
|
+
SUM(COALESCE(task.estimate_hours, 0)) FILTER (WHERE pa.is_billable = true),
|
|
12606
|
+
0
|
|
12607
|
+
) AS open_task_billable_hours
|
|
12608
|
+
FROM operations_task task
|
|
12609
|
+
LEFT JOIN operations_project_assignment pa
|
|
12610
|
+
ON pa.id = task.project_assignment_id
|
|
11836
12611
|
AND pa.deleted_at IS NULL
|
|
11837
|
-
|
|
11838
|
-
|
|
12612
|
+
WHERE task.deleted_at IS NULL
|
|
12613
|
+
AND task.status IN ('todo', 'doing', 'review')
|
|
12614
|
+
AND (
|
|
12615
|
+
task.assignee_collaborator_id = c.id
|
|
12616
|
+
OR pa.collaborator_id = c.id
|
|
12617
|
+
)
|
|
12618
|
+
) task_stats ON TRUE
|
|
11839
12619
|
WHERE ${where.join(' AND ')}
|
|
11840
12620
|
ORDER BY name ASC`,
|
|
11841
12621
|
params
|
|
11842
12622
|
);
|
|
11843
12623
|
|
|
11844
|
-
const fromDate = new Date(`${from}T00:00:00`);
|
|
11845
|
-
const toDate = new Date(`${to}T00:00:00`);
|
|
11846
|
-
const periodWeeks = Math.max(
|
|
11847
|
-
1,
|
|
11848
|
-
Math.ceil((toDate.getTime() - fromDate.getTime()) / 604800000)
|
|
11849
|
-
);
|
|
11850
12624
|
const rows = dbRows.map((row) => {
|
|
11851
|
-
const salaryCost = Number(row.salaryCost ?? 0);
|
|
11852
|
-
const benefitsCost = Number(row.benefitsCost ?? 0);
|
|
11853
|
-
const taxesCost = Number(row.taxesCost ?? 0);
|
|
11854
|
-
const toolsCost = Number(row.toolsCost ?? 0);
|
|
12625
|
+
const salaryCost = Number(row.salaryCost ?? 0) * periodMonths;
|
|
12626
|
+
const benefitsCost = Number(row.benefitsCost ?? 0) * periodMonths;
|
|
12627
|
+
const taxesCost = Number(row.taxesCost ?? 0) * periodMonths;
|
|
12628
|
+
const toolsCost = Number(row.toolsCost ?? 0) * periodMonths;
|
|
11855
12629
|
const availableHours = Number(row.weeklyCapacityHours ?? 40) * periodWeeks;
|
|
11856
|
-
const
|
|
11857
|
-
const
|
|
12630
|
+
const plannedAllocatedHours = Number(row.plannedAllocatedHours ?? 0);
|
|
12631
|
+
const plannedBillableHours = Number(row.plannedBillableHours ?? 0);
|
|
12632
|
+
const openTaskHours = Number(row.openTaskHours ?? 0);
|
|
12633
|
+
const openTaskBillableHours = Number(row.openTaskBillableHours ?? 0);
|
|
12634
|
+
const actualAllocatedHours = Number(row.allocatedHours ?? 0);
|
|
12635
|
+
const actualBillableHours = Number(row.billableHours ?? 0);
|
|
12636
|
+
const allocatedHours = Math.max(
|
|
12637
|
+
actualAllocatedHours,
|
|
12638
|
+
plannedAllocatedHours,
|
|
12639
|
+
openTaskHours
|
|
12640
|
+
);
|
|
12641
|
+
const billableHours = Math.max(
|
|
12642
|
+
actualBillableHours,
|
|
12643
|
+
plannedBillableHours,
|
|
12644
|
+
openTaskBillableHours
|
|
12645
|
+
);
|
|
11858
12646
|
const allocation = availableHours ? (allocatedHours / availableHours) * 100 : 0;
|
|
11859
12647
|
const risk = allocation >= 98 ? 'alto' : allocation < 75 ? 'médio' : 'baixo';
|
|
11860
12648
|
return {
|
|
@@ -11929,7 +12717,11 @@ export class OperationsService {
|
|
|
11929
12717
|
summary.freeHours = Math.max(summary.availableHours - summary.allocatedHours, 0);
|
|
11930
12718
|
summary.allocation = summary.availableHours ? (summary.allocatedHours / summary.availableHours) * 100 : 0;
|
|
11931
12719
|
summary.utilization = summary.availableHours ? (summary.billableHours / summary.availableHours) * 100 : 0;
|
|
11932
|
-
summary.hourlyCost = summary.allocatedHours
|
|
12720
|
+
summary.hourlyCost = summary.allocatedHours
|
|
12721
|
+
? summary.cost / summary.allocatedHours
|
|
12722
|
+
: summary.availableHours
|
|
12723
|
+
? summary.cost / summary.availableHours
|
|
12724
|
+
: 0;
|
|
11933
12725
|
|
|
11934
12726
|
const forecast = Array.from({ length: 12 }, (_, index) => {
|
|
11935
12727
|
const monthDate = new Date(fromDate);
|
|
@@ -12173,4 +12965,1623 @@ export class OperationsService {
|
|
|
12173
12965
|
|
|
12174
12966
|
return { success: true };
|
|
12175
12967
|
}
|
|
12968
|
+
|
|
12969
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
12970
|
+
// Project Cost Categories
|
|
12971
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
12972
|
+
|
|
12973
|
+
async listProjectCostCategories(userId: number, filters: { search?: string; is_active?: boolean; pageSize?: number; page?: number } = {}) {
|
|
12974
|
+
await this.getActorContext(userId);
|
|
12975
|
+
const localeId = await this.resolvePreferredLocaleId();
|
|
12976
|
+
|
|
12977
|
+
const params: unknown[] = [localeId];
|
|
12978
|
+
const where: string[] = ['pcc.deleted_at IS NULL'];
|
|
12979
|
+
|
|
12980
|
+
if (filters.is_active === true) {
|
|
12981
|
+
where.push('pcc.is_active = true');
|
|
12982
|
+
}
|
|
12983
|
+
|
|
12984
|
+
if (filters.search?.trim()) {
|
|
12985
|
+
const p = this.param(params, `%${filters.search.trim()}%`);
|
|
12986
|
+
where.push(`(COALESCE(pccl.name, pcc.slug) ILIKE ${p} OR COALESCE(pcc.slug, '') ILIKE ${p})`);
|
|
12987
|
+
}
|
|
12988
|
+
|
|
12989
|
+
const whereClause = `WHERE ${where.join(' AND ')}`;
|
|
12990
|
+
|
|
12991
|
+
return this.queryRows<{
|
|
12992
|
+
id: number;
|
|
12993
|
+
slug: string;
|
|
12994
|
+
name: string | null;
|
|
12995
|
+
description: string | null;
|
|
12996
|
+
icon: string | null;
|
|
12997
|
+
color: string | null;
|
|
12998
|
+
isActive: boolean;
|
|
12999
|
+
sortOrder: number;
|
|
13000
|
+
createdAt: string;
|
|
13001
|
+
}>(
|
|
13002
|
+
`SELECT pcc.id,
|
|
13003
|
+
pcc.slug,
|
|
13004
|
+
COALESCE(pccl.name, pcc.slug) AS name,
|
|
13005
|
+
pccl.description,
|
|
13006
|
+
pcc.icon,
|
|
13007
|
+
pcc.color,
|
|
13008
|
+
pcc.is_active AS "isActive",
|
|
13009
|
+
pcc.sort_order AS "sortOrder",
|
|
13010
|
+
pcc.created_at AS "createdAt"
|
|
13011
|
+
FROM operations_project_cost_category pcc
|
|
13012
|
+
LEFT JOIN LATERAL (
|
|
13013
|
+
SELECT l.name, l.description
|
|
13014
|
+
FROM operations_project_cost_category_locale l
|
|
13015
|
+
WHERE l.operations_project_cost_category_id = pcc.id
|
|
13016
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
13017
|
+
l.id ASC
|
|
13018
|
+
LIMIT 1
|
|
13019
|
+
) pccl ON TRUE
|
|
13020
|
+
${whereClause}
|
|
13021
|
+
ORDER BY pcc.sort_order ASC, COALESCE(pccl.name, pcc.slug) ASC`,
|
|
13022
|
+
params
|
|
13023
|
+
);
|
|
13024
|
+
}
|
|
13025
|
+
|
|
13026
|
+
async createProjectCostCategory(userId: number, data: { slug: string; name?: any; description?: any; icon?: string | null; color?: string | null; is_active?: boolean; sort_order?: number }) {
|
|
13027
|
+
const actor = await this.getActorContext(userId);
|
|
13028
|
+
this.ensureDirector(actor);
|
|
13029
|
+
|
|
13030
|
+
const slug = data.slug?.trim();
|
|
13031
|
+
if (!slug) {
|
|
13032
|
+
throw new BadRequestException('Cost category slug is required.');
|
|
13033
|
+
}
|
|
13034
|
+
|
|
13035
|
+
return this.prisma.$transaction(async (tx) => {
|
|
13036
|
+
const localeId = await this.resolvePreferredLocaleId(tx as any);
|
|
13037
|
+
|
|
13038
|
+
const created = (await (tx as any).$queryRawUnsafe(
|
|
13039
|
+
`INSERT INTO operations_project_cost_category (slug, icon, color, is_active, sort_order, created_at, updated_at)
|
|
13040
|
+
VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
|
|
13041
|
+
RETURNING id`,
|
|
13042
|
+
slug,
|
|
13043
|
+
data.icon ?? null,
|
|
13044
|
+
data.color ?? null,
|
|
13045
|
+
data.is_active ?? true,
|
|
13046
|
+
data.sort_order ?? 0,
|
|
13047
|
+
)) as { id: number }[];
|
|
13048
|
+
|
|
13049
|
+
const createdId = created[0]?.id;
|
|
13050
|
+
if (!createdId) {
|
|
13051
|
+
throw new BadRequestException('Unable to create project cost category.');
|
|
13052
|
+
}
|
|
13053
|
+
|
|
13054
|
+
const name = typeof data.name === 'string' ? data.name : (data.name ? JSON.stringify(data.name) : slug);
|
|
13055
|
+
const description = typeof data.description === 'string' ? data.description : (data.description ? JSON.stringify(data.description) : null);
|
|
13056
|
+
|
|
13057
|
+
if (localeId && name) {
|
|
13058
|
+
await (tx as any).$executeRawUnsafe(
|
|
13059
|
+
`INSERT INTO operations_project_cost_category_locale (operations_project_cost_category_id, locale_id, name, description)
|
|
13060
|
+
VALUES ($1, $2, $3, $4)`,
|
|
13061
|
+
createdId,
|
|
13062
|
+
localeId,
|
|
13063
|
+
name,
|
|
13064
|
+
description ?? null,
|
|
13065
|
+
);
|
|
13066
|
+
}
|
|
13067
|
+
|
|
13068
|
+
const rows = (await (tx as any).$queryRawUnsafe(
|
|
13069
|
+
`SELECT pcc.id,
|
|
13070
|
+
pcc.slug,
|
|
13071
|
+
COALESCE(pccl.name, pcc.slug) AS name,
|
|
13072
|
+
pccl.description,
|
|
13073
|
+
pcc.icon,
|
|
13074
|
+
pcc.color,
|
|
13075
|
+
pcc.is_active AS "isActive",
|
|
13076
|
+
pcc.sort_order AS "sortOrder",
|
|
13077
|
+
pcc.created_at AS "createdAt"
|
|
13078
|
+
FROM operations_project_cost_category pcc
|
|
13079
|
+
LEFT JOIN operations_project_cost_category_locale pccl
|
|
13080
|
+
ON pccl.operations_project_cost_category_id = pcc.id AND pccl.locale_id = $2
|
|
13081
|
+
WHERE pcc.id = $1`,
|
|
13082
|
+
createdId,
|
|
13083
|
+
localeId,
|
|
13084
|
+
)) as { id: number; slug: string; name: string; description: string | null; icon: string | null; color: string | null; isActive: boolean; sortOrder: number; createdAt: string }[];
|
|
13085
|
+
|
|
13086
|
+
return rows[0] ?? null;
|
|
13087
|
+
});
|
|
13088
|
+
}
|
|
13089
|
+
|
|
13090
|
+
async updateProjectCostCategory(userId: number, id: number, data: Partial<{ slug: string; name?: any; description?: any; icon?: string | null; color?: string | null; is_active?: boolean; sort_order?: number }>) {
|
|
13091
|
+
const actor = await this.getActorContext(userId);
|
|
13092
|
+
this.ensureDirector(actor);
|
|
13093
|
+
|
|
13094
|
+
const category = await this.querySingle<{ id: number }>(
|
|
13095
|
+
`SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
13096
|
+
[id]
|
|
13097
|
+
);
|
|
13098
|
+
if (!category) {
|
|
13099
|
+
throw new NotFoundException('Project cost category not found.');
|
|
13100
|
+
}
|
|
13101
|
+
|
|
13102
|
+
const sets: string[] = [];
|
|
13103
|
+
const params: unknown[] = [];
|
|
13104
|
+
|
|
13105
|
+
if (data.slug !== undefined) sets.push(`slug = ${this.param(params, data.slug)}`);
|
|
13106
|
+
if (data.icon !== undefined) sets.push(`icon = ${this.param(params, data.icon)}`);
|
|
13107
|
+
if (data.color !== undefined) sets.push(`color = ${this.param(params, data.color)}`);
|
|
13108
|
+
if (data.is_active !== undefined) sets.push(`is_active = ${this.param(params, data.is_active)}`);
|
|
13109
|
+
if (data.sort_order !== undefined) sets.push(`sort_order = ${this.param(params, data.sort_order)}`);
|
|
13110
|
+
|
|
13111
|
+
if (sets.length > 0) {
|
|
13112
|
+
sets.push(`updated_at = NOW()`);
|
|
13113
|
+
await this.prisma.$queryRawUnsafe(
|
|
13114
|
+
`UPDATE operations_project_cost_category SET ${sets.join(', ')} WHERE id = ${this.param(params, id)}`,
|
|
13115
|
+
...params
|
|
13116
|
+
);
|
|
13117
|
+
}
|
|
13118
|
+
|
|
13119
|
+
if (data.name !== undefined || data.description !== undefined) {
|
|
13120
|
+
const localeId = await this.resolvePreferredLocaleId();
|
|
13121
|
+
if (localeId) {
|
|
13122
|
+
const name = typeof data.name === 'string' ? data.name : (data.name ? JSON.stringify(data.name) : undefined);
|
|
13123
|
+
const description = typeof data.description === 'string' ? data.description : (data.description ? JSON.stringify(data.description) : null);
|
|
13124
|
+
const existing = await this.querySingle<{ id: number }>(
|
|
13125
|
+
`SELECT id FROM operations_project_cost_category_locale WHERE operations_project_cost_category_id = $1 AND locale_id = $2 LIMIT 1`,
|
|
13126
|
+
[id, localeId]
|
|
13127
|
+
);
|
|
13128
|
+
if (existing) {
|
|
13129
|
+
const localeSets: string[] = [];
|
|
13130
|
+
const localeParams: unknown[] = [];
|
|
13131
|
+
if (name !== undefined) localeSets.push(`name = ${this.param(localeParams, name)}`);
|
|
13132
|
+
if (description !== undefined) localeSets.push(`description = ${this.param(localeParams, description)}`);
|
|
13133
|
+
if (localeSets.length > 0) {
|
|
13134
|
+
await this.prisma.$queryRawUnsafe(
|
|
13135
|
+
`UPDATE operations_project_cost_category_locale SET ${localeSets.join(', ')} WHERE operations_project_cost_category_id = ${this.param(localeParams, id)} AND locale_id = ${this.param(localeParams, localeId)}`,
|
|
13136
|
+
...localeParams
|
|
13137
|
+
);
|
|
13138
|
+
}
|
|
13139
|
+
} else if (name) {
|
|
13140
|
+
await this.prisma.$queryRawUnsafe(
|
|
13141
|
+
`INSERT INTO operations_project_cost_category_locale (operations_project_cost_category_id, locale_id, name, description) VALUES ($1, $2, $3, $4)`,
|
|
13142
|
+
id, localeId, name, description ?? null
|
|
13143
|
+
);
|
|
13144
|
+
}
|
|
13145
|
+
}
|
|
13146
|
+
}
|
|
13147
|
+
|
|
13148
|
+
return this.querySingle<{ id: number; slug: string }>(
|
|
13149
|
+
`SELECT id, slug FROM operations_project_cost_category WHERE id = $1`,
|
|
13150
|
+
[id]
|
|
13151
|
+
);
|
|
13152
|
+
}
|
|
13153
|
+
|
|
13154
|
+
async deleteProjectCostCategory(userId: number, id: number) {
|
|
13155
|
+
const actor = await this.getActorContext(userId);
|
|
13156
|
+
this.ensureDirector(actor);
|
|
13157
|
+
|
|
13158
|
+
const category = await this.querySingle<{ id: number }>(
|
|
13159
|
+
`SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
13160
|
+
[id]
|
|
13161
|
+
);
|
|
13162
|
+
if (!category) {
|
|
13163
|
+
throw new NotFoundException('Project cost category not found.');
|
|
13164
|
+
}
|
|
13165
|
+
|
|
13166
|
+
await this.prisma.$queryRawUnsafe(
|
|
13167
|
+
`UPDATE operations_project_cost_category SET deleted_at = NOW() WHERE id = $1`,
|
|
13168
|
+
id
|
|
13169
|
+
);
|
|
13170
|
+
|
|
13171
|
+
return { success: true };
|
|
13172
|
+
}
|
|
13173
|
+
|
|
13174
|
+
async getProjectCostCategory(userId: number, id: number) {
|
|
13175
|
+
await this.getActorContext(userId);
|
|
13176
|
+
const localeId = await this.resolvePreferredLocaleId();
|
|
13177
|
+
|
|
13178
|
+
const row = await this.querySingle<{
|
|
13179
|
+
id: number;
|
|
13180
|
+
slug: string;
|
|
13181
|
+
name: string | null;
|
|
13182
|
+
description: string | null;
|
|
13183
|
+
icon: string | null;
|
|
13184
|
+
color: string | null;
|
|
13185
|
+
isActive: boolean;
|
|
13186
|
+
sortOrder: number;
|
|
13187
|
+
createdAt: string;
|
|
13188
|
+
}>(
|
|
13189
|
+
`SELECT pcc.id,
|
|
13190
|
+
pcc.slug,
|
|
13191
|
+
COALESCE(pccl.name, pcc.slug) AS name,
|
|
13192
|
+
pccl.description,
|
|
13193
|
+
pcc.icon,
|
|
13194
|
+
pcc.color,
|
|
13195
|
+
pcc.is_active AS "isActive",
|
|
13196
|
+
pcc.sort_order AS "sortOrder",
|
|
13197
|
+
pcc.created_at AS "createdAt"
|
|
13198
|
+
FROM operations_project_cost_category pcc
|
|
13199
|
+
LEFT JOIN LATERAL (
|
|
13200
|
+
SELECT l.name, l.description
|
|
13201
|
+
FROM operations_project_cost_category_locale l
|
|
13202
|
+
WHERE l.operations_project_cost_category_id = pcc.id
|
|
13203
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
13204
|
+
l.id ASC
|
|
13205
|
+
LIMIT 1
|
|
13206
|
+
) pccl ON TRUE
|
|
13207
|
+
WHERE pcc.id = $2 AND pcc.deleted_at IS NULL`,
|
|
13208
|
+
[localeId, id]
|
|
13209
|
+
);
|
|
13210
|
+
|
|
13211
|
+
if (!row) {
|
|
13212
|
+
throw new NotFoundException('Project cost category not found.');
|
|
13213
|
+
}
|
|
13214
|
+
|
|
13215
|
+
return row;
|
|
13216
|
+
}
|
|
13217
|
+
|
|
13218
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
13219
|
+
// Project Cost Types
|
|
13220
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
13221
|
+
|
|
13222
|
+
async listProjectCostTypes(userId: number, filters: { search?: string; category_id?: number; is_active?: boolean; default_calculation_type?: string; pageSize?: number; page?: number } = {}) {
|
|
13223
|
+
await this.getActorContext(userId);
|
|
13224
|
+
const localeId = await this.resolvePreferredLocaleId();
|
|
13225
|
+
|
|
13226
|
+
const params: unknown[] = [localeId];
|
|
13227
|
+
const where: string[] = ['pct.deleted_at IS NULL'];
|
|
13228
|
+
|
|
13229
|
+
if (filters.is_active === true) {
|
|
13230
|
+
where.push('pct.is_active = true');
|
|
13231
|
+
}
|
|
13232
|
+
|
|
13233
|
+
if (filters.category_id) {
|
|
13234
|
+
where.push(`pct.category_id = ${this.param(params, filters.category_id)}`);
|
|
13235
|
+
}
|
|
13236
|
+
|
|
13237
|
+
if (filters.default_calculation_type) {
|
|
13238
|
+
where.push(`pct.default_calculation_type = ${this.param(params, filters.default_calculation_type)}`);
|
|
13239
|
+
}
|
|
13240
|
+
|
|
13241
|
+
if (filters.search?.trim()) {
|
|
13242
|
+
const p = this.param(params, `%${filters.search.trim()}%`);
|
|
13243
|
+
where.push(`(COALESCE(pctl.name, pct.slug) ILIKE ${p} OR COALESCE(pct.code, '') ILIKE ${p} OR COALESCE(pct.slug, '') ILIKE ${p})`);
|
|
13244
|
+
}
|
|
13245
|
+
|
|
13246
|
+
const whereClause = `WHERE ${where.join(' AND ')}`;
|
|
13247
|
+
|
|
13248
|
+
return this.queryRows<{
|
|
13249
|
+
id: number;
|
|
13250
|
+
slug: string;
|
|
13251
|
+
code: string;
|
|
13252
|
+
name: string | null;
|
|
13253
|
+
description: string | null;
|
|
13254
|
+
categoryId: number | null;
|
|
13255
|
+
categorySlug: string | null;
|
|
13256
|
+
categoryName: string | null;
|
|
13257
|
+
defaultUnit: string | null;
|
|
13258
|
+
defaultCalculationType: string | null;
|
|
13259
|
+
isRecurringAllowed: boolean;
|
|
13260
|
+
isActive: boolean;
|
|
13261
|
+
sortOrder: number;
|
|
13262
|
+
createdAt: string;
|
|
13263
|
+
}>(
|
|
13264
|
+
`SELECT pct.id,
|
|
13265
|
+
pct.slug,
|
|
13266
|
+
pct.code,
|
|
13267
|
+
COALESCE(pctl.name, pct.slug) AS name,
|
|
13268
|
+
pctl.description,
|
|
13269
|
+
pct.category_id AS "categoryId",
|
|
13270
|
+
pcc.slug AS "categorySlug",
|
|
13271
|
+
COALESCE(pccl.name, pcc.slug) AS "categoryName",
|
|
13272
|
+
pct.default_unit AS "defaultUnit",
|
|
13273
|
+
pct.default_calculation_type AS "defaultCalculationType",
|
|
13274
|
+
pct.is_recurring_allowed AS "isRecurringAllowed",
|
|
13275
|
+
pct.is_active AS "isActive",
|
|
13276
|
+
pct.sort_order AS "sortOrder",
|
|
13277
|
+
pct.created_at AS "createdAt"
|
|
13278
|
+
FROM operations_project_cost_type pct
|
|
13279
|
+
LEFT JOIN operations_project_cost_category pcc
|
|
13280
|
+
ON pcc.id = pct.category_id AND pcc.deleted_at IS NULL
|
|
13281
|
+
LEFT JOIN LATERAL (
|
|
13282
|
+
SELECT l.name, l.description
|
|
13283
|
+
FROM operations_project_cost_category_locale l
|
|
13284
|
+
WHERE l.operations_project_cost_category_id = pcc.id
|
|
13285
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
13286
|
+
l.id ASC
|
|
13287
|
+
LIMIT 1
|
|
13288
|
+
) pccl ON TRUE
|
|
13289
|
+
LEFT JOIN LATERAL (
|
|
13290
|
+
SELECT l.name, l.description
|
|
13291
|
+
FROM operations_project_cost_type_locale l
|
|
13292
|
+
WHERE l.operations_project_cost_type_id = pct.id
|
|
13293
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
13294
|
+
l.id ASC
|
|
13295
|
+
LIMIT 1
|
|
13296
|
+
) pctl ON TRUE
|
|
13297
|
+
${whereClause}
|
|
13298
|
+
ORDER BY pct.sort_order ASC, COALESCE(pctl.name, pct.slug) ASC`,
|
|
13299
|
+
params
|
|
13300
|
+
);
|
|
13301
|
+
}
|
|
13302
|
+
|
|
13303
|
+
async createProjectCostType(userId: number, data: { category_id?: number; slug: string; code: string; name?: any; description?: any; default_unit?: string | null; default_calculation_type?: string | null; is_recurring_allowed?: boolean; is_active?: boolean; sort_order?: number }) {
|
|
13304
|
+
const actor = await this.getActorContext(userId);
|
|
13305
|
+
this.ensureDirector(actor);
|
|
13306
|
+
|
|
13307
|
+
const slug = data.slug?.trim();
|
|
13308
|
+
if (!slug) {
|
|
13309
|
+
throw new BadRequestException('Cost type slug is required.');
|
|
13310
|
+
}
|
|
13311
|
+
|
|
13312
|
+
if (data.category_id) {
|
|
13313
|
+
const category = await this.querySingle<{ id: number }>(
|
|
13314
|
+
`SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
13315
|
+
[data.category_id]
|
|
13316
|
+
);
|
|
13317
|
+
if (!category) {
|
|
13318
|
+
throw new BadRequestException(`Category with id ${data.category_id} not found.`);
|
|
13319
|
+
}
|
|
13320
|
+
}
|
|
13321
|
+
|
|
13322
|
+
const existingSlug = await this.querySingle<{ id: number }>(
|
|
13323
|
+
`SELECT id FROM operations_project_cost_type WHERE slug = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
13324
|
+
[slug]
|
|
13325
|
+
);
|
|
13326
|
+
if (existingSlug) {
|
|
13327
|
+
throw new ConflictException(`A cost type with slug '${slug}' already exists.`);
|
|
13328
|
+
}
|
|
13329
|
+
|
|
13330
|
+
const code = data.code?.trim() ?? slug;
|
|
13331
|
+
const existingCode = await this.querySingle<{ id: number }>(
|
|
13332
|
+
`SELECT id FROM operations_project_cost_type WHERE code = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
13333
|
+
[code]
|
|
13334
|
+
);
|
|
13335
|
+
if (existingCode) {
|
|
13336
|
+
throw new ConflictException(`A cost type with code '${code}' already exists.`);
|
|
13337
|
+
}
|
|
13338
|
+
|
|
13339
|
+
return this.prisma.$transaction(async (tx) => {
|
|
13340
|
+
const localeId = await this.resolvePreferredLocaleId(tx as any);
|
|
13341
|
+
|
|
13342
|
+
const created = (await (tx as any).$queryRawUnsafe(
|
|
13343
|
+
`INSERT INTO operations_project_cost_type (category_id, slug, code, default_unit, default_calculation_type, is_recurring_allowed, is_active, sort_order, created_at, updated_at)
|
|
13344
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW())
|
|
13345
|
+
RETURNING id`,
|
|
13346
|
+
data.category_id ?? null,
|
|
13347
|
+
slug,
|
|
13348
|
+
data.code?.trim() ?? slug,
|
|
13349
|
+
data.default_unit ?? null,
|
|
13350
|
+
data.default_calculation_type ?? 'fixed',
|
|
13351
|
+
data.is_recurring_allowed ?? true,
|
|
13352
|
+
data.is_active ?? true,
|
|
13353
|
+
data.sort_order ?? 0,
|
|
13354
|
+
)) as { id: number }[];
|
|
13355
|
+
|
|
13356
|
+
const createdId = created[0]?.id;
|
|
13357
|
+
if (!createdId) {
|
|
13358
|
+
throw new BadRequestException('Unable to create project cost type.');
|
|
13359
|
+
}
|
|
13360
|
+
|
|
13361
|
+
const name = typeof data.name === 'string' ? data.name : (data.name ? JSON.stringify(data.name) : slug);
|
|
13362
|
+
const description = typeof data.description === 'string' ? data.description : (data.description ? JSON.stringify(data.description) : null);
|
|
13363
|
+
|
|
13364
|
+
if (localeId && name) {
|
|
13365
|
+
await (tx as any).$executeRawUnsafe(
|
|
13366
|
+
`INSERT INTO operations_project_cost_type_locale (operations_project_cost_type_id, locale_id, name, description)
|
|
13367
|
+
VALUES ($1, $2, $3, $4)`,
|
|
13368
|
+
createdId,
|
|
13369
|
+
localeId,
|
|
13370
|
+
name,
|
|
13371
|
+
description ?? null,
|
|
13372
|
+
);
|
|
13373
|
+
}
|
|
13374
|
+
|
|
13375
|
+
const rows = (await (tx as any).$queryRawUnsafe(
|
|
13376
|
+
`SELECT pct.id,
|
|
13377
|
+
pct.slug,
|
|
13378
|
+
pct.code,
|
|
13379
|
+
COALESCE(pctl.name, pct.slug) AS name,
|
|
13380
|
+
pctl.description,
|
|
13381
|
+
pct.category_id AS "categoryId",
|
|
13382
|
+
pct.default_unit AS "defaultUnit",
|
|
13383
|
+
pct.default_calculation_type AS "defaultCalculationType",
|
|
13384
|
+
pct.is_recurring_allowed AS "isRecurringAllowed",
|
|
13385
|
+
pct.is_active AS "isActive",
|
|
13386
|
+
pct.sort_order AS "sortOrder",
|
|
13387
|
+
pct.created_at AS "createdAt"
|
|
13388
|
+
FROM operations_project_cost_type pct
|
|
13389
|
+
LEFT JOIN operations_project_cost_type_locale pctl
|
|
13390
|
+
ON pctl.operations_project_cost_type_id = pct.id AND pctl.locale_id = $2
|
|
13391
|
+
WHERE pct.id = $1`,
|
|
13392
|
+
createdId,
|
|
13393
|
+
localeId,
|
|
13394
|
+
)) as any[];
|
|
13395
|
+
|
|
13396
|
+
return rows[0] ?? null;
|
|
13397
|
+
});
|
|
13398
|
+
}
|
|
13399
|
+
|
|
13400
|
+
async updateProjectCostType(userId: number, id: number, data: Partial<{ category_id: number; slug: string; code: string; name?: any; description?: any; default_unit?: string | null; default_calculation_type?: string | null; is_recurring_allowed?: boolean; is_active?: boolean; sort_order?: number }>) {
|
|
13401
|
+
const actor = await this.getActorContext(userId);
|
|
13402
|
+
this.ensureDirector(actor);
|
|
13403
|
+
|
|
13404
|
+
const costType = await this.querySingle<{ id: number }>(
|
|
13405
|
+
`SELECT id FROM operations_project_cost_type WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
13406
|
+
[id]
|
|
13407
|
+
);
|
|
13408
|
+
if (!costType) {
|
|
13409
|
+
throw new NotFoundException('Project cost type not found.');
|
|
13410
|
+
}
|
|
13411
|
+
|
|
13412
|
+
if (data.category_id !== undefined && data.category_id !== null) {
|
|
13413
|
+
const category = await this.querySingle<{ id: number }>(
|
|
13414
|
+
`SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
13415
|
+
[data.category_id]
|
|
13416
|
+
);
|
|
13417
|
+
if (!category) {
|
|
13418
|
+
throw new BadRequestException(`Category with id ${data.category_id} not found.`);
|
|
13419
|
+
}
|
|
13420
|
+
}
|
|
13421
|
+
|
|
13422
|
+
if (data.slug !== undefined) {
|
|
13423
|
+
const existingSlug = await this.querySingle<{ id: number }>(
|
|
13424
|
+
`SELECT id FROM operations_project_cost_type WHERE slug = $1 AND id != $2 AND deleted_at IS NULL LIMIT 1`,
|
|
13425
|
+
[data.slug, id]
|
|
13426
|
+
);
|
|
13427
|
+
if (existingSlug) {
|
|
13428
|
+
throw new ConflictException(`A cost type with slug '${data.slug}' already exists.`);
|
|
13429
|
+
}
|
|
13430
|
+
}
|
|
13431
|
+
|
|
13432
|
+
if (data.code !== undefined) {
|
|
13433
|
+
const existingCode = await this.querySingle<{ id: number }>(
|
|
13434
|
+
`SELECT id FROM operations_project_cost_type WHERE code = $1 AND id != $2 AND deleted_at IS NULL LIMIT 1`,
|
|
13435
|
+
[data.code, id]
|
|
13436
|
+
);
|
|
13437
|
+
if (existingCode) {
|
|
13438
|
+
throw new ConflictException(`A cost type with code '${data.code}' already exists.`);
|
|
13439
|
+
}
|
|
13440
|
+
}
|
|
13441
|
+
|
|
13442
|
+
const sets: string[] = [];
|
|
13443
|
+
const params: unknown[] = [];
|
|
13444
|
+
|
|
13445
|
+
if (data.category_id !== undefined) sets.push(`category_id = ${this.param(params, data.category_id)}`);
|
|
13446
|
+
if (data.slug !== undefined) sets.push(`slug = ${this.param(params, data.slug)}`);
|
|
13447
|
+
if (data.code !== undefined) sets.push(`code = ${this.param(params, data.code)}`);
|
|
13448
|
+
if (data.default_unit !== undefined) sets.push(`default_unit = ${this.param(params, data.default_unit)}`);
|
|
13449
|
+
if (data.default_calculation_type !== undefined) sets.push(`default_calculation_type = ${this.param(params, data.default_calculation_type)}`);
|
|
13450
|
+
if (data.is_recurring_allowed !== undefined) sets.push(`is_recurring_allowed = ${this.param(params, data.is_recurring_allowed)}`);
|
|
13451
|
+
if (data.is_active !== undefined) sets.push(`is_active = ${this.param(params, data.is_active)}`);
|
|
13452
|
+
if (data.sort_order !== undefined) sets.push(`sort_order = ${this.param(params, data.sort_order)}`);
|
|
13453
|
+
|
|
13454
|
+
if (sets.length > 0) {
|
|
13455
|
+
sets.push(`updated_at = NOW()`);
|
|
13456
|
+
await this.prisma.$queryRawUnsafe(
|
|
13457
|
+
`UPDATE operations_project_cost_type SET ${sets.join(', ')} WHERE id = ${this.param(params, id)}`,
|
|
13458
|
+
...params
|
|
13459
|
+
);
|
|
13460
|
+
}
|
|
13461
|
+
|
|
13462
|
+
if (data.name !== undefined || data.description !== undefined) {
|
|
13463
|
+
const localeId = await this.resolvePreferredLocaleId();
|
|
13464
|
+
if (localeId) {
|
|
13465
|
+
const name = typeof data.name === 'string' ? data.name : (data.name ? JSON.stringify(data.name) : undefined);
|
|
13466
|
+
const description = typeof data.description === 'string' ? data.description : (data.description ? JSON.stringify(data.description) : null);
|
|
13467
|
+
const existing = await this.querySingle<{ id: number }>(
|
|
13468
|
+
`SELECT id FROM operations_project_cost_type_locale WHERE operations_project_cost_type_id = $1 AND locale_id = $2 LIMIT 1`,
|
|
13469
|
+
[id, localeId]
|
|
13470
|
+
);
|
|
13471
|
+
if (existing) {
|
|
13472
|
+
const localeSets: string[] = [];
|
|
13473
|
+
const localeParams: unknown[] = [];
|
|
13474
|
+
if (name !== undefined) localeSets.push(`name = ${this.param(localeParams, name)}`);
|
|
13475
|
+
if (description !== undefined) localeSets.push(`description = ${this.param(localeParams, description)}`);
|
|
13476
|
+
if (localeSets.length > 0) {
|
|
13477
|
+
await this.prisma.$queryRawUnsafe(
|
|
13478
|
+
`UPDATE operations_project_cost_type_locale SET ${localeSets.join(', ')} WHERE operations_project_cost_type_id = ${this.param(localeParams, id)} AND locale_id = ${this.param(localeParams, localeId)}`,
|
|
13479
|
+
...localeParams
|
|
13480
|
+
);
|
|
13481
|
+
}
|
|
13482
|
+
} else if (name) {
|
|
13483
|
+
await this.prisma.$queryRawUnsafe(
|
|
13484
|
+
`INSERT INTO operations_project_cost_type_locale (operations_project_cost_type_id, locale_id, name, description) VALUES ($1, $2, $3, $4)`,
|
|
13485
|
+
id, localeId, name, description ?? null
|
|
13486
|
+
);
|
|
13487
|
+
}
|
|
13488
|
+
}
|
|
13489
|
+
}
|
|
13490
|
+
|
|
13491
|
+
return this.querySingle<{ id: number; slug: string }>(
|
|
13492
|
+
`SELECT id, slug FROM operations_project_cost_type WHERE id = $1`,
|
|
13493
|
+
[id]
|
|
13494
|
+
);
|
|
13495
|
+
}
|
|
13496
|
+
|
|
13497
|
+
async deleteProjectCostType(userId: number, id: number) {
|
|
13498
|
+
const actor = await this.getActorContext(userId);
|
|
13499
|
+
this.ensureDirector(actor);
|
|
13500
|
+
|
|
13501
|
+
const costType = await this.querySingle<{ id: number }>(
|
|
13502
|
+
`SELECT id FROM operations_project_cost_type WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
13503
|
+
[id]
|
|
13504
|
+
);
|
|
13505
|
+
if (!costType) {
|
|
13506
|
+
throw new NotFoundException('Project cost type not found.');
|
|
13507
|
+
}
|
|
13508
|
+
|
|
13509
|
+
await this.prisma.$queryRawUnsafe(
|
|
13510
|
+
`UPDATE operations_project_cost_type SET deleted_at = NOW() WHERE id = $1`,
|
|
13511
|
+
id
|
|
13512
|
+
);
|
|
13513
|
+
|
|
13514
|
+
return { success: true };
|
|
13515
|
+
}
|
|
13516
|
+
|
|
13517
|
+
async getProjectCostType(userId: number, id: number) {
|
|
13518
|
+
await this.getActorContext(userId);
|
|
13519
|
+
const localeId = await this.resolvePreferredLocaleId();
|
|
13520
|
+
|
|
13521
|
+
const row = await this.querySingle<{
|
|
13522
|
+
id: number;
|
|
13523
|
+
slug: string;
|
|
13524
|
+
code: string;
|
|
13525
|
+
name: string | null;
|
|
13526
|
+
description: string | null;
|
|
13527
|
+
default_unit: string | null;
|
|
13528
|
+
default_calculation_type: string | null;
|
|
13529
|
+
is_recurring_allowed: boolean;
|
|
13530
|
+
is_active: boolean;
|
|
13531
|
+
sort_order: number;
|
|
13532
|
+
category_id: number | null;
|
|
13533
|
+
category: { id: number; slug: string; name: string | null; color: string | null; icon: string | null } | null;
|
|
13534
|
+
}>(
|
|
13535
|
+
`SELECT pct.id,
|
|
13536
|
+
pct.slug,
|
|
13537
|
+
pct.code,
|
|
13538
|
+
COALESCE(pctl.name, pct.slug) AS name,
|
|
13539
|
+
pctl.description,
|
|
13540
|
+
pct.default_unit,
|
|
13541
|
+
pct.default_calculation_type,
|
|
13542
|
+
pct.is_recurring_allowed,
|
|
13543
|
+
pct.is_active,
|
|
13544
|
+
pct.sort_order,
|
|
13545
|
+
pct.category_id,
|
|
13546
|
+
CASE WHEN pcc.id IS NOT NULL THEN
|
|
13547
|
+
jsonb_build_object(
|
|
13548
|
+
'id', pcc.id,
|
|
13549
|
+
'slug', pcc.slug,
|
|
13550
|
+
'name', COALESCE(pccl.name, pcc.slug),
|
|
13551
|
+
'color', pcc.color,
|
|
13552
|
+
'icon', pcc.icon
|
|
13553
|
+
)
|
|
13554
|
+
ELSE NULL END AS category
|
|
13555
|
+
FROM operations_project_cost_type pct
|
|
13556
|
+
LEFT JOIN operations_project_cost_category pcc
|
|
13557
|
+
ON pcc.id = pct.category_id AND pcc.deleted_at IS NULL
|
|
13558
|
+
LEFT JOIN LATERAL (
|
|
13559
|
+
SELECT l.name, l.description
|
|
13560
|
+
FROM operations_project_cost_category_locale l
|
|
13561
|
+
WHERE l.operations_project_cost_category_id = pcc.id
|
|
13562
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
13563
|
+
l.id ASC
|
|
13564
|
+
LIMIT 1
|
|
13565
|
+
) pccl ON TRUE
|
|
13566
|
+
LEFT JOIN LATERAL (
|
|
13567
|
+
SELECT l.name, l.description
|
|
13568
|
+
FROM operations_project_cost_type_locale l
|
|
13569
|
+
WHERE l.operations_project_cost_type_id = pct.id
|
|
13570
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
13571
|
+
l.id ASC
|
|
13572
|
+
LIMIT 1
|
|
13573
|
+
) pctl ON TRUE
|
|
13574
|
+
WHERE pct.id = $2 AND pct.deleted_at IS NULL`,
|
|
13575
|
+
[localeId, id]
|
|
13576
|
+
);
|
|
13577
|
+
|
|
13578
|
+
if (!row) {
|
|
13579
|
+
throw new NotFoundException('Project cost type not found.');
|
|
13580
|
+
}
|
|
13581
|
+
|
|
13582
|
+
return row;
|
|
13583
|
+
}
|
|
13584
|
+
|
|
13585
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
13586
|
+
// Project Costs
|
|
13587
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
13588
|
+
|
|
13589
|
+
async listProjectCosts(userId: number, projectId: number, filters: {
|
|
13590
|
+
search?: string;
|
|
13591
|
+
cost_type_id?: number;
|
|
13592
|
+
category_id?: number;
|
|
13593
|
+
recurrence_type?: string;
|
|
13594
|
+
calculation_type?: string;
|
|
13595
|
+
status?: string;
|
|
13596
|
+
is_billable?: boolean;
|
|
13597
|
+
is_reimbursable?: boolean;
|
|
13598
|
+
date_from?: string;
|
|
13599
|
+
date_to?: string;
|
|
13600
|
+
} = {}) {
|
|
13601
|
+
await this.getActorContext(userId);
|
|
13602
|
+
const localeId = await this.resolvePreferredLocaleId();
|
|
13603
|
+
|
|
13604
|
+
const params: unknown[] = [localeId, projectId];
|
|
13605
|
+
const where: string[] = ['pc.deleted_at IS NULL', 'pc.project_id = $2'];
|
|
13606
|
+
|
|
13607
|
+
if (filters.cost_type_id) {
|
|
13608
|
+
where.push(`pc.cost_type_id = ${this.param(params, filters.cost_type_id)}`);
|
|
13609
|
+
}
|
|
13610
|
+
|
|
13611
|
+
if (filters.category_id) {
|
|
13612
|
+
where.push(`COALESCE(pc.category_id, pct.category_id) = ${this.param(params, filters.category_id)}`);
|
|
13613
|
+
}
|
|
13614
|
+
|
|
13615
|
+
if (filters.recurrence_type) {
|
|
13616
|
+
where.push(`pc.recurrence_type = ${this.param(params, filters.recurrence_type)}`);
|
|
13617
|
+
}
|
|
13618
|
+
|
|
13619
|
+
if (filters.calculation_type) {
|
|
13620
|
+
where.push(`pc.calculation_type = ${this.param(params, filters.calculation_type)}`);
|
|
13621
|
+
}
|
|
13622
|
+
|
|
13623
|
+
if (filters.status) {
|
|
13624
|
+
where.push(`pc.status = ${this.param(params, filters.status)}`);
|
|
13625
|
+
}
|
|
13626
|
+
|
|
13627
|
+
if (filters.is_billable !== undefined) {
|
|
13628
|
+
where.push(`pc.is_billable = ${this.param(params, filters.is_billable)}`);
|
|
13629
|
+
}
|
|
13630
|
+
|
|
13631
|
+
if (filters.is_reimbursable !== undefined) {
|
|
13632
|
+
where.push(`pc.is_reimbursable = ${this.param(params, filters.is_reimbursable)}`);
|
|
13633
|
+
}
|
|
13634
|
+
|
|
13635
|
+
if (filters.date_from) {
|
|
13636
|
+
where.push(`pc.cost_date >= ${this.param(params, filters.date_from)}::date`);
|
|
13637
|
+
}
|
|
13638
|
+
|
|
13639
|
+
if (filters.date_to) {
|
|
13640
|
+
where.push(`pc.cost_date <= ${this.param(params, filters.date_to)}::date`);
|
|
13641
|
+
}
|
|
13642
|
+
|
|
13643
|
+
if (filters.search?.trim()) {
|
|
13644
|
+
const p = this.param(params, `%${filters.search.trim()}%`);
|
|
13645
|
+
where.push(`(COALESCE(pc.description, '') ILIKE ${p} OR COALESCE(pc.notes, '') ILIKE ${p})`);
|
|
13646
|
+
}
|
|
13647
|
+
|
|
13648
|
+
const whereClause = `WHERE ${where.join(' AND ')}`;
|
|
13649
|
+
|
|
13650
|
+
const rows = await this.queryRows<{
|
|
13651
|
+
id: number;
|
|
13652
|
+
projectId: number;
|
|
13653
|
+
costTypeId: number | null;
|
|
13654
|
+
costTypeSlug: string | null;
|
|
13655
|
+
costTypeCode: string | null;
|
|
13656
|
+
costTypeName: string | null;
|
|
13657
|
+
categoryId: number | null;
|
|
13658
|
+
resolvedCategoryId: number | null;
|
|
13659
|
+
categorySlug: string | null;
|
|
13660
|
+
categoryName: string | null;
|
|
13661
|
+
categoryColor: string | null;
|
|
13662
|
+
categoryIcon: string | null;
|
|
13663
|
+
description: string | null;
|
|
13664
|
+
amount: string;
|
|
13665
|
+
quantity: string;
|
|
13666
|
+
unitAmount: string | null;
|
|
13667
|
+
currency: string;
|
|
13668
|
+
costDate: string | null;
|
|
13669
|
+
periodStart: string | null;
|
|
13670
|
+
periodEnd: string | null;
|
|
13671
|
+
calculationType: string;
|
|
13672
|
+
recurrenceType: string;
|
|
13673
|
+
isBillable: boolean;
|
|
13674
|
+
isReimbursable: boolean;
|
|
13675
|
+
notes: string | null;
|
|
13676
|
+
status: string;
|
|
13677
|
+
createdAt: string;
|
|
13678
|
+
}>(
|
|
13679
|
+
`SELECT pc.id,
|
|
13680
|
+
pc.project_id AS "projectId",
|
|
13681
|
+
pc.cost_type_id AS "costTypeId",
|
|
13682
|
+
pct.slug AS "costTypeSlug",
|
|
13683
|
+
pct.code AS "costTypeCode",
|
|
13684
|
+
COALESCE(pctl.name, pct.slug) AS "costTypeName",
|
|
13685
|
+
pc.category_id AS "categoryId",
|
|
13686
|
+
COALESCE(pc.category_id, pct.category_id) AS "resolvedCategoryId",
|
|
13687
|
+
pcc.slug AS "categorySlug",
|
|
13688
|
+
COALESCE(pccl.name, pcc.slug) AS "categoryName",
|
|
13689
|
+
pcc.color AS "categoryColor",
|
|
13690
|
+
pcc.icon AS "categoryIcon",
|
|
13691
|
+
pc.description,
|
|
13692
|
+
pc.amount::text AS amount,
|
|
13693
|
+
pc.quantity::text AS quantity,
|
|
13694
|
+
pc.unit_amount::text AS "unitAmount",
|
|
13695
|
+
pc.currency,
|
|
13696
|
+
TO_CHAR(pc.cost_date, 'YYYY-MM-DD') AS "costDate",
|
|
13697
|
+
TO_CHAR(pc.period_start, 'YYYY-MM-DD') AS "periodStart",
|
|
13698
|
+
TO_CHAR(pc.period_end, 'YYYY-MM-DD') AS "periodEnd",
|
|
13699
|
+
pc.calculation_type AS "calculationType",
|
|
13700
|
+
pc.recurrence_type AS "recurrenceType",
|
|
13701
|
+
pc.is_billable AS "isBillable",
|
|
13702
|
+
pc.is_reimbursable AS "isReimbursable",
|
|
13703
|
+
pc.notes,
|
|
13704
|
+
pc.status,
|
|
13705
|
+
pc.created_at AS "createdAt"
|
|
13706
|
+
FROM operations_project_cost pc
|
|
13707
|
+
LEFT JOIN operations_project_cost_type pct
|
|
13708
|
+
ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
|
|
13709
|
+
LEFT JOIN operations_project_cost_category pcc
|
|
13710
|
+
ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
|
|
13711
|
+
LEFT JOIN LATERAL (
|
|
13712
|
+
SELECT l.name
|
|
13713
|
+
FROM operations_project_cost_type_locale l
|
|
13714
|
+
WHERE l.operations_project_cost_type_id = pct.id
|
|
13715
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
13716
|
+
l.id ASC
|
|
13717
|
+
LIMIT 1
|
|
13718
|
+
) pctl ON TRUE
|
|
13719
|
+
LEFT JOIN LATERAL (
|
|
13720
|
+
SELECT l.name
|
|
13721
|
+
FROM operations_project_cost_category_locale l
|
|
13722
|
+
WHERE l.operations_project_cost_category_id = pcc.id
|
|
13723
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
13724
|
+
l.id ASC
|
|
13725
|
+
LIMIT 1
|
|
13726
|
+
) pccl ON TRUE
|
|
13727
|
+
${whereClause}
|
|
13728
|
+
ORDER BY pc.created_at DESC`,
|
|
13729
|
+
params
|
|
13730
|
+
);
|
|
13731
|
+
|
|
13732
|
+
return rows.map((row) => ({
|
|
13733
|
+
id: row.id,
|
|
13734
|
+
project_id: row.projectId,
|
|
13735
|
+
cost_type_id: row.costTypeId,
|
|
13736
|
+
category_id: row.categoryId,
|
|
13737
|
+
description: row.description,
|
|
13738
|
+
amount: row.amount,
|
|
13739
|
+
quantity: row.quantity,
|
|
13740
|
+
unit_amount: row.unitAmount,
|
|
13741
|
+
currency: row.currency,
|
|
13742
|
+
cost_date: row.costDate,
|
|
13743
|
+
period_start: row.periodStart,
|
|
13744
|
+
period_end: row.periodEnd,
|
|
13745
|
+
calculation_type: row.calculationType,
|
|
13746
|
+
recurrence_type: row.recurrenceType,
|
|
13747
|
+
is_billable: row.isBillable,
|
|
13748
|
+
is_reimbursable: row.isReimbursable,
|
|
13749
|
+
notes: row.notes,
|
|
13750
|
+
status: row.status,
|
|
13751
|
+
created_at: row.createdAt,
|
|
13752
|
+
cost_type: row.costTypeId
|
|
13753
|
+
? { id: row.costTypeId, slug: row.costTypeSlug, name: row.costTypeName, code: row.costTypeCode }
|
|
13754
|
+
: null,
|
|
13755
|
+
category: row.resolvedCategoryId
|
|
13756
|
+
? { id: row.resolvedCategoryId, slug: row.categorySlug, name: row.categoryName, color: row.categoryColor, icon: row.categoryIcon }
|
|
13757
|
+
: null,
|
|
13758
|
+
}));
|
|
13759
|
+
}
|
|
13760
|
+
|
|
13761
|
+
async getProjectCostsSummaryGrouped(userId: number, projectId: number) {
|
|
13762
|
+
const items = await this.listProjectCosts(userId, projectId, {});
|
|
13763
|
+
|
|
13764
|
+
// Group by resolved category
|
|
13765
|
+
const categoryMap = new Map<
|
|
13766
|
+
number | null,
|
|
13767
|
+
{
|
|
13768
|
+
category: { id: number; slug: string | null; name: string | null; color: string | null; icon: string | null } | null;
|
|
13769
|
+
items: typeof items;
|
|
13770
|
+
total_amount: number;
|
|
13771
|
+
}
|
|
13772
|
+
>();
|
|
13773
|
+
|
|
13774
|
+
for (const cost of items) {
|
|
13775
|
+
const cat = cost.category ?? null;
|
|
13776
|
+
const key = cat?.id ?? null;
|
|
13777
|
+
if (!categoryMap.has(key)) {
|
|
13778
|
+
categoryMap.set(key, { category: cat, items: [], total_amount: 0 });
|
|
13779
|
+
}
|
|
13780
|
+
const group = categoryMap.get(key)!;
|
|
13781
|
+
group.items.push(cost);
|
|
13782
|
+
group.total_amount += (parseFloat(String(cost.amount)) || 0) * (parseFloat(String(cost.quantity)) || 1);
|
|
13783
|
+
}
|
|
13784
|
+
|
|
13785
|
+
const grand_total = Array.from(categoryMap.values()).reduce(
|
|
13786
|
+
(sum, g) => sum + g.total_amount,
|
|
13787
|
+
0,
|
|
13788
|
+
);
|
|
13789
|
+
|
|
13790
|
+
return {
|
|
13791
|
+
categories: Array.from(categoryMap.values()).map((g) => ({
|
|
13792
|
+
category: g.category,
|
|
13793
|
+
items: g.items,
|
|
13794
|
+
total_amount: Math.round(g.total_amount * 100) / 100,
|
|
13795
|
+
count: g.items.length,
|
|
13796
|
+
})),
|
|
13797
|
+
grand_total: Math.round(grand_total * 100) / 100,
|
|
13798
|
+
};
|
|
13799
|
+
}
|
|
13800
|
+
|
|
13801
|
+
async getProjectCost(userId: number, projectId: number, id: number) {
|
|
13802
|
+
const rows = await this.listProjectCosts(userId, projectId, {});
|
|
13803
|
+
const cost = rows.find((r) => r.id === id);
|
|
13804
|
+
if (!cost) {
|
|
13805
|
+
throw new NotFoundException('Project cost not found.');
|
|
13806
|
+
}
|
|
13807
|
+
return cost;
|
|
13808
|
+
}
|
|
13809
|
+
|
|
13810
|
+
async getProjectCostsSummary(userId: number, projectId: number) {
|
|
13811
|
+
await this.getActorContext(userId);
|
|
13812
|
+
const localeId = await this.resolvePreferredLocaleId();
|
|
13813
|
+
|
|
13814
|
+
// ── 1. Verify project exists and fetch budget_amount ──────────────────
|
|
13815
|
+
const project = await this.querySingle<{ id: number; budgetAmount: string | null }>(
|
|
13816
|
+
`SELECT id, budget_amount::text AS "budgetAmount"
|
|
13817
|
+
FROM operations_project
|
|
13818
|
+
WHERE id = $1 AND deleted_at IS NULL
|
|
13819
|
+
LIMIT 1`,
|
|
13820
|
+
[projectId]
|
|
13821
|
+
);
|
|
13822
|
+
if (!project) {
|
|
13823
|
+
throw new NotFoundException('Project not found.');
|
|
13824
|
+
}
|
|
13825
|
+
|
|
13826
|
+
const budgetAmount = parseFloat(project.budgetAmount ?? '0') || 0;
|
|
13827
|
+
|
|
13828
|
+
// ── 2. Aggregated cost totals ─────────────────────────────────────────
|
|
13829
|
+
const totals = await this.querySingle<{
|
|
13830
|
+
extraCostTotal: string;
|
|
13831
|
+
plannedTotal: string;
|
|
13832
|
+
approvedTotal: string;
|
|
13833
|
+
realizedTotal: string;
|
|
13834
|
+
cancelledTotal: string;
|
|
13835
|
+
billableTotal: string;
|
|
13836
|
+
nonBillableTotal: string;
|
|
13837
|
+
reimbursableTotal: string;
|
|
13838
|
+
}>(
|
|
13839
|
+
`SELECT
|
|
13840
|
+
COALESCE(SUM(CASE WHEN status != 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "extraCostTotal",
|
|
13841
|
+
COALESCE(SUM(CASE WHEN status = 'planned' THEN amount * quantity ELSE 0 END), 0)::text AS "plannedTotal",
|
|
13842
|
+
COALESCE(SUM(CASE WHEN status = 'approved' THEN amount * quantity ELSE 0 END), 0)::text AS "approvedTotal",
|
|
13843
|
+
COALESCE(SUM(CASE WHEN status = 'realized' THEN amount * quantity ELSE 0 END), 0)::text AS "realizedTotal",
|
|
13844
|
+
COALESCE(SUM(CASE WHEN status = 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "cancelledTotal",
|
|
13845
|
+
COALESCE(SUM(CASE WHEN is_billable = true AND status != 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "billableTotal",
|
|
13846
|
+
COALESCE(SUM(CASE WHEN is_billable = false AND status != 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "nonBillableTotal",
|
|
13847
|
+
COALESCE(SUM(CASE WHEN is_reimbursable = true AND status != 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "reimbursableTotal"
|
|
13848
|
+
FROM operations_project_cost
|
|
13849
|
+
WHERE deleted_at IS NULL
|
|
13850
|
+
AND project_id = $1`,
|
|
13851
|
+
[projectId]
|
|
13852
|
+
);
|
|
13853
|
+
|
|
13854
|
+
const extraCostTotal = Math.round((parseFloat(totals?.extraCostTotal ?? '0') || 0) * 100) / 100;
|
|
13855
|
+
const plannedTotal = Math.round((parseFloat(totals?.plannedTotal ?? '0') || 0) * 100) / 100;
|
|
13856
|
+
const approvedTotal = Math.round((parseFloat(totals?.approvedTotal ?? '0') || 0) * 100) / 100;
|
|
13857
|
+
const realizedTotal = Math.round((parseFloat(totals?.realizedTotal ?? '0') || 0) * 100) / 100;
|
|
13858
|
+
const cancelledTotal = Math.round((parseFloat(totals?.cancelledTotal ?? '0') || 0) * 100) / 100;
|
|
13859
|
+
const billableTotal = Math.round((parseFloat(totals?.billableTotal ?? '0') || 0) * 100) / 100;
|
|
13860
|
+
const nonBillableTotal = Math.round((parseFloat(totals?.nonBillableTotal ?? '0') || 0) * 100) / 100;
|
|
13861
|
+
const reimbursableTotal = Math.round((parseFloat(totals?.reimbursableTotal ?? '0') || 0) * 100) / 100;
|
|
13862
|
+
|
|
13863
|
+
const teamCostTotal = 0;
|
|
13864
|
+
const totalProjectCost = Math.round((teamCostTotal + extraCostTotal) * 100) / 100;
|
|
13865
|
+
const remainingBudget = Math.round((budgetAmount - totalProjectCost) * 100) / 100;
|
|
13866
|
+
const budgetUsagePercent = budgetAmount > 0
|
|
13867
|
+
? Math.round((totalProjectCost / budgetAmount) * 10000) / 100
|
|
13868
|
+
: 0;
|
|
13869
|
+
|
|
13870
|
+
// ── 3. cost_by_category ───────────────────────────────────────────────
|
|
13871
|
+
const costByCategory = await this.queryRows<{
|
|
13872
|
+
categoryId: number | null;
|
|
13873
|
+
categorySlug: string | null;
|
|
13874
|
+
categoryName: string | null;
|
|
13875
|
+
categoryColor: string | null;
|
|
13876
|
+
categoryIcon: string | null;
|
|
13877
|
+
total: string;
|
|
13878
|
+
count: number;
|
|
13879
|
+
}>(
|
|
13880
|
+
`SELECT
|
|
13881
|
+
COALESCE(pc.category_id, pct.category_id) AS "categoryId",
|
|
13882
|
+
pcc.slug AS "categorySlug",
|
|
13883
|
+
COALESCE(pccl.name, pcc.slug) AS "categoryName",
|
|
13884
|
+
pcc.color AS "categoryColor",
|
|
13885
|
+
pcc.icon AS "categoryIcon",
|
|
13886
|
+
SUM(pc.amount * pc.quantity)::text AS total,
|
|
13887
|
+
COUNT(*)::int AS count
|
|
13888
|
+
FROM operations_project_cost pc
|
|
13889
|
+
LEFT JOIN operations_project_cost_type pct
|
|
13890
|
+
ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
|
|
13891
|
+
LEFT JOIN operations_project_cost_category pcc
|
|
13892
|
+
ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
|
|
13893
|
+
LEFT JOIN LATERAL (
|
|
13894
|
+
SELECT l.name
|
|
13895
|
+
FROM operations_project_cost_category_locale l
|
|
13896
|
+
WHERE l.operations_project_cost_category_id = pcc.id
|
|
13897
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
13898
|
+
l.id ASC
|
|
13899
|
+
LIMIT 1
|
|
13900
|
+
) pccl ON TRUE
|
|
13901
|
+
WHERE pc.deleted_at IS NULL
|
|
13902
|
+
AND pc.project_id = $2
|
|
13903
|
+
AND pc.status != 'cancelled'
|
|
13904
|
+
GROUP BY COALESCE(pc.category_id, pct.category_id), pcc.slug, pcc.color, pcc.icon, pccl.name
|
|
13905
|
+
ORDER BY SUM(pc.amount * pc.quantity) DESC`,
|
|
13906
|
+
[localeId, projectId]
|
|
13907
|
+
);
|
|
13908
|
+
|
|
13909
|
+
// ── 4. cost_by_type ───────────────────────────────────────────────────
|
|
13910
|
+
const costByType = await this.queryRows<{
|
|
13911
|
+
costTypeId: number | null;
|
|
13912
|
+
costTypeSlug: string | null;
|
|
13913
|
+
costTypeName: string | null;
|
|
13914
|
+
costTypeCode: string | null;
|
|
13915
|
+
total: string;
|
|
13916
|
+
count: number;
|
|
13917
|
+
}>(
|
|
13918
|
+
`SELECT
|
|
13919
|
+
pc.cost_type_id AS "costTypeId",
|
|
13920
|
+
pct.slug AS "costTypeSlug",
|
|
13921
|
+
COALESCE(pctl.name, pct.slug) AS "costTypeName",
|
|
13922
|
+
pct.code AS "costTypeCode",
|
|
13923
|
+
SUM(pc.amount * pc.quantity)::text AS total,
|
|
13924
|
+
COUNT(*)::int AS count
|
|
13925
|
+
FROM operations_project_cost pc
|
|
13926
|
+
LEFT JOIN operations_project_cost_type pct
|
|
13927
|
+
ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
|
|
13928
|
+
LEFT JOIN LATERAL (
|
|
13929
|
+
SELECT l.name
|
|
13930
|
+
FROM operations_project_cost_type_locale l
|
|
13931
|
+
WHERE l.operations_project_cost_type_id = pct.id
|
|
13932
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
13933
|
+
l.id ASC
|
|
13934
|
+
LIMIT 1
|
|
13935
|
+
) pctl ON TRUE
|
|
13936
|
+
WHERE pc.deleted_at IS NULL
|
|
13937
|
+
AND pc.project_id = $2
|
|
13938
|
+
AND pc.status != 'cancelled'
|
|
13939
|
+
GROUP BY pc.cost_type_id, pct.slug, pct.code, pctl.name
|
|
13940
|
+
ORDER BY SUM(pc.amount * pc.quantity) DESC`,
|
|
13941
|
+
[localeId, projectId]
|
|
13942
|
+
);
|
|
13943
|
+
|
|
13944
|
+
// ── 5. cost_by_month ──────────────────────────────────────────────────
|
|
13945
|
+
const costByMonth = await this.queryRows<{
|
|
13946
|
+
month: string;
|
|
13947
|
+
total: string;
|
|
13948
|
+
count: number;
|
|
13949
|
+
}>(
|
|
13950
|
+
`SELECT
|
|
13951
|
+
TO_CHAR(COALESCE(pc.cost_date, pc.created_at), 'YYYY-MM') AS month,
|
|
13952
|
+
SUM(pc.amount * pc.quantity)::text AS total,
|
|
13953
|
+
COUNT(*)::int AS count
|
|
13954
|
+
FROM operations_project_cost pc
|
|
13955
|
+
WHERE pc.deleted_at IS NULL
|
|
13956
|
+
AND pc.project_id = $1
|
|
13957
|
+
AND pc.status != 'cancelled'
|
|
13958
|
+
GROUP BY TO_CHAR(COALESCE(pc.cost_date, pc.created_at), 'YYYY-MM')
|
|
13959
|
+
ORDER BY month ASC`,
|
|
13960
|
+
[projectId]
|
|
13961
|
+
);
|
|
13962
|
+
|
|
13963
|
+
// ── 6. top_cost_types (top 5) ─────────────────────────────────────────
|
|
13964
|
+
const topCostTypes = costByType.slice(0, 5).map((ct) => {
|
|
13965
|
+
const typeTotal = Math.round((parseFloat(ct.total) || 0) * 100) / 100;
|
|
13966
|
+
const percentage = extraCostTotal > 0
|
|
13967
|
+
? Math.round((typeTotal / extraCostTotal) * 10000) / 100
|
|
13968
|
+
: 0;
|
|
13969
|
+
return {
|
|
13970
|
+
cost_type_id: ct.costTypeId,
|
|
13971
|
+
cost_type_slug: ct.costTypeSlug,
|
|
13972
|
+
cost_type_name: ct.costTypeName,
|
|
13973
|
+
cost_type_code: ct.costTypeCode,
|
|
13974
|
+
total: typeTotal,
|
|
13975
|
+
percentage,
|
|
13976
|
+
};
|
|
13977
|
+
});
|
|
13978
|
+
|
|
13979
|
+
return {
|
|
13980
|
+
project_id: projectId,
|
|
13981
|
+
budget_amount: budgetAmount,
|
|
13982
|
+
team_cost_total: teamCostTotal,
|
|
13983
|
+
extra_cost_total: extraCostTotal,
|
|
13984
|
+
total_project_cost: totalProjectCost,
|
|
13985
|
+
remaining_budget: remainingBudget,
|
|
13986
|
+
budget_usage_percent: budgetUsagePercent,
|
|
13987
|
+
planned_total: plannedTotal,
|
|
13988
|
+
approved_total: approvedTotal,
|
|
13989
|
+
realized_total: realizedTotal,
|
|
13990
|
+
cancelled_total: cancelledTotal,
|
|
13991
|
+
billable_total: billableTotal,
|
|
13992
|
+
non_billable_total: nonBillableTotal,
|
|
13993
|
+
reimbursable_total: reimbursableTotal,
|
|
13994
|
+
cost_by_category: costByCategory.map((c) => ({
|
|
13995
|
+
category_id: c.categoryId,
|
|
13996
|
+
category_slug: c.categorySlug,
|
|
13997
|
+
category_name: c.categoryName,
|
|
13998
|
+
category_color: c.categoryColor,
|
|
13999
|
+
category_icon: c.categoryIcon,
|
|
14000
|
+
total: Math.round((parseFloat(c.total) || 0) * 100) / 100,
|
|
14001
|
+
count: Number(c.count),
|
|
14002
|
+
})),
|
|
14003
|
+
cost_by_type: costByType.map((t) => ({
|
|
14004
|
+
cost_type_id: t.costTypeId,
|
|
14005
|
+
cost_type_slug: t.costTypeSlug,
|
|
14006
|
+
cost_type_name: t.costTypeName,
|
|
14007
|
+
cost_type_code: t.costTypeCode,
|
|
14008
|
+
total: Math.round((parseFloat(t.total) || 0) * 100) / 100,
|
|
14009
|
+
count: Number(t.count),
|
|
14010
|
+
})),
|
|
14011
|
+
cost_by_month: costByMonth.map((m) => ({
|
|
14012
|
+
month: m.month,
|
|
14013
|
+
total: Math.round((parseFloat(m.total) || 0) * 100) / 100,
|
|
14014
|
+
count: Number(m.count),
|
|
14015
|
+
})),
|
|
14016
|
+
top_cost_types: topCostTypes,
|
|
14017
|
+
};
|
|
14018
|
+
}
|
|
14019
|
+
|
|
14020
|
+
async getProjectCostReport(
|
|
14021
|
+
userId: number,
|
|
14022
|
+
projectId: number,
|
|
14023
|
+
filters: {
|
|
14024
|
+
date_from?: string;
|
|
14025
|
+
date_to?: string;
|
|
14026
|
+
category_id?: number;
|
|
14027
|
+
cost_type_id?: number;
|
|
14028
|
+
status?: string;
|
|
14029
|
+
is_billable?: boolean;
|
|
14030
|
+
is_reimbursable?: boolean;
|
|
14031
|
+
},
|
|
14032
|
+
) {
|
|
14033
|
+
await this.getActorContext(userId);
|
|
14034
|
+
const localeId = await this.resolvePreferredLocaleId();
|
|
14035
|
+
|
|
14036
|
+
// ── Verify project ───────────────────────────────────────────────────
|
|
14037
|
+
const project = await this.querySingle<{ id: number; budgetAmount: string | null }>(
|
|
14038
|
+
`SELECT id, budget_amount::text AS "budgetAmount"
|
|
14039
|
+
FROM operations_project
|
|
14040
|
+
WHERE id = $1 AND deleted_at IS NULL
|
|
14041
|
+
LIMIT 1`,
|
|
14042
|
+
[projectId],
|
|
14043
|
+
);
|
|
14044
|
+
if (!project) {
|
|
14045
|
+
throw new NotFoundException('Project not found.');
|
|
14046
|
+
}
|
|
14047
|
+
const budgetAmount = parseFloat(project.budgetAmount ?? '0') || 0;
|
|
14048
|
+
|
|
14049
|
+
// ── Build dynamic WHERE clause ────────────────────────────────────────
|
|
14050
|
+
const conditions: string[] = [
|
|
14051
|
+
'pc.deleted_at IS NULL',
|
|
14052
|
+
'pc.project_id = $1',
|
|
14053
|
+
];
|
|
14054
|
+
const params: unknown[] = [projectId];
|
|
14055
|
+
|
|
14056
|
+
if (filters.date_from) {
|
|
14057
|
+
params.push(filters.date_from);
|
|
14058
|
+
conditions.push(`COALESCE(pc.cost_date, pc.created_at::date) >= $${params.length}::date`);
|
|
14059
|
+
}
|
|
14060
|
+
if (filters.date_to) {
|
|
14061
|
+
params.push(filters.date_to);
|
|
14062
|
+
conditions.push(`COALESCE(pc.cost_date, pc.created_at::date) <= $${params.length}::date`);
|
|
14063
|
+
}
|
|
14064
|
+
if (filters.category_id !== undefined) {
|
|
14065
|
+
params.push(filters.category_id);
|
|
14066
|
+
conditions.push(
|
|
14067
|
+
`(pc.category_id = $${params.length} OR (pc.category_id IS NULL AND EXISTS (
|
|
14068
|
+
SELECT 1 FROM operations_project_cost_type pct2
|
|
14069
|
+
WHERE pct2.id = pc.cost_type_id AND pct2.category_id = $${params.length} AND pct2.deleted_at IS NULL
|
|
14070
|
+
)))`,
|
|
14071
|
+
);
|
|
14072
|
+
}
|
|
14073
|
+
if (filters.cost_type_id !== undefined) {
|
|
14074
|
+
params.push(filters.cost_type_id);
|
|
14075
|
+
conditions.push(`pc.cost_type_id = $${params.length}`);
|
|
14076
|
+
}
|
|
14077
|
+
if (filters.status !== undefined) {
|
|
14078
|
+
params.push(filters.status);
|
|
14079
|
+
conditions.push(`pc.status = $${params.length}`);
|
|
14080
|
+
}
|
|
14081
|
+
if (filters.is_billable !== undefined) {
|
|
14082
|
+
params.push(filters.is_billable);
|
|
14083
|
+
conditions.push(`pc.is_billable = $${params.length}`);
|
|
14084
|
+
}
|
|
14085
|
+
if (filters.is_reimbursable !== undefined) {
|
|
14086
|
+
params.push(filters.is_reimbursable);
|
|
14087
|
+
conditions.push(`pc.is_reimbursable = $${params.length}`);
|
|
14088
|
+
}
|
|
14089
|
+
|
|
14090
|
+
const whereClause = conditions.join(' AND ');
|
|
14091
|
+
|
|
14092
|
+
// ── Totals ────────────────────────────────────────────────────────────
|
|
14093
|
+
const totals = await this.querySingle<{
|
|
14094
|
+
grandTotal: string;
|
|
14095
|
+
plannedTotal: string;
|
|
14096
|
+
approvedTotal: string;
|
|
14097
|
+
realizedTotal: string;
|
|
14098
|
+
cancelledTotal: string;
|
|
14099
|
+
billableTotal: string;
|
|
14100
|
+
nonBillableTotal: string;
|
|
14101
|
+
reimbursableTotal: string;
|
|
14102
|
+
totalCount: number;
|
|
14103
|
+
}>(
|
|
14104
|
+
`SELECT
|
|
14105
|
+
COALESCE(SUM(pc.amount * pc.quantity), 0)::text AS "grandTotal",
|
|
14106
|
+
COALESCE(SUM(CASE WHEN pc.status = 'planned' THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "plannedTotal",
|
|
14107
|
+
COALESCE(SUM(CASE WHEN pc.status = 'approved' THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "approvedTotal",
|
|
14108
|
+
COALESCE(SUM(CASE WHEN pc.status = 'realized' THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "realizedTotal",
|
|
14109
|
+
COALESCE(SUM(CASE WHEN pc.status = 'cancelled' THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "cancelledTotal",
|
|
14110
|
+
COALESCE(SUM(CASE WHEN pc.is_billable = true THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "billableTotal",
|
|
14111
|
+
COALESCE(SUM(CASE WHEN pc.is_billable = false THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "nonBillableTotal",
|
|
14112
|
+
COALESCE(SUM(CASE WHEN pc.is_reimbursable = true THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "reimbursableTotal",
|
|
14113
|
+
COUNT(*)::int AS "totalCount"
|
|
14114
|
+
FROM operations_project_cost pc
|
|
14115
|
+
WHERE ${whereClause}`,
|
|
14116
|
+
params,
|
|
14117
|
+
);
|
|
14118
|
+
|
|
14119
|
+
const round2 = (v: string | null | undefined) =>
|
|
14120
|
+
Math.round((parseFloat(v ?? '0') || 0) * 100) / 100;
|
|
14121
|
+
|
|
14122
|
+
const grandTotal = round2(totals?.grandTotal);
|
|
14123
|
+
const plannedTotal = round2(totals?.plannedTotal);
|
|
14124
|
+
const approvedTotal = round2(totals?.approvedTotal);
|
|
14125
|
+
const realizedTotal = round2(totals?.realizedTotal);
|
|
14126
|
+
const cancelledTotal = round2(totals?.cancelledTotal);
|
|
14127
|
+
const billableTotal = round2(totals?.billableTotal);
|
|
14128
|
+
const nonBillableTotal = round2(totals?.nonBillableTotal);
|
|
14129
|
+
const reimbursableTotal= round2(totals?.reimbursableTotal);
|
|
14130
|
+
|
|
14131
|
+
// ── By category ───────────────────────────────────────────────────────
|
|
14132
|
+
const costByCategory = await this.queryRows<{
|
|
14133
|
+
categoryId: number | null;
|
|
14134
|
+
categorySlug: string | null;
|
|
14135
|
+
categoryName: string | null;
|
|
14136
|
+
categoryColor: string | null;
|
|
14137
|
+
categoryIcon: string | null;
|
|
14138
|
+
total: string;
|
|
14139
|
+
count: number;
|
|
14140
|
+
plannedSubtotal: string;
|
|
14141
|
+
realizedSubtotal: string;
|
|
14142
|
+
}>(
|
|
14143
|
+
`SELECT
|
|
14144
|
+
COALESCE(pc.category_id, pct.category_id) AS "categoryId",
|
|
14145
|
+
pcc.slug AS "categorySlug",
|
|
14146
|
+
COALESCE(pccl.name, pcc.slug) AS "categoryName",
|
|
14147
|
+
pcc.color AS "categoryColor",
|
|
14148
|
+
pcc.icon AS "categoryIcon",
|
|
14149
|
+
SUM(pc.amount * pc.quantity)::text AS total,
|
|
14150
|
+
COUNT(*)::int AS count,
|
|
14151
|
+
COALESCE(SUM(CASE WHEN pc.status='planned' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "plannedSubtotal",
|
|
14152
|
+
COALESCE(SUM(CASE WHEN pc.status='realized' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "realizedSubtotal"
|
|
14153
|
+
FROM operations_project_cost pc
|
|
14154
|
+
LEFT JOIN operations_project_cost_type pct
|
|
14155
|
+
ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
|
|
14156
|
+
LEFT JOIN operations_project_cost_category pcc
|
|
14157
|
+
ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
|
|
14158
|
+
LEFT JOIN LATERAL (
|
|
14159
|
+
SELECT l.name
|
|
14160
|
+
FROM operations_project_cost_category_locale l
|
|
14161
|
+
WHERE l.operations_project_cost_category_id = pcc.id
|
|
14162
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
|
|
14163
|
+
LIMIT 1
|
|
14164
|
+
) pccl ON TRUE
|
|
14165
|
+
WHERE ${whereClause.replace(/\$(\d+)/g, (m, n) => '$' + (Number(n) + 1))}
|
|
14166
|
+
GROUP BY COALESCE(pc.category_id, pct.category_id), pcc.slug, pcc.color, pcc.icon, pccl.name
|
|
14167
|
+
ORDER BY SUM(pc.amount * pc.quantity) DESC`,
|
|
14168
|
+
[localeId, ...params],
|
|
14169
|
+
);
|
|
14170
|
+
|
|
14171
|
+
// ── By type ───────────────────────────────────────────────────────────
|
|
14172
|
+
const costByType = await this.queryRows<{
|
|
14173
|
+
costTypeId: number | null;
|
|
14174
|
+
costTypeSlug: string | null;
|
|
14175
|
+
costTypeName: string | null;
|
|
14176
|
+
costTypeCode: string | null;
|
|
14177
|
+
total: string;
|
|
14178
|
+
count: number;
|
|
14179
|
+
plannedSubtotal: string;
|
|
14180
|
+
realizedSubtotal: string;
|
|
14181
|
+
}>(
|
|
14182
|
+
`SELECT
|
|
14183
|
+
pc.cost_type_id AS "costTypeId",
|
|
14184
|
+
pct.slug AS "costTypeSlug",
|
|
14185
|
+
COALESCE(pctl.name, pct.slug) AS "costTypeName",
|
|
14186
|
+
pct.code AS "costTypeCode",
|
|
14187
|
+
SUM(pc.amount * pc.quantity)::text AS total,
|
|
14188
|
+
COUNT(*)::int AS count,
|
|
14189
|
+
COALESCE(SUM(CASE WHEN pc.status='planned' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "plannedSubtotal",
|
|
14190
|
+
COALESCE(SUM(CASE WHEN pc.status='realized' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "realizedSubtotal"
|
|
14191
|
+
FROM operations_project_cost pc
|
|
14192
|
+
LEFT JOIN operations_project_cost_type pct
|
|
14193
|
+
ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
|
|
14194
|
+
LEFT JOIN LATERAL (
|
|
14195
|
+
SELECT l.name
|
|
14196
|
+
FROM operations_project_cost_type_locale l
|
|
14197
|
+
WHERE l.operations_project_cost_type_id = pct.id
|
|
14198
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
|
|
14199
|
+
LIMIT 1
|
|
14200
|
+
) pctl ON TRUE
|
|
14201
|
+
WHERE ${whereClause.replace(/\$(\d+)/g, (m, n) => '$' + (Number(n) + 1))}
|
|
14202
|
+
GROUP BY pc.cost_type_id, pct.slug, pct.code, pctl.name
|
|
14203
|
+
ORDER BY SUM(pc.amount * pc.quantity) DESC`,
|
|
14204
|
+
[localeId, ...params],
|
|
14205
|
+
);
|
|
14206
|
+
|
|
14207
|
+
// ── By month ──────────────────────────────────────────────────────────
|
|
14208
|
+
const costByMonth = await this.queryRows<{
|
|
14209
|
+
month: string;
|
|
14210
|
+
total: string;
|
|
14211
|
+
plannedSubtotal: string;
|
|
14212
|
+
realizedSubtotal: string;
|
|
14213
|
+
count: number;
|
|
14214
|
+
}>(
|
|
14215
|
+
`SELECT
|
|
14216
|
+
TO_CHAR(COALESCE(pc.cost_date, pc.created_at::date), 'YYYY-MM') AS month,
|
|
14217
|
+
SUM(pc.amount * pc.quantity)::text AS total,
|
|
14218
|
+
COALESCE(SUM(CASE WHEN pc.status='planned' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "plannedSubtotal",
|
|
14219
|
+
COALESCE(SUM(CASE WHEN pc.status='realized' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "realizedSubtotal",
|
|
14220
|
+
COUNT(*)::int AS count
|
|
14221
|
+
FROM operations_project_cost pc
|
|
14222
|
+
WHERE ${whereClause}
|
|
14223
|
+
GROUP BY TO_CHAR(COALESCE(pc.cost_date, pc.created_at::date), 'YYYY-MM')
|
|
14224
|
+
ORDER BY month ASC`,
|
|
14225
|
+
params,
|
|
14226
|
+
);
|
|
14227
|
+
|
|
14228
|
+
// ── Top 5 individual costs ────────────────────────────────────────────
|
|
14229
|
+
const top5Costs = await this.queryRows<{
|
|
14230
|
+
id: number;
|
|
14231
|
+
description: string | null;
|
|
14232
|
+
amount: string;
|
|
14233
|
+
quantity: string;
|
|
14234
|
+
status: string;
|
|
14235
|
+
costTypeName: string | null;
|
|
14236
|
+
categoryName: string | null;
|
|
14237
|
+
categoryColor: string | null;
|
|
14238
|
+
costDate: string | null;
|
|
14239
|
+
}>(
|
|
14240
|
+
`SELECT
|
|
14241
|
+
pc.id,
|
|
14242
|
+
pc.description,
|
|
14243
|
+
pc.amount::text,
|
|
14244
|
+
pc.quantity::text,
|
|
14245
|
+
pc.status,
|
|
14246
|
+
pc.cost_date AS "costDate",
|
|
14247
|
+
COALESCE(pctl.name, pct.slug) AS "costTypeName",
|
|
14248
|
+
COALESCE(pccl.name, pcc.slug) AS "categoryName",
|
|
14249
|
+
pcc.color AS "categoryColor"
|
|
14250
|
+
FROM operations_project_cost pc
|
|
14251
|
+
LEFT JOIN operations_project_cost_type pct
|
|
14252
|
+
ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
|
|
14253
|
+
LEFT JOIN LATERAL (
|
|
14254
|
+
SELECT l.name
|
|
14255
|
+
FROM operations_project_cost_type_locale l
|
|
14256
|
+
WHERE l.operations_project_cost_type_id = pct.id
|
|
14257
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
|
|
14258
|
+
LIMIT 1
|
|
14259
|
+
) pctl ON TRUE
|
|
14260
|
+
LEFT JOIN operations_project_cost_category pcc
|
|
14261
|
+
ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
|
|
14262
|
+
LEFT JOIN LATERAL (
|
|
14263
|
+
SELECT l.name
|
|
14264
|
+
FROM operations_project_cost_category_locale l
|
|
14265
|
+
WHERE l.operations_project_cost_category_id = pcc.id
|
|
14266
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
|
|
14267
|
+
LIMIT 1
|
|
14268
|
+
) pccl ON TRUE
|
|
14269
|
+
WHERE ${whereClause.replace(/\$(\d+)/g, (m, n) => '$' + (Number(n) + 1))}
|
|
14270
|
+
ORDER BY (pc.amount * pc.quantity) DESC
|
|
14271
|
+
LIMIT 5`,
|
|
14272
|
+
[localeId, ...params],
|
|
14273
|
+
);
|
|
14274
|
+
|
|
14275
|
+
// ── Detailed list ─────────────────────────────────────────────────────
|
|
14276
|
+
const detailedList = await this.queryRows<{
|
|
14277
|
+
id: number;
|
|
14278
|
+
description: string | null;
|
|
14279
|
+
amount: string;
|
|
14280
|
+
quantity: string;
|
|
14281
|
+
unitAmount: string | null;
|
|
14282
|
+
currency: string | null;
|
|
14283
|
+
calculationType: string | null;
|
|
14284
|
+
recurrenceType: string | null;
|
|
14285
|
+
status: string;
|
|
14286
|
+
isBillable: boolean;
|
|
14287
|
+
isReimbursable: boolean;
|
|
14288
|
+
costDate: string | null;
|
|
14289
|
+
periodStart: string | null;
|
|
14290
|
+
periodEnd: string | null;
|
|
14291
|
+
notes: string | null;
|
|
14292
|
+
costTypeId: number | null;
|
|
14293
|
+
costTypeName: string | null;
|
|
14294
|
+
costTypeCode: string | null;
|
|
14295
|
+
categoryId: number | null;
|
|
14296
|
+
categoryName: string | null;
|
|
14297
|
+
categoryColor: string | null;
|
|
14298
|
+
createdAt: string;
|
|
14299
|
+
}>(
|
|
14300
|
+
`SELECT
|
|
14301
|
+
pc.id,
|
|
14302
|
+
pc.description,
|
|
14303
|
+
pc.amount::text,
|
|
14304
|
+
pc.quantity::text,
|
|
14305
|
+
pc.unit_amount::text AS "unitAmount",
|
|
14306
|
+
pc.currency,
|
|
14307
|
+
pc.calculation_type AS "calculationType",
|
|
14308
|
+
pc.recurrence_type AS "recurrenceType",
|
|
14309
|
+
pc.status,
|
|
14310
|
+
pc.is_billable AS "isBillable",
|
|
14311
|
+
pc.is_reimbursable AS "isReimbursable",
|
|
14312
|
+
pc.cost_date AS "costDate",
|
|
14313
|
+
pc.period_start AS "periodStart",
|
|
14314
|
+
pc.period_end AS "periodEnd",
|
|
14315
|
+
pc.notes,
|
|
14316
|
+
pc.cost_type_id AS "costTypeId",
|
|
14317
|
+
COALESCE(pctl.name, pct.slug) AS "costTypeName",
|
|
14318
|
+
pct.code AS "costTypeCode",
|
|
14319
|
+
COALESCE(pc.category_id, pct.category_id) AS "categoryId",
|
|
14320
|
+
COALESCE(pccl.name, pcc.slug) AS "categoryName",
|
|
14321
|
+
pcc.color AS "categoryColor",
|
|
14322
|
+
pc.created_at::text AS "createdAt"
|
|
14323
|
+
FROM operations_project_cost pc
|
|
14324
|
+
LEFT JOIN operations_project_cost_type pct
|
|
14325
|
+
ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
|
|
14326
|
+
LEFT JOIN LATERAL (
|
|
14327
|
+
SELECT l.name
|
|
14328
|
+
FROM operations_project_cost_type_locale l
|
|
14329
|
+
WHERE l.operations_project_cost_type_id = pct.id
|
|
14330
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
|
|
14331
|
+
LIMIT 1
|
|
14332
|
+
) pctl ON TRUE
|
|
14333
|
+
LEFT JOIN operations_project_cost_category pcc
|
|
14334
|
+
ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
|
|
14335
|
+
LEFT JOIN LATERAL (
|
|
14336
|
+
SELECT l.name
|
|
14337
|
+
FROM operations_project_cost_category_locale l
|
|
14338
|
+
WHERE l.operations_project_cost_category_id = pcc.id
|
|
14339
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
|
|
14340
|
+
LIMIT 1
|
|
14341
|
+
) pccl ON TRUE
|
|
14342
|
+
WHERE ${whereClause.replace(/\$(\d+)/g, (m, n) => '$' + (Number(n) + 1))}
|
|
14343
|
+
ORDER BY (pc.amount * pc.quantity) DESC, pc.cost_date DESC NULLS LAST`,
|
|
14344
|
+
[localeId, ...params],
|
|
14345
|
+
);
|
|
14346
|
+
|
|
14347
|
+
return {
|
|
14348
|
+
project_id: projectId,
|
|
14349
|
+
budget_amount: budgetAmount,
|
|
14350
|
+
filters_applied: {
|
|
14351
|
+
date_from: filters.date_from ?? null,
|
|
14352
|
+
date_to: filters.date_to ?? null,
|
|
14353
|
+
category_id: filters.category_id ?? null,
|
|
14354
|
+
cost_type_id: filters.cost_type_id ?? null,
|
|
14355
|
+
status: filters.status ?? null,
|
|
14356
|
+
is_billable: filters.is_billable ?? null,
|
|
14357
|
+
is_reimbursable:filters.is_reimbursable ?? null,
|
|
14358
|
+
},
|
|
14359
|
+
totals: {
|
|
14360
|
+
grand_total: grandTotal,
|
|
14361
|
+
planned_total: plannedTotal,
|
|
14362
|
+
approved_total: approvedTotal,
|
|
14363
|
+
realized_total: realizedTotal,
|
|
14364
|
+
cancelled_total: cancelledTotal,
|
|
14365
|
+
billable_total: billableTotal,
|
|
14366
|
+
non_billable_total: nonBillableTotal,
|
|
14367
|
+
reimbursable_total: reimbursableTotal,
|
|
14368
|
+
total_count: Number(totals?.totalCount ?? 0),
|
|
14369
|
+
},
|
|
14370
|
+
cost_by_category: costByCategory.map((c) => ({
|
|
14371
|
+
category_id: c.categoryId,
|
|
14372
|
+
category_slug: c.categorySlug,
|
|
14373
|
+
category_name: c.categoryName,
|
|
14374
|
+
category_color: c.categoryColor,
|
|
14375
|
+
category_icon: c.categoryIcon,
|
|
14376
|
+
total: round2(c.total),
|
|
14377
|
+
count: Number(c.count),
|
|
14378
|
+
planned_subtotal: round2(c.plannedSubtotal),
|
|
14379
|
+
realized_subtotal: round2(c.realizedSubtotal),
|
|
14380
|
+
})),
|
|
14381
|
+
cost_by_type: costByType.map((t) => ({
|
|
14382
|
+
cost_type_id: t.costTypeId,
|
|
14383
|
+
cost_type_slug: t.costTypeSlug,
|
|
14384
|
+
cost_type_name: t.costTypeName,
|
|
14385
|
+
cost_type_code: t.costTypeCode,
|
|
14386
|
+
total: round2(t.total),
|
|
14387
|
+
count: Number(t.count),
|
|
14388
|
+
planned_subtotal: round2(t.plannedSubtotal),
|
|
14389
|
+
realized_subtotal: round2(t.realizedSubtotal),
|
|
14390
|
+
})),
|
|
14391
|
+
cost_by_month: costByMonth.map((m) => ({
|
|
14392
|
+
month: m.month,
|
|
14393
|
+
total: round2(m.total),
|
|
14394
|
+
planned_subtotal: round2(m.plannedSubtotal),
|
|
14395
|
+
realized_subtotal: round2(m.realizedSubtotal),
|
|
14396
|
+
count: Number(m.count),
|
|
14397
|
+
})),
|
|
14398
|
+
top_5_costs: top5Costs.map((c) => ({
|
|
14399
|
+
id: c.id,
|
|
14400
|
+
description: c.description,
|
|
14401
|
+
total: round2(String(parseFloat(c.amount) * parseFloat(c.quantity))),
|
|
14402
|
+
amount: round2(c.amount),
|
|
14403
|
+
quantity: parseFloat(c.quantity),
|
|
14404
|
+
status: c.status,
|
|
14405
|
+
cost_type_name: c.costTypeName,
|
|
14406
|
+
category_name: c.categoryName,
|
|
14407
|
+
category_color: c.categoryColor,
|
|
14408
|
+
cost_date: c.costDate,
|
|
14409
|
+
})),
|
|
14410
|
+
detailed_list: detailedList.map((c) => ({
|
|
14411
|
+
id: c.id,
|
|
14412
|
+
description: c.description,
|
|
14413
|
+
amount: round2(c.amount),
|
|
14414
|
+
quantity: parseFloat(c.quantity),
|
|
14415
|
+
unit_amount: c.unitAmount ? round2(c.unitAmount) : null,
|
|
14416
|
+
total: round2(String(parseFloat(c.amount) * parseFloat(c.quantity))),
|
|
14417
|
+
currency: c.currency,
|
|
14418
|
+
calculation_type: c.calculationType,
|
|
14419
|
+
recurrence_type: c.recurrenceType,
|
|
14420
|
+
status: c.status,
|
|
14421
|
+
is_billable: c.isBillable,
|
|
14422
|
+
is_reimbursable: c.isReimbursable,
|
|
14423
|
+
cost_date: c.costDate,
|
|
14424
|
+
period_start: c.periodStart,
|
|
14425
|
+
period_end: c.periodEnd,
|
|
14426
|
+
notes: c.notes,
|
|
14427
|
+
cost_type_id: c.costTypeId,
|
|
14428
|
+
cost_type_name: c.costTypeName,
|
|
14429
|
+
cost_type_code: c.costTypeCode,
|
|
14430
|
+
category_id: c.categoryId,
|
|
14431
|
+
category_name: c.categoryName,
|
|
14432
|
+
category_color: c.categoryColor,
|
|
14433
|
+
created_at: c.createdAt,
|
|
14434
|
+
})),
|
|
14435
|
+
};
|
|
14436
|
+
}
|
|
14437
|
+
|
|
14438
|
+
async createProjectCost(userId: number, projectId: number, data: { cost_type_id?: number; category_id?: number; description?: string; amount: number; quantity?: number; unit_amount?: number; currency?: string; cost_date?: string; period_start?: string; period_end?: string; calculation_type?: string; recurrence_type?: string; is_billable?: boolean; is_reimbursable?: boolean; notes?: string; status?: string }) {
|
|
14439
|
+
const actor = await this.getActorContext(userId);
|
|
14440
|
+
this.ensureSupervisor(actor);
|
|
14441
|
+
|
|
14442
|
+
const project = await this.querySingle<{ id: number }>(
|
|
14443
|
+
`SELECT id FROM operations_project WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
14444
|
+
[projectId]
|
|
14445
|
+
);
|
|
14446
|
+
if (!project) {
|
|
14447
|
+
throw new NotFoundException('Project not found.');
|
|
14448
|
+
}
|
|
14449
|
+
|
|
14450
|
+
if (data.cost_type_id) {
|
|
14451
|
+
const costType = await this.querySingle<{ id: number }>(
|
|
14452
|
+
`SELECT id FROM operations_project_cost_type WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
14453
|
+
[data.cost_type_id]
|
|
14454
|
+
);
|
|
14455
|
+
if (!costType) {
|
|
14456
|
+
throw new BadRequestException(`Cost type with id ${data.cost_type_id} not found.`);
|
|
14457
|
+
}
|
|
14458
|
+
}
|
|
14459
|
+
|
|
14460
|
+
if (data.category_id) {
|
|
14461
|
+
const category = await this.querySingle<{ id: number }>(
|
|
14462
|
+
`SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
14463
|
+
[data.category_id]
|
|
14464
|
+
);
|
|
14465
|
+
if (!category) {
|
|
14466
|
+
throw new BadRequestException(`Cost category with id ${data.category_id} not found.`);
|
|
14467
|
+
}
|
|
14468
|
+
}
|
|
14469
|
+
|
|
14470
|
+
const calcType = data.calculation_type ?? 'fixed';
|
|
14471
|
+
let effectiveAmount = data.amount;
|
|
14472
|
+
if (['unit', 'hourly', 'monthly'].includes(calcType) && data.unit_amount !== undefined && data.unit_amount !== null) {
|
|
14473
|
+
const qty = data.quantity ?? 1;
|
|
14474
|
+
effectiveAmount = Math.round(qty * data.unit_amount * 100) / 100;
|
|
14475
|
+
}
|
|
14476
|
+
|
|
14477
|
+
const created = await this.querySingle<{ id: number }>(
|
|
14478
|
+
`INSERT INTO operations_project_cost
|
|
14479
|
+
(project_id, cost_type_id, category_id, description, amount, quantity, unit_amount, currency, cost_date, period_start, period_end, calculation_type, recurrence_type, is_billable, is_reimbursable, notes, status, created_at, updated_at)
|
|
14480
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::date, $10::date, $11::date, $12::operations_project_cost_calculation_type_134cdfb49c_enum, $13::operations_project_cost_recurrence_type_09baf0f043_enum, $14, $15, $16, $17::operations_project_cost_status_153e8592ce_enum, NOW(), NOW())
|
|
14481
|
+
RETURNING id`,
|
|
14482
|
+
[
|
|
14483
|
+
projectId,
|
|
14484
|
+
data.cost_type_id ?? null,
|
|
14485
|
+
data.category_id ?? null,
|
|
14486
|
+
data.description ?? null,
|
|
14487
|
+
effectiveAmount,
|
|
14488
|
+
data.quantity ?? 1,
|
|
14489
|
+
data.unit_amount ?? null,
|
|
14490
|
+
data.currency ?? 'BRL',
|
|
14491
|
+
data.cost_date ?? null,
|
|
14492
|
+
data.period_start ?? null,
|
|
14493
|
+
data.period_end ?? null,
|
|
14494
|
+
calcType,
|
|
14495
|
+
data.recurrence_type ?? 'none',
|
|
14496
|
+
data.is_billable ?? false,
|
|
14497
|
+
data.is_reimbursable ?? false,
|
|
14498
|
+
data.notes ?? null,
|
|
14499
|
+
data.status ?? 'planned',
|
|
14500
|
+
]
|
|
14501
|
+
);
|
|
14502
|
+
|
|
14503
|
+
if (!created?.id) {
|
|
14504
|
+
throw new BadRequestException('Unable to create project cost.');
|
|
14505
|
+
}
|
|
14506
|
+
|
|
14507
|
+
const rows = await this.listProjectCosts(userId, projectId, {});
|
|
14508
|
+
return rows.find((r) => r.id === created.id) ?? null;
|
|
14509
|
+
}
|
|
14510
|
+
|
|
14511
|
+
async updateProjectCost(userId: number, id: number, data: Partial<{ cost_type_id: number; category_id: number; description: string; amount: number; quantity: number; unit_amount: number; currency: string; cost_date: string; period_start: string; period_end: string; calculation_type: string; recurrence_type: string; is_billable: boolean; is_reimbursable: boolean; notes: string; status: string }>) {
|
|
14512
|
+
const actor = await this.getActorContext(userId);
|
|
14513
|
+
this.ensureSupervisor(actor);
|
|
14514
|
+
|
|
14515
|
+
const cost = await this.querySingle<{ id: number; projectId: number; calculationType: string; unitAmount: string | null; quantity: string }>(
|
|
14516
|
+
`SELECT id, project_id AS "projectId", calculation_type AS "calculationType", unit_amount::text AS "unitAmount", quantity::text AS quantity FROM operations_project_cost WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
14517
|
+
[id]
|
|
14518
|
+
);
|
|
14519
|
+
if (!cost) {
|
|
14520
|
+
throw new NotFoundException('Project cost not found.');
|
|
14521
|
+
}
|
|
14522
|
+
|
|
14523
|
+
// Auto-calculate amount when applicable
|
|
14524
|
+
const effectiveCalcType = data.calculation_type ?? cost.calculationType;
|
|
14525
|
+
if (['unit', 'hourly', 'monthly'].includes(effectiveCalcType)) {
|
|
14526
|
+
const ua = data.unit_amount !== undefined ? data.unit_amount : (cost.unitAmount !== null ? parseFloat(cost.unitAmount) : null);
|
|
14527
|
+
const qty = data.quantity !== undefined ? data.quantity : parseFloat(cost.quantity);
|
|
14528
|
+
if (ua !== null && ua !== undefined) {
|
|
14529
|
+
data = { ...data, amount: Math.round(qty * ua * 100) / 100 };
|
|
14530
|
+
}
|
|
14531
|
+
}
|
|
14532
|
+
|
|
14533
|
+
const sets: string[] = [];
|
|
14534
|
+
const params: unknown[] = [];
|
|
14535
|
+
|
|
14536
|
+
if (data.cost_type_id !== undefined) sets.push(`cost_type_id = ${this.param(params, data.cost_type_id)}`);
|
|
14537
|
+
if (data.category_id !== undefined) sets.push(`category_id = ${this.param(params, data.category_id)}`);
|
|
14538
|
+
if (data.description !== undefined) sets.push(`description = ${this.param(params, data.description)}`);
|
|
14539
|
+
if (data.amount !== undefined) sets.push(`amount = ${this.param(params, data.amount)}`);
|
|
14540
|
+
if (data.currency !== undefined) sets.push(`currency = ${this.param(params, data.currency)}`);
|
|
14541
|
+
if (data.quantity !== undefined) sets.push(`quantity = ${this.param(params, data.quantity)}`);
|
|
14542
|
+
if (data.unit_amount !== undefined) sets.push(`unit_amount = ${this.param(params, data.unit_amount)}`);
|
|
14543
|
+
if (data.calculation_type !== undefined) sets.push(`calculation_type = ${this.param(params, data.calculation_type)}::operations_project_cost_calculation_type_134cdfb49c_enum`);
|
|
14544
|
+
if (data.recurrence_type !== undefined) sets.push(`recurrence_type = ${this.param(params, data.recurrence_type)}::operations_project_cost_recurrence_type_09baf0f043_enum`);
|
|
14545
|
+
if (data.is_billable !== undefined) sets.push(`is_billable = ${this.param(params, data.is_billable)}`);
|
|
14546
|
+
if (data.is_reimbursable !== undefined) sets.push(`is_reimbursable = ${this.param(params, data.is_reimbursable)}`);
|
|
14547
|
+
if (data.cost_date !== undefined) sets.push(`cost_date = ${this.param(params, data.cost_date)}::date`);
|
|
14548
|
+
if (data.period_start !== undefined) sets.push(`period_start = ${this.param(params, data.period_start)}::date`);
|
|
14549
|
+
if (data.period_end !== undefined) sets.push(`period_end = ${this.param(params, data.period_end)}::date`);
|
|
14550
|
+
if (data.notes !== undefined) sets.push(`notes = ${this.param(params, data.notes)}`);
|
|
14551
|
+
if (data.status !== undefined) sets.push(`status = ${this.param(params, data.status)}::operations_project_cost_status_153e8592ce_enum`);
|
|
14552
|
+
|
|
14553
|
+
if (sets.length === 0) {
|
|
14554
|
+
const rows = await this.listProjectCosts(userId, cost.projectId, {});
|
|
14555
|
+
return rows.find((r) => r.id === id) ?? null;
|
|
14556
|
+
}
|
|
14557
|
+
|
|
14558
|
+
sets.push(`updated_at = NOW()`);
|
|
14559
|
+
await this.prisma.$queryRawUnsafe(
|
|
14560
|
+
`UPDATE operations_project_cost SET ${sets.join(', ')} WHERE id = ${this.param(params, id)}`,
|
|
14561
|
+
...params
|
|
14562
|
+
);
|
|
14563
|
+
|
|
14564
|
+
const rows = await this.listProjectCosts(userId, cost.projectId, {});
|
|
14565
|
+
return rows.find((r) => r.id === id) ?? null;
|
|
14566
|
+
}
|
|
14567
|
+
|
|
14568
|
+
async deleteProjectCost(userId: number, id: number) {
|
|
14569
|
+
const actor = await this.getActorContext(userId);
|
|
14570
|
+
this.ensureSupervisor(actor);
|
|
14571
|
+
|
|
14572
|
+
const cost = await this.querySingle<{ id: number }>(
|
|
14573
|
+
`SELECT id FROM operations_project_cost WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
14574
|
+
[id]
|
|
14575
|
+
);
|
|
14576
|
+
if (!cost) {
|
|
14577
|
+
throw new NotFoundException('Project cost not found.');
|
|
14578
|
+
}
|
|
14579
|
+
|
|
14580
|
+
await this.prisma.$queryRawUnsafe(
|
|
14581
|
+
`UPDATE operations_project_cost SET deleted_at = NOW() WHERE id = $1`,
|
|
14582
|
+
id
|
|
14583
|
+
);
|
|
14584
|
+
|
|
14585
|
+
return { success: true };
|
|
14586
|
+
}
|
|
12176
14587
|
}
|