@hed-hog/operations 0.0.322 → 0.0.325
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 +9 -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 +562 -0
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +1657 -47
- package/dist/operations.service.js.map +1 -1
- 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/frontend/app/_components/collaborator-costs-section.tsx.ejs +2 -18
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +185 -276
- 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 +167 -59
- 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 +2003 -1846
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +56 -11
- package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +297 -2
- package/hedhog/frontend/app/_components/task-form-sheet.tsx.ejs +530 -0
- package/hedhog/frontend/app/_lib/api.ts.ejs +247 -0
- package/hedhog/frontend/app/_lib/types.ts.ejs +196 -7
- 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 +219 -122
- 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 +192 -484
- package/hedhog/frontend/messages/en.json +279 -10
- package/hedhog/frontend/messages/en.json.ejs +2043 -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 +284 -13
- package/hedhog/frontend/messages/pt.json.ejs +2056 -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 +836 -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 +860 -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 +836 -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 +860 -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_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 +2274 -39
|
@@ -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,
|
|
@@ -544,6 +545,18 @@ type TaskPayload = {
|
|
|
544
545
|
archived?: boolean;
|
|
545
546
|
};
|
|
546
547
|
|
|
548
|
+
type TaskCommentRecord = {
|
|
549
|
+
id: number;
|
|
550
|
+
taskId: number;
|
|
551
|
+
content: string;
|
|
552
|
+
actorCollaboratorId: number | null;
|
|
553
|
+
actorName: string | null;
|
|
554
|
+
actorUserPhotoId: number | null;
|
|
555
|
+
actorPersonAvatarId: number | null;
|
|
556
|
+
createdAt: string;
|
|
557
|
+
updatedAt: string | null;
|
|
558
|
+
};
|
|
559
|
+
|
|
547
560
|
type QuickTimesheetEntryPayload = {
|
|
548
561
|
projectId?: number | null;
|
|
549
562
|
projectAssignmentId?: number | null;
|
|
@@ -2149,6 +2162,95 @@ export class OperationsService {
|
|
|
2149
2162
|
return this.getCollaboratorByIdForUser(userId, collaboratorId);
|
|
2150
2163
|
}
|
|
2151
2164
|
|
|
2165
|
+
async updateCollaboratorProjectAssignment(
|
|
2166
|
+
collaboratorId: number,
|
|
2167
|
+
projectId: number,
|
|
2168
|
+
data: {
|
|
2169
|
+
projectRoleId?: number | null;
|
|
2170
|
+
roleLabel?: string | null;
|
|
2171
|
+
allocationPercent?: number | null;
|
|
2172
|
+
weeklyHours?: number | null;
|
|
2173
|
+
startDate?: string | null;
|
|
2174
|
+
endDate?: string | null;
|
|
2175
|
+
status?: string;
|
|
2176
|
+
}
|
|
2177
|
+
) {
|
|
2178
|
+
const sets: string[] = [];
|
|
2179
|
+
const params: unknown[] = [collaboratorId, projectId];
|
|
2180
|
+
let idx = 3;
|
|
2181
|
+
|
|
2182
|
+
if ('projectRoleId' in data) {
|
|
2183
|
+
sets.push(`project_role_id = $${idx++}`);
|
|
2184
|
+
params.push(data.projectRoleId ?? null);
|
|
2185
|
+
}
|
|
2186
|
+
if ('roleLabel' in data) {
|
|
2187
|
+
sets.push(`role_label = $${idx++}`);
|
|
2188
|
+
params.push(data.roleLabel ?? null);
|
|
2189
|
+
}
|
|
2190
|
+
if ('allocationPercent' in data) {
|
|
2191
|
+
sets.push(`allocation_percent = $${idx++}`);
|
|
2192
|
+
params.push(data.allocationPercent ?? null);
|
|
2193
|
+
}
|
|
2194
|
+
if ('weeklyHours' in data) {
|
|
2195
|
+
sets.push(`weekly_hours = $${idx++}`);
|
|
2196
|
+
params.push(data.weeklyHours ?? null);
|
|
2197
|
+
}
|
|
2198
|
+
if ('startDate' in data) {
|
|
2199
|
+
sets.push(`start_date = $${idx++}::date`);
|
|
2200
|
+
params.push(data.startDate ?? null);
|
|
2201
|
+
}
|
|
2202
|
+
if ('endDate' in data) {
|
|
2203
|
+
sets.push(`end_date = $${idx++}::date`);
|
|
2204
|
+
params.push(data.endDate ?? null);
|
|
2205
|
+
}
|
|
2206
|
+
if ('status' in data) {
|
|
2207
|
+
sets.push(
|
|
2208
|
+
`status = $${idx++}::operations_project_assignment_status_155b459bbf_enum`
|
|
2209
|
+
);
|
|
2210
|
+
params.push(data.status);
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
if (!sets.length) return { updated: false };
|
|
2214
|
+
|
|
2215
|
+
sets.push(`updated_at = NOW()`);
|
|
2216
|
+
|
|
2217
|
+
await this.prisma.$executeRawUnsafe(
|
|
2218
|
+
`UPDATE operations_project_assignment
|
|
2219
|
+
SET ${sets.join(', ')}
|
|
2220
|
+
WHERE collaborator_id = $1
|
|
2221
|
+
AND project_id = $2
|
|
2222
|
+
AND deleted_at IS NULL`,
|
|
2223
|
+
...params
|
|
2224
|
+
);
|
|
2225
|
+
|
|
2226
|
+
return { updated: true };
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
async addCollaboratorProjectAssignment(
|
|
2230
|
+
collaboratorId: number,
|
|
2231
|
+
data: { projectId: number; roleLabel?: string }
|
|
2232
|
+
) {
|
|
2233
|
+
const existing = await this.querySingle<{ id: number }>(
|
|
2234
|
+
`SELECT id FROM operations_project_assignment
|
|
2235
|
+
WHERE collaborator_id = $1 AND project_id = $2 AND deleted_at IS NULL`,
|
|
2236
|
+
[collaboratorId, data.projectId]
|
|
2237
|
+
);
|
|
2238
|
+
|
|
2239
|
+
if (existing) {
|
|
2240
|
+
return { id: existing.id, created: false };
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
const row = await this.querySingle<{ id: number }>(
|
|
2244
|
+
`INSERT INTO operations_project_assignment
|
|
2245
|
+
(collaborator_id, project_id, role_label, status)
|
|
2246
|
+
VALUES ($1, $2, $3, 'active')
|
|
2247
|
+
RETURNING id`,
|
|
2248
|
+
[collaboratorId, data.projectId, data.roleLabel ?? '']
|
|
2249
|
+
);
|
|
2250
|
+
|
|
2251
|
+
return { id: row!.id, created: true };
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2152
2254
|
async getCollaboratorCompensationHistory(
|
|
2153
2255
|
userId: number,
|
|
2154
2256
|
collaboratorId: number
|
|
@@ -2755,6 +2857,7 @@ export class OperationsService {
|
|
|
2755
2857
|
status?: string;
|
|
2756
2858
|
myOnly?: boolean;
|
|
2757
2859
|
archived?: boolean;
|
|
2860
|
+
collaboratorId?: number;
|
|
2758
2861
|
}
|
|
2759
2862
|
) {
|
|
2760
2863
|
const actor = await this.getActorContext(userId);
|
|
@@ -2828,6 +2931,13 @@ export class OperationsService {
|
|
|
2828
2931
|
filters.push(`t.status::text = ${this.param(params, paginationParams.status)}`);
|
|
2829
2932
|
}
|
|
2830
2933
|
|
|
2934
|
+
if (paginationParams.collaboratorId) {
|
|
2935
|
+
const colId = paginationParams.collaboratorId;
|
|
2936
|
+
filters.push(
|
|
2937
|
+
`(pa.collaborator_id = ${this.param(params, colId)} OR t.assignee_collaborator_id = ${this.param(params, colId)})`
|
|
2938
|
+
);
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2831
2941
|
const whereClause = filters.join(' AND ');
|
|
2832
2942
|
const totalRow = await this.querySingle<{ total: string }>(
|
|
2833
2943
|
`SELECT COUNT(*)::text AS total
|
|
@@ -2868,6 +2978,8 @@ export class OperationsService {
|
|
|
2868
2978
|
assigneeName: string | null;
|
|
2869
2979
|
assigneeUserPhotoId: number | null;
|
|
2870
2980
|
assigneePersonAvatarId: number | null;
|
|
2981
|
+
commentCount: number;
|
|
2982
|
+
fileCount: number;
|
|
2871
2983
|
createdAt: string;
|
|
2872
2984
|
deletedAt: string | null;
|
|
2873
2985
|
}>(
|
|
@@ -2886,6 +2998,8 @@ export class OperationsService {
|
|
|
2886
2998
|
ac.display_name AS "assigneeName",
|
|
2887
2999
|
au.photo_id AS "assigneeUserPhotoId",
|
|
2888
3000
|
ap.avatar_id AS "assigneePersonAvatarId",
|
|
3001
|
+
COALESCE(task_comments.count, 0)::int AS "commentCount",
|
|
3002
|
+
COALESCE(task_files.count, 0)::int AS "fileCount",
|
|
2889
3003
|
t.created_at AS "createdAt",
|
|
2890
3004
|
t.deleted_at AS "deletedAt"
|
|
2891
3005
|
FROM operations_task t
|
|
@@ -2898,6 +3012,16 @@ export class OperationsService {
|
|
|
2898
3012
|
ON au.id = ac.user_id
|
|
2899
3013
|
LEFT JOIN person ap
|
|
2900
3014
|
ON ap.id = ac.person_id
|
|
3015
|
+
LEFT JOIN LATERAL (
|
|
3016
|
+
SELECT COUNT(*) AS count
|
|
3017
|
+
FROM operations_task_comment tc
|
|
3018
|
+
WHERE tc.task_id = t.id
|
|
3019
|
+
) task_comments ON TRUE
|
|
3020
|
+
LEFT JOIN LATERAL (
|
|
3021
|
+
SELECT COUNT(*) AS count
|
|
3022
|
+
FROM operations_task_file tf
|
|
3023
|
+
WHERE tf.operations_task_id = t.id
|
|
3024
|
+
) task_files ON TRUE
|
|
2901
3025
|
JOIN operations_project p
|
|
2902
3026
|
ON p.id = COALESCE(t.project_id, pa.project_id)
|
|
2903
3027
|
WHERE ${whereClause}
|
|
@@ -3262,6 +3386,183 @@ export class OperationsService {
|
|
|
3262
3386
|
return { success: true };
|
|
3263
3387
|
}
|
|
3264
3388
|
|
|
3389
|
+
async listTaskComments(userId: number, taskId: number) {
|
|
3390
|
+
const actor = await this.getActorContext(userId);
|
|
3391
|
+
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
3392
|
+
throw new ForbiddenException(
|
|
3393
|
+
'Operations collaborator access is required.'
|
|
3394
|
+
);
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
const current = await this.getTaskRecordForActor(
|
|
3398
|
+
this.prisma,
|
|
3399
|
+
actor,
|
|
3400
|
+
taskId
|
|
3401
|
+
);
|
|
3402
|
+
await this.assertProjectAccess(actor, current.projectId);
|
|
3403
|
+
|
|
3404
|
+
return this.queryRows<TaskCommentRecord>(
|
|
3405
|
+
`SELECT tc.id,
|
|
3406
|
+
tc.task_id AS "taskId",
|
|
3407
|
+
tc.content,
|
|
3408
|
+
tc.actor_collaborator_id AS "actorCollaboratorId",
|
|
3409
|
+
actor.display_name AS "actorName",
|
|
3410
|
+
actor_user.photo_id AS "actorUserPhotoId",
|
|
3411
|
+
actor_person.avatar_id AS "actorPersonAvatarId",
|
|
3412
|
+
tc.created_at AS "createdAt",
|
|
3413
|
+
tc.updated_at AS "updatedAt"
|
|
3414
|
+
FROM operations_task_comment tc
|
|
3415
|
+
LEFT JOIN operations_collaborator actor
|
|
3416
|
+
ON actor.id = tc.actor_collaborator_id
|
|
3417
|
+
AND actor.deleted_at IS NULL
|
|
3418
|
+
LEFT JOIN "user" actor_user
|
|
3419
|
+
ON actor_user.id = actor.user_id
|
|
3420
|
+
LEFT JOIN person actor_person
|
|
3421
|
+
ON actor_person.id = actor.person_id
|
|
3422
|
+
WHERE tc.task_id = $1
|
|
3423
|
+
ORDER BY tc.created_at ASC, tc.id ASC`,
|
|
3424
|
+
[taskId]
|
|
3425
|
+
);
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
async addTaskComment(userId: number, taskId: number, content: string) {
|
|
3429
|
+
const actor = await this.getActorContext(userId);
|
|
3430
|
+
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
3431
|
+
throw new ForbiddenException(
|
|
3432
|
+
'Operations collaborator access is required.'
|
|
3433
|
+
);
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
const current = await this.getTaskRecordForActor(
|
|
3437
|
+
this.prisma,
|
|
3438
|
+
actor,
|
|
3439
|
+
taskId
|
|
3440
|
+
);
|
|
3441
|
+
await this.assertProjectAccess(actor, current.projectId);
|
|
3442
|
+
|
|
3443
|
+
const normalizedContent = this.normalizeOptionalText(content);
|
|
3444
|
+
if (!normalizedContent) {
|
|
3445
|
+
throw new BadRequestException('Comment content is required.');
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
const inserted = await this.queryRows<{ id: number }>(
|
|
3449
|
+
`INSERT INTO operations_task_comment (
|
|
3450
|
+
task_id,
|
|
3451
|
+
actor_collaborator_id,
|
|
3452
|
+
content,
|
|
3453
|
+
created_at,
|
|
3454
|
+
updated_at
|
|
3455
|
+
) VALUES ($1, $2, $3, NOW(), NOW())
|
|
3456
|
+
RETURNING id`,
|
|
3457
|
+
[taskId, actor.collaboratorId ?? null, normalizedContent]
|
|
3458
|
+
);
|
|
3459
|
+
|
|
3460
|
+
const commentId = inserted[0]?.id;
|
|
3461
|
+
const comments = await this.listTaskComments(userId, taskId);
|
|
3462
|
+
const createdComment = comments.find((comment) => comment.id === commentId);
|
|
3463
|
+
|
|
3464
|
+
if (!createdComment) {
|
|
3465
|
+
throw new NotFoundException('Task comment could not be loaded.');
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
return createdComment;
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
async updateTaskComment(
|
|
3472
|
+
userId: number,
|
|
3473
|
+
taskId: number,
|
|
3474
|
+
commentId: number,
|
|
3475
|
+
content: string
|
|
3476
|
+
) {
|
|
3477
|
+
const actor = await this.getActorContext(userId);
|
|
3478
|
+
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
3479
|
+
throw new ForbiddenException(
|
|
3480
|
+
'Operations collaborator access is required.'
|
|
3481
|
+
);
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
const current = await this.getTaskRecordForActor(
|
|
3485
|
+
this.prisma,
|
|
3486
|
+
actor,
|
|
3487
|
+
taskId
|
|
3488
|
+
);
|
|
3489
|
+
await this.assertProjectAccess(actor, current.projectId);
|
|
3490
|
+
|
|
3491
|
+
const normalizedContent = this.normalizeOptionalText(content);
|
|
3492
|
+
if (!normalizedContent) {
|
|
3493
|
+
throw new BadRequestException('Comment content is required.');
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3496
|
+
const rows = await this.queryRows<{ id: number; actorCollaboratorId: number | null }>(
|
|
3497
|
+
`SELECT id, actor_collaborator_id AS "actorCollaboratorId"
|
|
3498
|
+
FROM operations_task_comment
|
|
3499
|
+
WHERE id = $1 AND task_id = $2`,
|
|
3500
|
+
[commentId, taskId]
|
|
3501
|
+
);
|
|
3502
|
+
|
|
3503
|
+
const row = rows[0];
|
|
3504
|
+
if (!row) {
|
|
3505
|
+
throw new NotFoundException('Comment not found.');
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
if (row.actorCollaboratorId !== actor.collaboratorId) {
|
|
3509
|
+
throw new ForbiddenException('You can only edit your own comments.');
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
await this.queryRows(
|
|
3513
|
+
`UPDATE operations_task_comment
|
|
3514
|
+
SET content = $1, updated_at = NOW()
|
|
3515
|
+
WHERE id = $2`,
|
|
3516
|
+
[normalizedContent, commentId]
|
|
3517
|
+
);
|
|
3518
|
+
|
|
3519
|
+
const comments = await this.listTaskComments(userId, taskId);
|
|
3520
|
+
return comments.find((c) => c.id === commentId) ?? null;
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
async removeTaskComment(
|
|
3524
|
+
userId: number,
|
|
3525
|
+
taskId: number,
|
|
3526
|
+
commentId: number
|
|
3527
|
+
) {
|
|
3528
|
+
const actor = await this.getActorContext(userId);
|
|
3529
|
+
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
3530
|
+
throw new ForbiddenException(
|
|
3531
|
+
'Operations collaborator access is required.'
|
|
3532
|
+
);
|
|
3533
|
+
}
|
|
3534
|
+
|
|
3535
|
+
const current = await this.getTaskRecordForActor(
|
|
3536
|
+
this.prisma,
|
|
3537
|
+
actor,
|
|
3538
|
+
taskId
|
|
3539
|
+
);
|
|
3540
|
+
await this.assertProjectAccess(actor, current.projectId);
|
|
3541
|
+
|
|
3542
|
+
const rows = await this.queryRows<{ id: number; actorCollaboratorId: number | null }>(
|
|
3543
|
+
`SELECT id, actor_collaborator_id AS "actorCollaboratorId"
|
|
3544
|
+
FROM operations_task_comment
|
|
3545
|
+
WHERE id = $1 AND task_id = $2`,
|
|
3546
|
+
[commentId, taskId]
|
|
3547
|
+
);
|
|
3548
|
+
|
|
3549
|
+
const row = rows[0];
|
|
3550
|
+
if (!row) {
|
|
3551
|
+
throw new NotFoundException('Comment not found.');
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
if (row.actorCollaboratorId !== actor.collaboratorId) {
|
|
3555
|
+
throw new ForbiddenException('You can only delete your own comments.');
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
await this.queryRows(
|
|
3559
|
+
`DELETE FROM operations_task_comment WHERE id = $1`,
|
|
3560
|
+
[commentId]
|
|
3561
|
+
);
|
|
3562
|
+
|
|
3563
|
+
return { success: true };
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3265
3566
|
async listTimesheetEntries(
|
|
3266
3567
|
userId: number,
|
|
3267
3568
|
paginationParams: {
|
|
@@ -5099,6 +5400,7 @@ export class OperationsService {
|
|
|
5099
5400
|
status?: string;
|
|
5100
5401
|
dateFrom?: string;
|
|
5101
5402
|
dateTo?: string;
|
|
5403
|
+
collaboratorId?: number;
|
|
5102
5404
|
} = {}
|
|
5103
5405
|
) {
|
|
5104
5406
|
const actor = await this.getActorContext(userId);
|
|
@@ -5125,6 +5427,10 @@ export class OperationsService {
|
|
|
5125
5427
|
where.push(`t.week_start_date <= ${this.param(params, filters.dateTo)}::date`);
|
|
5126
5428
|
}
|
|
5127
5429
|
|
|
5430
|
+
if (filters.collaboratorId) {
|
|
5431
|
+
where.push(`t.collaborator_id = ${this.param(params, filters.collaboratorId)}`);
|
|
5432
|
+
}
|
|
5433
|
+
|
|
5128
5434
|
if (pagination?.search) {
|
|
5129
5435
|
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
5130
5436
|
where.push(`(
|
|
@@ -5164,6 +5470,7 @@ export class OperationsService {
|
|
|
5164
5470
|
reviewedAt: string | null;
|
|
5165
5471
|
notes: string | null;
|
|
5166
5472
|
decisionNote: string | null;
|
|
5473
|
+
approvalId: number | null;
|
|
5167
5474
|
}>(
|
|
5168
5475
|
`SELECT t.id,
|
|
5169
5476
|
t.collaborator_id AS "collaboratorId",
|
|
@@ -5177,7 +5484,8 @@ export class OperationsService {
|
|
|
5177
5484
|
t.submitted_at AS "submittedAt",
|
|
5178
5485
|
t.reviewed_at AS "reviewedAt",
|
|
5179
5486
|
t.notes,
|
|
5180
|
-
approval.decision_note AS "decisionNote"
|
|
5487
|
+
approval.decision_note AS "decisionNote",
|
|
5488
|
+
approval.id AS "approvalId"
|
|
5181
5489
|
FROM operations_timesheet t
|
|
5182
5490
|
JOIN operations_collaborator c ON c.id = t.collaborator_id
|
|
5183
5491
|
LEFT JOIN operations_collaborator a ON a.id = t.approver_collaborator_id
|
|
@@ -8026,6 +8334,7 @@ export class OperationsService {
|
|
|
8026
8334
|
assigneeUserPhotoId: number | null;
|
|
8027
8335
|
assigneePersonAvatarId: number | null;
|
|
8028
8336
|
projectAssignmentId: number | null;
|
|
8337
|
+
commentCount: number;
|
|
8029
8338
|
createdAt: string;
|
|
8030
8339
|
}>(
|
|
8031
8340
|
`SELECT t.id,
|
|
@@ -8042,6 +8351,7 @@ export class OperationsService {
|
|
|
8042
8351
|
au.photo_id AS "assigneeUserPhotoId",
|
|
8043
8352
|
ap.avatar_id AS "assigneePersonAvatarId",
|
|
8044
8353
|
t.project_assignment_id AS "projectAssignmentId",
|
|
8354
|
+
COALESCE(task_comments.count, 0)::int AS "commentCount",
|
|
8045
8355
|
t.created_at AS "createdAt"
|
|
8046
8356
|
FROM operations_task t
|
|
8047
8357
|
LEFT JOIN operations_collaborator ac
|
|
@@ -8050,6 +8360,11 @@ export class OperationsService {
|
|
|
8050
8360
|
ON au.id = ac.user_id
|
|
8051
8361
|
LEFT JOIN person ap
|
|
8052
8362
|
ON ap.id = ac.person_id
|
|
8363
|
+
LEFT JOIN LATERAL (
|
|
8364
|
+
SELECT COUNT(*) AS count
|
|
8365
|
+
FROM operations_task_comment tc
|
|
8366
|
+
WHERE tc.task_id = t.id
|
|
8367
|
+
) task_comments ON TRUE
|
|
8053
8368
|
WHERE COALESCE(t.project_id, (
|
|
8054
8369
|
SELECT pa.project_id FROM operations_project_assignment pa
|
|
8055
8370
|
WHERE pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
|
|
@@ -8080,6 +8395,7 @@ export class OperationsService {
|
|
|
8080
8395
|
assigneePersonAvatarId: number | null;
|
|
8081
8396
|
projectAssignmentId: number | null;
|
|
8082
8397
|
projectId: number | null;
|
|
8398
|
+
commentCount: number;
|
|
8083
8399
|
createdAt: string;
|
|
8084
8400
|
deletedAt: string | null;
|
|
8085
8401
|
}>(
|
|
@@ -8098,6 +8414,7 @@ export class OperationsService {
|
|
|
8098
8414
|
ap.avatar_id AS "assigneePersonAvatarId",
|
|
8099
8415
|
t.project_assignment_id AS "projectAssignmentId",
|
|
8100
8416
|
COALESCE(t.project_id, pa.project_id) AS "projectId",
|
|
8417
|
+
COALESCE(task_comments.count, 0)::int AS "commentCount",
|
|
8101
8418
|
t.created_at AS "createdAt",
|
|
8102
8419
|
t.deleted_at AS "deletedAt"
|
|
8103
8420
|
FROM operations_task t
|
|
@@ -8107,6 +8424,11 @@ export class OperationsService {
|
|
|
8107
8424
|
LEFT JOIN person ap ON ap.id = ac.person_id
|
|
8108
8425
|
LEFT JOIN operations_project_assignment pa
|
|
8109
8426
|
ON pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
|
|
8427
|
+
LEFT JOIN LATERAL (
|
|
8428
|
+
SELECT COUNT(*) AS count
|
|
8429
|
+
FROM operations_task_comment tc
|
|
8430
|
+
WHERE tc.task_id = t.id
|
|
8431
|
+
) task_comments ON TRUE
|
|
8110
8432
|
WHERE t.id = $1`,
|
|
8111
8433
|
[taskId]
|
|
8112
8434
|
);
|
|
@@ -11052,12 +11374,18 @@ export class OperationsService {
|
|
|
11052
11374
|
au.photo_id AS "assigneeUserPhotoId",
|
|
11053
11375
|
ap.avatar_id AS "assigneePersonAvatarId",
|
|
11054
11376
|
t.project_assignment_id AS "projectAssignmentId",
|
|
11377
|
+
COALESCE(task_comments.count, 0)::int AS "commentCount",
|
|
11055
11378
|
t.created_at AS "createdAt"
|
|
11056
11379
|
FROM operations_task t
|
|
11057
11380
|
LEFT JOIN operations_collaborator ac
|
|
11058
11381
|
ON ac.id = t.assignee_collaborator_id AND ac.deleted_at IS NULL
|
|
11059
11382
|
LEFT JOIN "user" au ON au.id = ac.user_id
|
|
11060
11383
|
LEFT JOIN person ap ON ap.id = ac.person_id
|
|
11384
|
+
LEFT JOIN LATERAL (
|
|
11385
|
+
SELECT COUNT(*) AS count
|
|
11386
|
+
FROM operations_task_comment tc
|
|
11387
|
+
WHERE tc.task_id = t.id
|
|
11388
|
+
) task_comments ON TRUE
|
|
11061
11389
|
WHERE COALESCE(t.project_id, (
|
|
11062
11390
|
SELECT pa.project_id FROM operations_project_assignment pa
|
|
11063
11391
|
WHERE pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
|
|
@@ -11121,7 +11449,9 @@ export class OperationsService {
|
|
|
11121
11449
|
];
|
|
11122
11450
|
|
|
11123
11451
|
if (actor.collaboratorId) {
|
|
11124
|
-
|
|
11452
|
+
const p1 = this.param(params, actor.collaboratorId);
|
|
11453
|
+
const p2 = this.param(params, actor.collaboratorId);
|
|
11454
|
+
filters.push(`(pa.collaborator_id = ${p1} OR t.assignee_collaborator_id = ${p2})`);
|
|
11125
11455
|
}
|
|
11126
11456
|
|
|
11127
11457
|
if (pagination.search) {
|
|
@@ -11178,6 +11508,7 @@ export class OperationsService {
|
|
|
11178
11508
|
assigneeName: string | null;
|
|
11179
11509
|
assigneeUserPhotoId: number | null;
|
|
11180
11510
|
assigneePersonAvatarId: number | null;
|
|
11511
|
+
commentCount: number;
|
|
11181
11512
|
createdAt: string;
|
|
11182
11513
|
deletedAt: string | null;
|
|
11183
11514
|
}>(
|
|
@@ -11196,6 +11527,7 @@ export class OperationsService {
|
|
|
11196
11527
|
ac.display_name AS "assigneeName",
|
|
11197
11528
|
au.photo_id AS "assigneeUserPhotoId",
|
|
11198
11529
|
ap.avatar_id AS "assigneePersonAvatarId",
|
|
11530
|
+
COALESCE(task_comments.count, 0)::int AS "commentCount",
|
|
11199
11531
|
t.created_at AS "createdAt",
|
|
11200
11532
|
t.deleted_at AS "deletedAt"
|
|
11201
11533
|
FROM operations_task t
|
|
@@ -11208,6 +11540,11 @@ export class OperationsService {
|
|
|
11208
11540
|
ON au.id = ac.user_id
|
|
11209
11541
|
LEFT JOIN person ap
|
|
11210
11542
|
ON ap.id = ac.person_id
|
|
11543
|
+
LEFT JOIN LATERAL (
|
|
11544
|
+
SELECT COUNT(*) AS count
|
|
11545
|
+
FROM operations_task_comment tc
|
|
11546
|
+
WHERE tc.task_id = t.id
|
|
11547
|
+
) task_comments ON TRUE
|
|
11211
11548
|
JOIN operations_project p
|
|
11212
11549
|
ON p.id = COALESCE(t.project_id, pa.project_id)
|
|
11213
11550
|
WHERE ${whereClause}
|
|
@@ -11442,7 +11779,16 @@ export class OperationsService {
|
|
|
11442
11779
|
? { revenue: 0.9, cost: 0.96, backlog: 0.82 }
|
|
11443
11780
|
: { revenue: 1, cost: 1, backlog: 1 };
|
|
11444
11781
|
|
|
11445
|
-
const
|
|
11782
|
+
const fromDate = new Date(`${from}T00:00:00`);
|
|
11783
|
+
const toDate = new Date(`${to}T00:00:00`);
|
|
11784
|
+
const periodDays = Math.max(
|
|
11785
|
+
1,
|
|
11786
|
+
Math.floor((toDate.getTime() - fromDate.getTime()) / 86400000) + 1
|
|
11787
|
+
);
|
|
11788
|
+
const periodWeeks = Math.max(1, Math.ceil(periodDays / 7));
|
|
11789
|
+
const periodMonths = periodDays / 30.4375;
|
|
11790
|
+
|
|
11791
|
+
const params: unknown[] = [from, to, periodMonths, periodWeeks];
|
|
11446
11792
|
const where = [
|
|
11447
11793
|
'p.deleted_at IS NULL',
|
|
11448
11794
|
'(p.end_date IS NULL OR p.end_date >= $1::date)',
|
|
@@ -11469,6 +11815,8 @@ export class OperationsService {
|
|
|
11469
11815
|
weeklyHours: string | null;
|
|
11470
11816
|
actualHours: string | null;
|
|
11471
11817
|
billableHours: string | null;
|
|
11818
|
+
realizedCost: string | null;
|
|
11819
|
+
allocatedCost: string | null;
|
|
11472
11820
|
openTasks: string | null;
|
|
11473
11821
|
backlogHours: string | null;
|
|
11474
11822
|
futureDeliveries: string | null;
|
|
@@ -11487,6 +11835,8 @@ export class OperationsService {
|
|
|
11487
11835
|
COALESCE(assignment_stats.weekly_hours, 0)::text AS "weeklyHours",
|
|
11488
11836
|
COALESCE(time_stats.actual_hours, 0)::text AS "actualHours",
|
|
11489
11837
|
COALESCE(time_stats.billable_hours, 0)::text AS "billableHours",
|
|
11838
|
+
COALESCE(cost_stats.realized_cost, 0)::text AS "realizedCost",
|
|
11839
|
+
COALESCE(alloc_cost_stats.allocated_cost, 0)::text AS "allocatedCost",
|
|
11490
11840
|
COALESCE(task_stats.open_tasks, 0)::text AS "openTasks",
|
|
11491
11841
|
COALESCE(task_stats.backlog_hours, 0)::text AS "backlogHours",
|
|
11492
11842
|
COALESCE(task_stats.future_deliveries, 0)::text AS "futureDeliveries"
|
|
@@ -11515,6 +11865,146 @@ export class OperationsService {
|
|
|
11515
11865
|
AND entry.deleted_at IS NULL
|
|
11516
11866
|
AND entry.work_date BETWEEN $1::date AND $2::date
|
|
11517
11867
|
) time_stats ON TRUE
|
|
11868
|
+
LEFT JOIN LATERAL (
|
|
11869
|
+
SELECT COALESCE(
|
|
11870
|
+
SUM(
|
|
11871
|
+
entry.hours
|
|
11872
|
+
* (
|
|
11873
|
+
(
|
|
11874
|
+
COALESCE(collaborator_costs.salary_cost, 0)
|
|
11875
|
+
+ COALESCE(collaborator_costs.benefits_cost, 0)
|
|
11876
|
+
+ COALESCE(collaborator_costs.taxes_cost, 0)
|
|
11877
|
+
+ COALESCE(collaborator_costs.tools_cost, 0)
|
|
11878
|
+
)
|
|
11879
|
+
* $3::numeric
|
|
11880
|
+
/ GREATEST(
|
|
11881
|
+
COALESCE(collaborator_record.weekly_capacity_hours, 40)::numeric * $4::numeric,
|
|
11882
|
+
COALESCE(collaborator_hours.total_hours, 0),
|
|
11883
|
+
1
|
|
11884
|
+
)
|
|
11885
|
+
)
|
|
11886
|
+
),
|
|
11887
|
+
0
|
|
11888
|
+
) AS realized_cost
|
|
11889
|
+
FROM operations_timesheet_entry entry
|
|
11890
|
+
JOIN operations_project_assignment pa
|
|
11891
|
+
ON pa.id = entry.project_assignment_id
|
|
11892
|
+
AND pa.deleted_at IS NULL
|
|
11893
|
+
JOIN operations_collaborator collaborator_record
|
|
11894
|
+
ON collaborator_record.id = pa.collaborator_id
|
|
11895
|
+
AND collaborator_record.deleted_at IS NULL
|
|
11896
|
+
LEFT JOIN LATERAL (
|
|
11897
|
+
SELECT COALESCE(NULLIF(cost_totals.salary_cost, 0), compensation_history.amount, hiring_contract.budget_amount, 0) AS salary_cost,
|
|
11898
|
+
cost_totals.benefits_cost,
|
|
11899
|
+
cost_totals.taxes_cost,
|
|
11900
|
+
cost_totals.tools_cost
|
|
11901
|
+
FROM (
|
|
11902
|
+
SELECT COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('salario-base', 'pro-labore')), 0) AS salary_cost,
|
|
11903
|
+
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,
|
|
11904
|
+
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,
|
|
11905
|
+
COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('software-licenca', 'equipamento')), 0) AS tools_cost
|
|
11906
|
+
FROM operations_collaborator_cost cost
|
|
11907
|
+
LEFT JOIN operations_cost_type cost_type
|
|
11908
|
+
ON cost_type.id = cost.cost_type_id
|
|
11909
|
+
WHERE cost.collaborator_id = collaborator_record.id
|
|
11910
|
+
AND (cost.start_date IS NULL OR cost.start_date <= $2::date)
|
|
11911
|
+
AND (cost.end_date IS NULL OR cost.end_date >= $1::date)
|
|
11912
|
+
) cost_totals
|
|
11913
|
+
LEFT JOIN LATERAL (
|
|
11914
|
+
SELECT h.amount
|
|
11915
|
+
FROM operations_collaborator_compensation_history h
|
|
11916
|
+
WHERE h.collaborator_id = collaborator_record.id
|
|
11917
|
+
AND (h.effective_date IS NULL OR h.effective_date <= $2::date)
|
|
11918
|
+
ORDER BY h.effective_date DESC NULLS LAST, h.created_at DESC
|
|
11919
|
+
LIMIT 1
|
|
11920
|
+
) compensation_history ON TRUE
|
|
11921
|
+
LEFT JOIN LATERAL (
|
|
11922
|
+
SELECT oc.budget_amount
|
|
11923
|
+
FROM operations_contract oc
|
|
11924
|
+
WHERE oc.related_collaborator_id = collaborator_record.id
|
|
11925
|
+
AND oc.deleted_at IS NULL
|
|
11926
|
+
ORDER BY CASE WHEN oc.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
|
|
11927
|
+
oc.created_at DESC
|
|
11928
|
+
LIMIT 1
|
|
11929
|
+
) hiring_contract ON TRUE
|
|
11930
|
+
) collaborator_costs ON TRUE
|
|
11931
|
+
LEFT JOIN LATERAL (
|
|
11932
|
+
SELECT COALESCE(SUM(entry2.hours), 0) AS total_hours
|
|
11933
|
+
FROM operations_timesheet_entry entry2
|
|
11934
|
+
JOIN operations_project_assignment pa2
|
|
11935
|
+
ON pa2.id = entry2.project_assignment_id
|
|
11936
|
+
AND pa2.deleted_at IS NULL
|
|
11937
|
+
WHERE pa2.collaborator_id = collaborator_record.id
|
|
11938
|
+
AND entry2.deleted_at IS NULL
|
|
11939
|
+
AND entry2.work_date BETWEEN $1::date AND $2::date
|
|
11940
|
+
) collaborator_hours ON TRUE
|
|
11941
|
+
WHERE pa.project_id = p.id
|
|
11942
|
+
AND entry.deleted_at IS NULL
|
|
11943
|
+
AND entry.work_date BETWEEN $1::date AND $2::date
|
|
11944
|
+
) cost_stats ON TRUE
|
|
11945
|
+
LEFT JOIN LATERAL (
|
|
11946
|
+
SELECT COALESCE(
|
|
11947
|
+
SUM(
|
|
11948
|
+
pa.weekly_hours
|
|
11949
|
+
* (
|
|
11950
|
+
(
|
|
11951
|
+
COALESCE(alloc_costs.salary_cost, 0)
|
|
11952
|
+
+ COALESCE(alloc_costs.benefits_cost, 0)
|
|
11953
|
+
+ COALESCE(alloc_costs.taxes_cost, 0)
|
|
11954
|
+
+ COALESCE(alloc_costs.tools_cost, 0)
|
|
11955
|
+
)
|
|
11956
|
+
* $3::numeric
|
|
11957
|
+
/ GREATEST(
|
|
11958
|
+
COALESCE(alloc_col.weekly_capacity_hours, 40)::numeric,
|
|
11959
|
+
1
|
|
11960
|
+
)
|
|
11961
|
+
)
|
|
11962
|
+
),
|
|
11963
|
+
0
|
|
11964
|
+
) AS allocated_cost
|
|
11965
|
+
FROM operations_project_assignment pa
|
|
11966
|
+
JOIN operations_collaborator alloc_col
|
|
11967
|
+
ON alloc_col.id = pa.collaborator_id
|
|
11968
|
+
AND alloc_col.deleted_at IS NULL
|
|
11969
|
+
LEFT JOIN LATERAL (
|
|
11970
|
+
SELECT COALESCE(NULLIF(ct.salary_cost, 0), ch.amount, hc.budget_amount, 0) AS salary_cost,
|
|
11971
|
+
ct.benefits_cost,
|
|
11972
|
+
ct.taxes_cost,
|
|
11973
|
+
ct.tools_cost
|
|
11974
|
+
FROM (
|
|
11975
|
+
SELECT COALESCE(SUM(c.amount) FILTER (WHERE c.recurrence::text = 'monthly' AND ct2.slug IN ('salario-base', 'pro-labore')), 0) AS salary_cost,
|
|
11976
|
+
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,
|
|
11977
|
+
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,
|
|
11978
|
+
COALESCE(SUM(c.amount) FILTER (WHERE c.recurrence::text = 'monthly' AND ct2.slug IN ('software-licenca', 'equipamento')), 0) AS tools_cost
|
|
11979
|
+
FROM operations_collaborator_cost c
|
|
11980
|
+
LEFT JOIN operations_cost_type ct2
|
|
11981
|
+
ON ct2.id = c.cost_type_id
|
|
11982
|
+
WHERE c.collaborator_id = alloc_col.id
|
|
11983
|
+
AND (c.start_date IS NULL OR c.start_date <= $2::date)
|
|
11984
|
+
AND (c.end_date IS NULL OR c.end_date >= $1::date)
|
|
11985
|
+
) ct
|
|
11986
|
+
LEFT JOIN LATERAL (
|
|
11987
|
+
SELECT h.amount
|
|
11988
|
+
FROM operations_collaborator_compensation_history h
|
|
11989
|
+
WHERE h.collaborator_id = alloc_col.id
|
|
11990
|
+
AND (h.effective_date IS NULL OR h.effective_date <= $2::date)
|
|
11991
|
+
ORDER BY h.effective_date DESC NULLS LAST, h.created_at DESC
|
|
11992
|
+
LIMIT 1
|
|
11993
|
+
) ch ON TRUE
|
|
11994
|
+
LEFT JOIN LATERAL (
|
|
11995
|
+
SELECT oc.budget_amount
|
|
11996
|
+
FROM operations_contract oc
|
|
11997
|
+
WHERE oc.related_collaborator_id = alloc_col.id
|
|
11998
|
+
AND oc.deleted_at IS NULL
|
|
11999
|
+
ORDER BY CASE WHEN oc.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
|
|
12000
|
+
oc.created_at DESC
|
|
12001
|
+
LIMIT 1
|
|
12002
|
+
) hc ON TRUE
|
|
12003
|
+
) alloc_costs ON TRUE
|
|
12004
|
+
WHERE pa.project_id = p.id
|
|
12005
|
+
AND pa.deleted_at IS NULL
|
|
12006
|
+
AND pa.status IN ('planned', 'active')
|
|
12007
|
+
) alloc_cost_stats ON TRUE
|
|
11518
12008
|
LEFT JOIN LATERAL (
|
|
11519
12009
|
SELECT COUNT(*) FILTER (WHERE task.status IN ('todo', 'doing', 'review')) AS open_tasks,
|
|
11520
12010
|
COALESCE(SUM(task.estimate_hours) FILTER (WHERE task.status IN ('todo', 'doing', 'review')), 0) AS backlog_hours,
|
|
@@ -11528,12 +12018,6 @@ export class OperationsService {
|
|
|
11528
12018
|
params
|
|
11529
12019
|
);
|
|
11530
12020
|
|
|
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
12021
|
const rows = dbRows
|
|
11538
12022
|
.map((row) => {
|
|
11539
12023
|
const progress = Number(row.progressPercent ?? 0);
|
|
@@ -11541,7 +12025,12 @@ export class OperationsService {
|
|
|
11541
12025
|
const recognizedRevenue = contractedRevenue * (progress / 100);
|
|
11542
12026
|
const actualHours = Number(row.actualHours ?? 0);
|
|
11543
12027
|
const plannedHours = Math.max(Number(row.weeklyHours ?? 0) * periodWeeks, actualHours);
|
|
11544
|
-
const realizedCost = 0;
|
|
12028
|
+
const realizedCost = Number(row.realizedCost ?? 0);
|
|
12029
|
+
const allocatedCost = Number(row.allocatedCost ?? 0);
|
|
12030
|
+
const consumedHoursCost = realizedCost;
|
|
12031
|
+
const idlenessHours = Math.max(plannedHours - actualHours, 0);
|
|
12032
|
+
const idlenessRate = plannedHours > 0 ? (idlenessHours / plannedHours) * 100 : 0;
|
|
12033
|
+
const idlenessCost = Math.max(allocatedCost - consumedHoursCost, 0);
|
|
11545
12034
|
const reportStatus =
|
|
11546
12035
|
row.status === 'paused'
|
|
11547
12036
|
? 'paused'
|
|
@@ -11576,7 +12065,7 @@ export class OperationsService {
|
|
|
11576
12065
|
contractedRevenue,
|
|
11577
12066
|
recognizedRevenue,
|
|
11578
12067
|
realizedCost,
|
|
11579
|
-
forecastCost: realizedCost,
|
|
12068
|
+
forecastCost: realizedCost * multiplier.cost,
|
|
11580
12069
|
teamCost: realizedCost,
|
|
11581
12070
|
infraCost: 0,
|
|
11582
12071
|
licenseCost: 0,
|
|
@@ -11592,6 +12081,10 @@ export class OperationsService {
|
|
|
11592
12081
|
financialProgress: contractedRevenue ? (recognizedRevenue / contractedRevenue) * 100 : 0,
|
|
11593
12082
|
backlogValue: Math.max(contractedRevenue - recognizedRevenue, 0),
|
|
11594
12083
|
futureDeliveries: Number(row.futureDeliveries ?? 0),
|
|
12084
|
+
allocatedCost,
|
|
12085
|
+
consumedHoursCost,
|
|
12086
|
+
idlenessRate,
|
|
12087
|
+
idlenessCost,
|
|
11595
12088
|
risk,
|
|
11596
12089
|
recommendation:
|
|
11597
12090
|
risk === 'alto'
|
|
@@ -11617,6 +12110,9 @@ export class OperationsService {
|
|
|
11617
12110
|
acc.avgDeadline += row.physicalProgress;
|
|
11618
12111
|
acc.avgAllocation += row.allocatedCapacity;
|
|
11619
12112
|
acc.atRisk += row.risk === 'alto' ? 1 : 0;
|
|
12113
|
+
acc.allocatedCost += row.allocatedCost;
|
|
12114
|
+
acc.consumedHoursCost += row.consumedHoursCost;
|
|
12115
|
+
acc.idlenessCost += row.idlenessCost;
|
|
11620
12116
|
return acc;
|
|
11621
12117
|
},
|
|
11622
12118
|
{
|
|
@@ -11635,6 +12131,11 @@ export class OperationsService {
|
|
|
11635
12131
|
avgAllocation: 0,
|
|
11636
12132
|
atRisk: 0,
|
|
11637
12133
|
burnRate: 0,
|
|
12134
|
+
allocatedCost: 0,
|
|
12135
|
+
consumedHoursCost: 0,
|
|
12136
|
+
idlenessCost: 0,
|
|
12137
|
+
idlenessRate: 0,
|
|
12138
|
+
plannedProfit: 0,
|
|
11638
12139
|
}
|
|
11639
12140
|
);
|
|
11640
12141
|
summary.profit = summary.recognizedRevenue - summary.realizedCost;
|
|
@@ -11642,6 +12143,10 @@ export class OperationsService {
|
|
|
11642
12143
|
summary.avgDeadline = rows.length ? summary.avgDeadline / rows.length : 0;
|
|
11643
12144
|
summary.avgAllocation = rows.length ? summary.avgAllocation / rows.length : 0;
|
|
11644
12145
|
summary.burnRate = summary.plannedHours ? (summary.actualHours / summary.plannedHours) * 100 : 0;
|
|
12146
|
+
summary.plannedProfit = summary.contractedRevenue - summary.allocatedCost;
|
|
12147
|
+
summary.idlenessRate = summary.plannedHours > 0
|
|
12148
|
+
? Math.max(0, (summary.plannedHours - summary.actualHours) / summary.plannedHours * 100)
|
|
12149
|
+
: 0;
|
|
11645
12150
|
|
|
11646
12151
|
const forecast = Array.from({ length: 12 }, (_, index) => {
|
|
11647
12152
|
const monthDate = new Date(fromDate);
|
|
@@ -11744,6 +12249,15 @@ export class OperationsService {
|
|
|
11744
12249
|
: scenario === 'conservative'
|
|
11745
12250
|
? { revenue: 0.9, cost: 0.96, capacity: 0.94 }
|
|
11746
12251
|
: { revenue: 1, cost: 1, capacity: 1 };
|
|
12252
|
+
const fromDate = new Date(`${from}T00:00:00`);
|
|
12253
|
+
const toDate = new Date(`${to}T00:00:00`);
|
|
12254
|
+
const periodDays = Math.max(
|
|
12255
|
+
1,
|
|
12256
|
+
Math.floor((toDate.getTime() - fromDate.getTime()) / 86400000) + 1
|
|
12257
|
+
);
|
|
12258
|
+
const periodWeeks = Math.max(1, Math.ceil(periodDays / 7));
|
|
12259
|
+
const periodMonths = periodDays / 30.4375;
|
|
12260
|
+
|
|
11747
12261
|
const params: unknown[] = [from, to];
|
|
11748
12262
|
const where = [
|
|
11749
12263
|
'c.deleted_at IS NULL',
|
|
@@ -11774,6 +12288,11 @@ export class OperationsService {
|
|
|
11774
12288
|
taxesCost: string | null;
|
|
11775
12289
|
toolsCost: string | null;
|
|
11776
12290
|
billableValue: string | null;
|
|
12291
|
+
plannedAllocatedHours: string | null;
|
|
12292
|
+
plannedBillableHours: string | null;
|
|
12293
|
+
openTaskHours: string | null;
|
|
12294
|
+
openTaskBillableHours: string | null;
|
|
12295
|
+
openTasks: string | null;
|
|
11777
12296
|
allocatedHours: string | null;
|
|
11778
12297
|
billableHours: string | null;
|
|
11779
12298
|
projects: string | null;
|
|
@@ -11792,9 +12311,14 @@ export class OperationsService {
|
|
|
11792
12311
|
COALESCE(cost_stats.taxes_cost, 0)::text AS "taxesCost",
|
|
11793
12312
|
COALESCE(cost_stats.tools_cost, 0)::text AS "toolsCost",
|
|
11794
12313
|
COALESCE(value_stats.billable_value, 0)::text AS "billableValue",
|
|
12314
|
+
COALESCE(assignment_stats.planned_allocated_hours, 0)::text AS "plannedAllocatedHours",
|
|
12315
|
+
COALESCE(assignment_stats.planned_billable_hours, 0)::text AS "plannedBillableHours",
|
|
12316
|
+
COALESCE(task_stats.open_task_hours, 0)::text AS "openTaskHours",
|
|
12317
|
+
COALESCE(task_stats.open_task_billable_hours, 0)::text AS "openTaskBillableHours",
|
|
12318
|
+
COALESCE(task_stats.open_tasks, 0)::text AS "openTasks",
|
|
11795
12319
|
COALESCE(value_stats.allocated_hours, 0)::text AS "allocatedHours",
|
|
11796
12320
|
COALESCE(value_stats.billable_hours, 0)::text AS "billableHours",
|
|
11797
|
-
COALESCE(
|
|
12321
|
+
COALESCE(assignment_stats.projects, 0)::text AS projects
|
|
11798
12322
|
FROM operations_collaborator c
|
|
11799
12323
|
LEFT JOIN person person_record ON person_record.id = c.person_id
|
|
11800
12324
|
LEFT JOIN operations_department department_record
|
|
@@ -11807,16 +12331,85 @@ export class OperationsService {
|
|
|
11807
12331
|
ON collaborator_type.id = c.collaborator_type_id
|
|
11808
12332
|
AND collaborator_type.deleted_at IS NULL
|
|
11809
12333
|
LEFT JOIN LATERAL (
|
|
11810
|
-
SELECT COALESCE(
|
|
11811
|
-
|
|
11812
|
-
|
|
11813
|
-
|
|
11814
|
-
FROM
|
|
11815
|
-
|
|
11816
|
-
|
|
11817
|
-
|
|
11818
|
-
|
|
12334
|
+
SELECT COALESCE(NULLIF(cost_totals.salary_cost, 0), compensation_history.amount, hiring_contract.budget_amount, 0) AS salary_cost,
|
|
12335
|
+
cost_totals.benefits_cost,
|
|
12336
|
+
cost_totals.taxes_cost,
|
|
12337
|
+
cost_totals.tools_cost
|
|
12338
|
+
FROM (
|
|
12339
|
+
SELECT COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('salario-base', 'pro-labore')), 0) AS salary_cost,
|
|
12340
|
+
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,
|
|
12341
|
+
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,
|
|
12342
|
+
COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('software-licenca', 'equipamento')), 0) AS tools_cost
|
|
12343
|
+
FROM operations_collaborator_cost cost
|
|
12344
|
+
LEFT JOIN operations_cost_type cost_type
|
|
12345
|
+
ON cost_type.id = cost.cost_type_id
|
|
12346
|
+
WHERE cost.collaborator_id = c.id
|
|
12347
|
+
AND (cost.start_date IS NULL OR cost.start_date <= $2::date)
|
|
12348
|
+
AND (cost.end_date IS NULL OR cost.end_date >= $1::date)
|
|
12349
|
+
) cost_totals
|
|
12350
|
+
LEFT JOIN LATERAL (
|
|
12351
|
+
SELECT h.amount
|
|
12352
|
+
FROM operations_collaborator_compensation_history h
|
|
12353
|
+
WHERE h.collaborator_id = c.id
|
|
12354
|
+
AND (h.effective_date IS NULL OR h.effective_date <= $2::date)
|
|
12355
|
+
ORDER BY h.effective_date DESC NULLS LAST, h.created_at DESC
|
|
12356
|
+
LIMIT 1
|
|
12357
|
+
) compensation_history ON TRUE
|
|
12358
|
+
LEFT JOIN LATERAL (
|
|
12359
|
+
SELECT oc.budget_amount
|
|
12360
|
+
FROM operations_contract oc
|
|
12361
|
+
WHERE oc.related_collaborator_id = c.id
|
|
12362
|
+
AND oc.deleted_at IS NULL
|
|
12363
|
+
ORDER BY CASE WHEN oc.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
|
|
12364
|
+
oc.created_at DESC
|
|
12365
|
+
LIMIT 1
|
|
12366
|
+
) hiring_contract ON TRUE
|
|
11819
12367
|
) cost_stats ON TRUE
|
|
12368
|
+
LEFT JOIN LATERAL (
|
|
12369
|
+
SELECT COALESCE(
|
|
12370
|
+
SUM(
|
|
12371
|
+
COALESCE(
|
|
12372
|
+
pa.weekly_hours,
|
|
12373
|
+
COALESCE(c.weekly_capacity_hours, 40) * COALESCE(pa.allocation_percent, 0) / 100
|
|
12374
|
+
) * GREATEST(
|
|
12375
|
+
CEIL(
|
|
12376
|
+
(
|
|
12377
|
+
LEAST(COALESCE(pa.end_date, $2::date), $2::date)
|
|
12378
|
+
- GREATEST(COALESCE(pa.start_date, $1::date), $1::date)
|
|
12379
|
+
+ 1
|
|
12380
|
+
) / 7.0
|
|
12381
|
+
),
|
|
12382
|
+
0
|
|
12383
|
+
)
|
|
12384
|
+
),
|
|
12385
|
+
0
|
|
12386
|
+
) AS planned_allocated_hours,
|
|
12387
|
+
COALESCE(
|
|
12388
|
+
SUM(
|
|
12389
|
+
COALESCE(
|
|
12390
|
+
pa.weekly_hours,
|
|
12391
|
+
COALESCE(c.weekly_capacity_hours, 40) * COALESCE(pa.allocation_percent, 0) / 100
|
|
12392
|
+
) * GREATEST(
|
|
12393
|
+
CEIL(
|
|
12394
|
+
(
|
|
12395
|
+
LEAST(COALESCE(pa.end_date, $2::date), $2::date)
|
|
12396
|
+
- GREATEST(COALESCE(pa.start_date, $1::date), $1::date)
|
|
12397
|
+
+ 1
|
|
12398
|
+
) / 7.0
|
|
12399
|
+
),
|
|
12400
|
+
0
|
|
12401
|
+
)
|
|
12402
|
+
) FILTER (WHERE pa.is_billable = true),
|
|
12403
|
+
0
|
|
12404
|
+
) AS planned_billable_hours,
|
|
12405
|
+
COUNT(DISTINCT pa.project_id) AS projects
|
|
12406
|
+
FROM operations_project_assignment pa
|
|
12407
|
+
WHERE pa.collaborator_id = c.id
|
|
12408
|
+
AND pa.deleted_at IS NULL
|
|
12409
|
+
AND pa.status IN ('planned', 'active')
|
|
12410
|
+
AND (pa.start_date IS NULL OR pa.start_date <= $2::date)
|
|
12411
|
+
AND (pa.end_date IS NULL OR pa.end_date >= $1::date)
|
|
12412
|
+
) assignment_stats ON TRUE
|
|
11820
12413
|
LEFT JOIN LATERAL (
|
|
11821
12414
|
SELECT COALESCE(SUM(entry.hours), 0) AS allocated_hours,
|
|
11822
12415
|
COALESCE(SUM(entry.hours) FILTER (WHERE pa.is_billable = true), 0) AS billable_hours,
|
|
@@ -11830,31 +12423,50 @@ export class OperationsService {
|
|
|
11830
12423
|
AND entry.work_date BETWEEN $1::date AND $2::date
|
|
11831
12424
|
) value_stats ON TRUE
|
|
11832
12425
|
LEFT JOIN LATERAL (
|
|
11833
|
-
SELECT COUNT(
|
|
11834
|
-
|
|
11835
|
-
|
|
12426
|
+
SELECT COUNT(*) AS open_tasks,
|
|
12427
|
+
COALESCE(SUM(COALESCE(task.estimate_hours, 0)), 0) AS open_task_hours,
|
|
12428
|
+
COALESCE(
|
|
12429
|
+
SUM(COALESCE(task.estimate_hours, 0)) FILTER (WHERE pa.is_billable = true),
|
|
12430
|
+
0
|
|
12431
|
+
) AS open_task_billable_hours
|
|
12432
|
+
FROM operations_task task
|
|
12433
|
+
LEFT JOIN operations_project_assignment pa
|
|
12434
|
+
ON pa.id = task.project_assignment_id
|
|
11836
12435
|
AND pa.deleted_at IS NULL
|
|
11837
|
-
|
|
11838
|
-
|
|
12436
|
+
WHERE task.deleted_at IS NULL
|
|
12437
|
+
AND task.status IN ('todo', 'doing', 'review')
|
|
12438
|
+
AND (
|
|
12439
|
+
task.assignee_collaborator_id = c.id
|
|
12440
|
+
OR pa.collaborator_id = c.id
|
|
12441
|
+
)
|
|
12442
|
+
) task_stats ON TRUE
|
|
11839
12443
|
WHERE ${where.join(' AND ')}
|
|
11840
12444
|
ORDER BY name ASC`,
|
|
11841
12445
|
params
|
|
11842
12446
|
);
|
|
11843
12447
|
|
|
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
12448
|
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);
|
|
12449
|
+
const salaryCost = Number(row.salaryCost ?? 0) * periodMonths;
|
|
12450
|
+
const benefitsCost = Number(row.benefitsCost ?? 0) * periodMonths;
|
|
12451
|
+
const taxesCost = Number(row.taxesCost ?? 0) * periodMonths;
|
|
12452
|
+
const toolsCost = Number(row.toolsCost ?? 0) * periodMonths;
|
|
11855
12453
|
const availableHours = Number(row.weeklyCapacityHours ?? 40) * periodWeeks;
|
|
11856
|
-
const
|
|
11857
|
-
const
|
|
12454
|
+
const plannedAllocatedHours = Number(row.plannedAllocatedHours ?? 0);
|
|
12455
|
+
const plannedBillableHours = Number(row.plannedBillableHours ?? 0);
|
|
12456
|
+
const openTaskHours = Number(row.openTaskHours ?? 0);
|
|
12457
|
+
const openTaskBillableHours = Number(row.openTaskBillableHours ?? 0);
|
|
12458
|
+
const actualAllocatedHours = Number(row.allocatedHours ?? 0);
|
|
12459
|
+
const actualBillableHours = Number(row.billableHours ?? 0);
|
|
12460
|
+
const allocatedHours = Math.max(
|
|
12461
|
+
actualAllocatedHours,
|
|
12462
|
+
plannedAllocatedHours,
|
|
12463
|
+
openTaskHours
|
|
12464
|
+
);
|
|
12465
|
+
const billableHours = Math.max(
|
|
12466
|
+
actualBillableHours,
|
|
12467
|
+
plannedBillableHours,
|
|
12468
|
+
openTaskBillableHours
|
|
12469
|
+
);
|
|
11858
12470
|
const allocation = availableHours ? (allocatedHours / availableHours) * 100 : 0;
|
|
11859
12471
|
const risk = allocation >= 98 ? 'alto' : allocation < 75 ? 'médio' : 'baixo';
|
|
11860
12472
|
return {
|
|
@@ -11929,7 +12541,11 @@ export class OperationsService {
|
|
|
11929
12541
|
summary.freeHours = Math.max(summary.availableHours - summary.allocatedHours, 0);
|
|
11930
12542
|
summary.allocation = summary.availableHours ? (summary.allocatedHours / summary.availableHours) * 100 : 0;
|
|
11931
12543
|
summary.utilization = summary.availableHours ? (summary.billableHours / summary.availableHours) * 100 : 0;
|
|
11932
|
-
summary.hourlyCost = summary.allocatedHours
|
|
12544
|
+
summary.hourlyCost = summary.allocatedHours
|
|
12545
|
+
? summary.cost / summary.allocatedHours
|
|
12546
|
+
: summary.availableHours
|
|
12547
|
+
? summary.cost / summary.availableHours
|
|
12548
|
+
: 0;
|
|
11933
12549
|
|
|
11934
12550
|
const forecast = Array.from({ length: 12 }, (_, index) => {
|
|
11935
12551
|
const monthDate = new Date(fromDate);
|
|
@@ -12173,4 +12789,1623 @@ export class OperationsService {
|
|
|
12173
12789
|
|
|
12174
12790
|
return { success: true };
|
|
12175
12791
|
}
|
|
12792
|
+
|
|
12793
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
12794
|
+
// Project Cost Categories
|
|
12795
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
12796
|
+
|
|
12797
|
+
async listProjectCostCategories(userId: number, filters: { search?: string; is_active?: boolean; pageSize?: number; page?: number } = {}) {
|
|
12798
|
+
await this.getActorContext(userId);
|
|
12799
|
+
const localeId = await this.resolvePreferredLocaleId();
|
|
12800
|
+
|
|
12801
|
+
const params: unknown[] = [localeId];
|
|
12802
|
+
const where: string[] = ['pcc.deleted_at IS NULL'];
|
|
12803
|
+
|
|
12804
|
+
if (filters.is_active === true) {
|
|
12805
|
+
where.push('pcc.is_active = true');
|
|
12806
|
+
}
|
|
12807
|
+
|
|
12808
|
+
if (filters.search?.trim()) {
|
|
12809
|
+
const p = this.param(params, `%${filters.search.trim()}%`);
|
|
12810
|
+
where.push(`(COALESCE(pccl.name, pcc.slug) ILIKE ${p} OR COALESCE(pcc.slug, '') ILIKE ${p})`);
|
|
12811
|
+
}
|
|
12812
|
+
|
|
12813
|
+
const whereClause = `WHERE ${where.join(' AND ')}`;
|
|
12814
|
+
|
|
12815
|
+
return this.queryRows<{
|
|
12816
|
+
id: number;
|
|
12817
|
+
slug: string;
|
|
12818
|
+
name: string | null;
|
|
12819
|
+
description: string | null;
|
|
12820
|
+
icon: string | null;
|
|
12821
|
+
color: string | null;
|
|
12822
|
+
isActive: boolean;
|
|
12823
|
+
sortOrder: number;
|
|
12824
|
+
createdAt: string;
|
|
12825
|
+
}>(
|
|
12826
|
+
`SELECT pcc.id,
|
|
12827
|
+
pcc.slug,
|
|
12828
|
+
COALESCE(pccl.name, pcc.slug) AS name,
|
|
12829
|
+
pccl.description,
|
|
12830
|
+
pcc.icon,
|
|
12831
|
+
pcc.color,
|
|
12832
|
+
pcc.is_active AS "isActive",
|
|
12833
|
+
pcc.sort_order AS "sortOrder",
|
|
12834
|
+
pcc.created_at AS "createdAt"
|
|
12835
|
+
FROM operations_project_cost_category pcc
|
|
12836
|
+
LEFT JOIN LATERAL (
|
|
12837
|
+
SELECT l.name, l.description
|
|
12838
|
+
FROM operations_project_cost_category_locale l
|
|
12839
|
+
WHERE l.operations_project_cost_category_id = pcc.id
|
|
12840
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
12841
|
+
l.id ASC
|
|
12842
|
+
LIMIT 1
|
|
12843
|
+
) pccl ON TRUE
|
|
12844
|
+
${whereClause}
|
|
12845
|
+
ORDER BY pcc.sort_order ASC, COALESCE(pccl.name, pcc.slug) ASC`,
|
|
12846
|
+
params
|
|
12847
|
+
);
|
|
12848
|
+
}
|
|
12849
|
+
|
|
12850
|
+
async createProjectCostCategory(userId: number, data: { slug: string; name?: any; description?: any; icon?: string | null; color?: string | null; is_active?: boolean; sort_order?: number }) {
|
|
12851
|
+
const actor = await this.getActorContext(userId);
|
|
12852
|
+
this.ensureDirector(actor);
|
|
12853
|
+
|
|
12854
|
+
const slug = data.slug?.trim();
|
|
12855
|
+
if (!slug) {
|
|
12856
|
+
throw new BadRequestException('Cost category slug is required.');
|
|
12857
|
+
}
|
|
12858
|
+
|
|
12859
|
+
return this.prisma.$transaction(async (tx) => {
|
|
12860
|
+
const localeId = await this.resolvePreferredLocaleId(tx as any);
|
|
12861
|
+
|
|
12862
|
+
const created = (await (tx as any).$queryRawUnsafe(
|
|
12863
|
+
`INSERT INTO operations_project_cost_category (slug, icon, color, is_active, sort_order, created_at, updated_at)
|
|
12864
|
+
VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
|
|
12865
|
+
RETURNING id`,
|
|
12866
|
+
slug,
|
|
12867
|
+
data.icon ?? null,
|
|
12868
|
+
data.color ?? null,
|
|
12869
|
+
data.is_active ?? true,
|
|
12870
|
+
data.sort_order ?? 0,
|
|
12871
|
+
)) as { id: number }[];
|
|
12872
|
+
|
|
12873
|
+
const createdId = created[0]?.id;
|
|
12874
|
+
if (!createdId) {
|
|
12875
|
+
throw new BadRequestException('Unable to create project cost category.');
|
|
12876
|
+
}
|
|
12877
|
+
|
|
12878
|
+
const name = typeof data.name === 'string' ? data.name : (data.name ? JSON.stringify(data.name) : slug);
|
|
12879
|
+
const description = typeof data.description === 'string' ? data.description : (data.description ? JSON.stringify(data.description) : null);
|
|
12880
|
+
|
|
12881
|
+
if (localeId && name) {
|
|
12882
|
+
await (tx as any).$executeRawUnsafe(
|
|
12883
|
+
`INSERT INTO operations_project_cost_category_locale (operations_project_cost_category_id, locale_id, name, description)
|
|
12884
|
+
VALUES ($1, $2, $3, $4)`,
|
|
12885
|
+
createdId,
|
|
12886
|
+
localeId,
|
|
12887
|
+
name,
|
|
12888
|
+
description ?? null,
|
|
12889
|
+
);
|
|
12890
|
+
}
|
|
12891
|
+
|
|
12892
|
+
const rows = (await (tx as any).$queryRawUnsafe(
|
|
12893
|
+
`SELECT pcc.id,
|
|
12894
|
+
pcc.slug,
|
|
12895
|
+
COALESCE(pccl.name, pcc.slug) AS name,
|
|
12896
|
+
pccl.description,
|
|
12897
|
+
pcc.icon,
|
|
12898
|
+
pcc.color,
|
|
12899
|
+
pcc.is_active AS "isActive",
|
|
12900
|
+
pcc.sort_order AS "sortOrder",
|
|
12901
|
+
pcc.created_at AS "createdAt"
|
|
12902
|
+
FROM operations_project_cost_category pcc
|
|
12903
|
+
LEFT JOIN operations_project_cost_category_locale pccl
|
|
12904
|
+
ON pccl.operations_project_cost_category_id = pcc.id AND pccl.locale_id = $2
|
|
12905
|
+
WHERE pcc.id = $1`,
|
|
12906
|
+
createdId,
|
|
12907
|
+
localeId,
|
|
12908
|
+
)) as { id: number; slug: string; name: string; description: string | null; icon: string | null; color: string | null; isActive: boolean; sortOrder: number; createdAt: string }[];
|
|
12909
|
+
|
|
12910
|
+
return rows[0] ?? null;
|
|
12911
|
+
});
|
|
12912
|
+
}
|
|
12913
|
+
|
|
12914
|
+
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 }>) {
|
|
12915
|
+
const actor = await this.getActorContext(userId);
|
|
12916
|
+
this.ensureDirector(actor);
|
|
12917
|
+
|
|
12918
|
+
const category = await this.querySingle<{ id: number }>(
|
|
12919
|
+
`SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
12920
|
+
[id]
|
|
12921
|
+
);
|
|
12922
|
+
if (!category) {
|
|
12923
|
+
throw new NotFoundException('Project cost category not found.');
|
|
12924
|
+
}
|
|
12925
|
+
|
|
12926
|
+
const sets: string[] = [];
|
|
12927
|
+
const params: unknown[] = [];
|
|
12928
|
+
|
|
12929
|
+
if (data.slug !== undefined) sets.push(`slug = ${this.param(params, data.slug)}`);
|
|
12930
|
+
if (data.icon !== undefined) sets.push(`icon = ${this.param(params, data.icon)}`);
|
|
12931
|
+
if (data.color !== undefined) sets.push(`color = ${this.param(params, data.color)}`);
|
|
12932
|
+
if (data.is_active !== undefined) sets.push(`is_active = ${this.param(params, data.is_active)}`);
|
|
12933
|
+
if (data.sort_order !== undefined) sets.push(`sort_order = ${this.param(params, data.sort_order)}`);
|
|
12934
|
+
|
|
12935
|
+
if (sets.length > 0) {
|
|
12936
|
+
sets.push(`updated_at = NOW()`);
|
|
12937
|
+
await this.prisma.$queryRawUnsafe(
|
|
12938
|
+
`UPDATE operations_project_cost_category SET ${sets.join(', ')} WHERE id = ${this.param(params, id)}`,
|
|
12939
|
+
...params
|
|
12940
|
+
);
|
|
12941
|
+
}
|
|
12942
|
+
|
|
12943
|
+
if (data.name !== undefined || data.description !== undefined) {
|
|
12944
|
+
const localeId = await this.resolvePreferredLocaleId();
|
|
12945
|
+
if (localeId) {
|
|
12946
|
+
const name = typeof data.name === 'string' ? data.name : (data.name ? JSON.stringify(data.name) : undefined);
|
|
12947
|
+
const description = typeof data.description === 'string' ? data.description : (data.description ? JSON.stringify(data.description) : null);
|
|
12948
|
+
const existing = await this.querySingle<{ id: number }>(
|
|
12949
|
+
`SELECT id FROM operations_project_cost_category_locale WHERE operations_project_cost_category_id = $1 AND locale_id = $2 LIMIT 1`,
|
|
12950
|
+
[id, localeId]
|
|
12951
|
+
);
|
|
12952
|
+
if (existing) {
|
|
12953
|
+
const localeSets: string[] = [];
|
|
12954
|
+
const localeParams: unknown[] = [];
|
|
12955
|
+
if (name !== undefined) localeSets.push(`name = ${this.param(localeParams, name)}`);
|
|
12956
|
+
if (description !== undefined) localeSets.push(`description = ${this.param(localeParams, description)}`);
|
|
12957
|
+
if (localeSets.length > 0) {
|
|
12958
|
+
await this.prisma.$queryRawUnsafe(
|
|
12959
|
+
`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)}`,
|
|
12960
|
+
...localeParams
|
|
12961
|
+
);
|
|
12962
|
+
}
|
|
12963
|
+
} else if (name) {
|
|
12964
|
+
await this.prisma.$queryRawUnsafe(
|
|
12965
|
+
`INSERT INTO operations_project_cost_category_locale (operations_project_cost_category_id, locale_id, name, description) VALUES ($1, $2, $3, $4)`,
|
|
12966
|
+
id, localeId, name, description ?? null
|
|
12967
|
+
);
|
|
12968
|
+
}
|
|
12969
|
+
}
|
|
12970
|
+
}
|
|
12971
|
+
|
|
12972
|
+
return this.querySingle<{ id: number; slug: string }>(
|
|
12973
|
+
`SELECT id, slug FROM operations_project_cost_category WHERE id = $1`,
|
|
12974
|
+
[id]
|
|
12975
|
+
);
|
|
12976
|
+
}
|
|
12977
|
+
|
|
12978
|
+
async deleteProjectCostCategory(userId: number, id: number) {
|
|
12979
|
+
const actor = await this.getActorContext(userId);
|
|
12980
|
+
this.ensureDirector(actor);
|
|
12981
|
+
|
|
12982
|
+
const category = await this.querySingle<{ id: number }>(
|
|
12983
|
+
`SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
12984
|
+
[id]
|
|
12985
|
+
);
|
|
12986
|
+
if (!category) {
|
|
12987
|
+
throw new NotFoundException('Project cost category not found.');
|
|
12988
|
+
}
|
|
12989
|
+
|
|
12990
|
+
await this.prisma.$queryRawUnsafe(
|
|
12991
|
+
`UPDATE operations_project_cost_category SET deleted_at = NOW() WHERE id = $1`,
|
|
12992
|
+
id
|
|
12993
|
+
);
|
|
12994
|
+
|
|
12995
|
+
return { success: true };
|
|
12996
|
+
}
|
|
12997
|
+
|
|
12998
|
+
async getProjectCostCategory(userId: number, id: number) {
|
|
12999
|
+
await this.getActorContext(userId);
|
|
13000
|
+
const localeId = await this.resolvePreferredLocaleId();
|
|
13001
|
+
|
|
13002
|
+
const row = await this.querySingle<{
|
|
13003
|
+
id: number;
|
|
13004
|
+
slug: string;
|
|
13005
|
+
name: string | null;
|
|
13006
|
+
description: string | null;
|
|
13007
|
+
icon: string | null;
|
|
13008
|
+
color: string | null;
|
|
13009
|
+
isActive: boolean;
|
|
13010
|
+
sortOrder: number;
|
|
13011
|
+
createdAt: string;
|
|
13012
|
+
}>(
|
|
13013
|
+
`SELECT pcc.id,
|
|
13014
|
+
pcc.slug,
|
|
13015
|
+
COALESCE(pccl.name, pcc.slug) AS name,
|
|
13016
|
+
pccl.description,
|
|
13017
|
+
pcc.icon,
|
|
13018
|
+
pcc.color,
|
|
13019
|
+
pcc.is_active AS "isActive",
|
|
13020
|
+
pcc.sort_order AS "sortOrder",
|
|
13021
|
+
pcc.created_at AS "createdAt"
|
|
13022
|
+
FROM operations_project_cost_category pcc
|
|
13023
|
+
LEFT JOIN LATERAL (
|
|
13024
|
+
SELECT l.name, l.description
|
|
13025
|
+
FROM operations_project_cost_category_locale l
|
|
13026
|
+
WHERE l.operations_project_cost_category_id = pcc.id
|
|
13027
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
13028
|
+
l.id ASC
|
|
13029
|
+
LIMIT 1
|
|
13030
|
+
) pccl ON TRUE
|
|
13031
|
+
WHERE pcc.id = $2 AND pcc.deleted_at IS NULL`,
|
|
13032
|
+
[localeId, id]
|
|
13033
|
+
);
|
|
13034
|
+
|
|
13035
|
+
if (!row) {
|
|
13036
|
+
throw new NotFoundException('Project cost category not found.');
|
|
13037
|
+
}
|
|
13038
|
+
|
|
13039
|
+
return row;
|
|
13040
|
+
}
|
|
13041
|
+
|
|
13042
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
13043
|
+
// Project Cost Types
|
|
13044
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
13045
|
+
|
|
13046
|
+
async listProjectCostTypes(userId: number, filters: { search?: string; category_id?: number; is_active?: boolean; default_calculation_type?: string; pageSize?: number; page?: number } = {}) {
|
|
13047
|
+
await this.getActorContext(userId);
|
|
13048
|
+
const localeId = await this.resolvePreferredLocaleId();
|
|
13049
|
+
|
|
13050
|
+
const params: unknown[] = [localeId];
|
|
13051
|
+
const where: string[] = ['pct.deleted_at IS NULL'];
|
|
13052
|
+
|
|
13053
|
+
if (filters.is_active === true) {
|
|
13054
|
+
where.push('pct.is_active = true');
|
|
13055
|
+
}
|
|
13056
|
+
|
|
13057
|
+
if (filters.category_id) {
|
|
13058
|
+
where.push(`pct.category_id = ${this.param(params, filters.category_id)}`);
|
|
13059
|
+
}
|
|
13060
|
+
|
|
13061
|
+
if (filters.default_calculation_type) {
|
|
13062
|
+
where.push(`pct.default_calculation_type = ${this.param(params, filters.default_calculation_type)}`);
|
|
13063
|
+
}
|
|
13064
|
+
|
|
13065
|
+
if (filters.search?.trim()) {
|
|
13066
|
+
const p = this.param(params, `%${filters.search.trim()}%`);
|
|
13067
|
+
where.push(`(COALESCE(pctl.name, pct.slug) ILIKE ${p} OR COALESCE(pct.code, '') ILIKE ${p} OR COALESCE(pct.slug, '') ILIKE ${p})`);
|
|
13068
|
+
}
|
|
13069
|
+
|
|
13070
|
+
const whereClause = `WHERE ${where.join(' AND ')}`;
|
|
13071
|
+
|
|
13072
|
+
return this.queryRows<{
|
|
13073
|
+
id: number;
|
|
13074
|
+
slug: string;
|
|
13075
|
+
code: string;
|
|
13076
|
+
name: string | null;
|
|
13077
|
+
description: string | null;
|
|
13078
|
+
categoryId: number | null;
|
|
13079
|
+
categorySlug: string | null;
|
|
13080
|
+
categoryName: string | null;
|
|
13081
|
+
defaultUnit: string | null;
|
|
13082
|
+
defaultCalculationType: string | null;
|
|
13083
|
+
isRecurringAllowed: boolean;
|
|
13084
|
+
isActive: boolean;
|
|
13085
|
+
sortOrder: number;
|
|
13086
|
+
createdAt: string;
|
|
13087
|
+
}>(
|
|
13088
|
+
`SELECT pct.id,
|
|
13089
|
+
pct.slug,
|
|
13090
|
+
pct.code,
|
|
13091
|
+
COALESCE(pctl.name, pct.slug) AS name,
|
|
13092
|
+
pctl.description,
|
|
13093
|
+
pct.category_id AS "categoryId",
|
|
13094
|
+
pcc.slug AS "categorySlug",
|
|
13095
|
+
COALESCE(pccl.name, pcc.slug) AS "categoryName",
|
|
13096
|
+
pct.default_unit AS "defaultUnit",
|
|
13097
|
+
pct.default_calculation_type AS "defaultCalculationType",
|
|
13098
|
+
pct.is_recurring_allowed AS "isRecurringAllowed",
|
|
13099
|
+
pct.is_active AS "isActive",
|
|
13100
|
+
pct.sort_order AS "sortOrder",
|
|
13101
|
+
pct.created_at AS "createdAt"
|
|
13102
|
+
FROM operations_project_cost_type pct
|
|
13103
|
+
LEFT JOIN operations_project_cost_category pcc
|
|
13104
|
+
ON pcc.id = pct.category_id AND pcc.deleted_at IS NULL
|
|
13105
|
+
LEFT JOIN LATERAL (
|
|
13106
|
+
SELECT l.name, l.description
|
|
13107
|
+
FROM operations_project_cost_category_locale l
|
|
13108
|
+
WHERE l.operations_project_cost_category_id = pcc.id
|
|
13109
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
13110
|
+
l.id ASC
|
|
13111
|
+
LIMIT 1
|
|
13112
|
+
) pccl ON TRUE
|
|
13113
|
+
LEFT JOIN LATERAL (
|
|
13114
|
+
SELECT l.name, l.description
|
|
13115
|
+
FROM operations_project_cost_type_locale l
|
|
13116
|
+
WHERE l.operations_project_cost_type_id = pct.id
|
|
13117
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
13118
|
+
l.id ASC
|
|
13119
|
+
LIMIT 1
|
|
13120
|
+
) pctl ON TRUE
|
|
13121
|
+
${whereClause}
|
|
13122
|
+
ORDER BY pct.sort_order ASC, COALESCE(pctl.name, pct.slug) ASC`,
|
|
13123
|
+
params
|
|
13124
|
+
);
|
|
13125
|
+
}
|
|
13126
|
+
|
|
13127
|
+
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 }) {
|
|
13128
|
+
const actor = await this.getActorContext(userId);
|
|
13129
|
+
this.ensureDirector(actor);
|
|
13130
|
+
|
|
13131
|
+
const slug = data.slug?.trim();
|
|
13132
|
+
if (!slug) {
|
|
13133
|
+
throw new BadRequestException('Cost type slug is required.');
|
|
13134
|
+
}
|
|
13135
|
+
|
|
13136
|
+
if (data.category_id) {
|
|
13137
|
+
const category = await this.querySingle<{ id: number }>(
|
|
13138
|
+
`SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
13139
|
+
[data.category_id]
|
|
13140
|
+
);
|
|
13141
|
+
if (!category) {
|
|
13142
|
+
throw new BadRequestException(`Category with id ${data.category_id} not found.`);
|
|
13143
|
+
}
|
|
13144
|
+
}
|
|
13145
|
+
|
|
13146
|
+
const existingSlug = await this.querySingle<{ id: number }>(
|
|
13147
|
+
`SELECT id FROM operations_project_cost_type WHERE slug = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
13148
|
+
[slug]
|
|
13149
|
+
);
|
|
13150
|
+
if (existingSlug) {
|
|
13151
|
+
throw new ConflictException(`A cost type with slug '${slug}' already exists.`);
|
|
13152
|
+
}
|
|
13153
|
+
|
|
13154
|
+
const code = data.code?.trim() ?? slug;
|
|
13155
|
+
const existingCode = await this.querySingle<{ id: number }>(
|
|
13156
|
+
`SELECT id FROM operations_project_cost_type WHERE code = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
13157
|
+
[code]
|
|
13158
|
+
);
|
|
13159
|
+
if (existingCode) {
|
|
13160
|
+
throw new ConflictException(`A cost type with code '${code}' already exists.`);
|
|
13161
|
+
}
|
|
13162
|
+
|
|
13163
|
+
return this.prisma.$transaction(async (tx) => {
|
|
13164
|
+
const localeId = await this.resolvePreferredLocaleId(tx as any);
|
|
13165
|
+
|
|
13166
|
+
const created = (await (tx as any).$queryRawUnsafe(
|
|
13167
|
+
`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)
|
|
13168
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW())
|
|
13169
|
+
RETURNING id`,
|
|
13170
|
+
data.category_id ?? null,
|
|
13171
|
+
slug,
|
|
13172
|
+
data.code?.trim() ?? slug,
|
|
13173
|
+
data.default_unit ?? null,
|
|
13174
|
+
data.default_calculation_type ?? 'fixed',
|
|
13175
|
+
data.is_recurring_allowed ?? true,
|
|
13176
|
+
data.is_active ?? true,
|
|
13177
|
+
data.sort_order ?? 0,
|
|
13178
|
+
)) as { id: number }[];
|
|
13179
|
+
|
|
13180
|
+
const createdId = created[0]?.id;
|
|
13181
|
+
if (!createdId) {
|
|
13182
|
+
throw new BadRequestException('Unable to create project cost type.');
|
|
13183
|
+
}
|
|
13184
|
+
|
|
13185
|
+
const name = typeof data.name === 'string' ? data.name : (data.name ? JSON.stringify(data.name) : slug);
|
|
13186
|
+
const description = typeof data.description === 'string' ? data.description : (data.description ? JSON.stringify(data.description) : null);
|
|
13187
|
+
|
|
13188
|
+
if (localeId && name) {
|
|
13189
|
+
await (tx as any).$executeRawUnsafe(
|
|
13190
|
+
`INSERT INTO operations_project_cost_type_locale (operations_project_cost_type_id, locale_id, name, description)
|
|
13191
|
+
VALUES ($1, $2, $3, $4)`,
|
|
13192
|
+
createdId,
|
|
13193
|
+
localeId,
|
|
13194
|
+
name,
|
|
13195
|
+
description ?? null,
|
|
13196
|
+
);
|
|
13197
|
+
}
|
|
13198
|
+
|
|
13199
|
+
const rows = (await (tx as any).$queryRawUnsafe(
|
|
13200
|
+
`SELECT pct.id,
|
|
13201
|
+
pct.slug,
|
|
13202
|
+
pct.code,
|
|
13203
|
+
COALESCE(pctl.name, pct.slug) AS name,
|
|
13204
|
+
pctl.description,
|
|
13205
|
+
pct.category_id AS "categoryId",
|
|
13206
|
+
pct.default_unit AS "defaultUnit",
|
|
13207
|
+
pct.default_calculation_type AS "defaultCalculationType",
|
|
13208
|
+
pct.is_recurring_allowed AS "isRecurringAllowed",
|
|
13209
|
+
pct.is_active AS "isActive",
|
|
13210
|
+
pct.sort_order AS "sortOrder",
|
|
13211
|
+
pct.created_at AS "createdAt"
|
|
13212
|
+
FROM operations_project_cost_type pct
|
|
13213
|
+
LEFT JOIN operations_project_cost_type_locale pctl
|
|
13214
|
+
ON pctl.operations_project_cost_type_id = pct.id AND pctl.locale_id = $2
|
|
13215
|
+
WHERE pct.id = $1`,
|
|
13216
|
+
createdId,
|
|
13217
|
+
localeId,
|
|
13218
|
+
)) as any[];
|
|
13219
|
+
|
|
13220
|
+
return rows[0] ?? null;
|
|
13221
|
+
});
|
|
13222
|
+
}
|
|
13223
|
+
|
|
13224
|
+
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 }>) {
|
|
13225
|
+
const actor = await this.getActorContext(userId);
|
|
13226
|
+
this.ensureDirector(actor);
|
|
13227
|
+
|
|
13228
|
+
const costType = await this.querySingle<{ id: number }>(
|
|
13229
|
+
`SELECT id FROM operations_project_cost_type WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
13230
|
+
[id]
|
|
13231
|
+
);
|
|
13232
|
+
if (!costType) {
|
|
13233
|
+
throw new NotFoundException('Project cost type not found.');
|
|
13234
|
+
}
|
|
13235
|
+
|
|
13236
|
+
if (data.category_id !== undefined && data.category_id !== null) {
|
|
13237
|
+
const category = await this.querySingle<{ id: number }>(
|
|
13238
|
+
`SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
13239
|
+
[data.category_id]
|
|
13240
|
+
);
|
|
13241
|
+
if (!category) {
|
|
13242
|
+
throw new BadRequestException(`Category with id ${data.category_id} not found.`);
|
|
13243
|
+
}
|
|
13244
|
+
}
|
|
13245
|
+
|
|
13246
|
+
if (data.slug !== undefined) {
|
|
13247
|
+
const existingSlug = await this.querySingle<{ id: number }>(
|
|
13248
|
+
`SELECT id FROM operations_project_cost_type WHERE slug = $1 AND id != $2 AND deleted_at IS NULL LIMIT 1`,
|
|
13249
|
+
[data.slug, id]
|
|
13250
|
+
);
|
|
13251
|
+
if (existingSlug) {
|
|
13252
|
+
throw new ConflictException(`A cost type with slug '${data.slug}' already exists.`);
|
|
13253
|
+
}
|
|
13254
|
+
}
|
|
13255
|
+
|
|
13256
|
+
if (data.code !== undefined) {
|
|
13257
|
+
const existingCode = await this.querySingle<{ id: number }>(
|
|
13258
|
+
`SELECT id FROM operations_project_cost_type WHERE code = $1 AND id != $2 AND deleted_at IS NULL LIMIT 1`,
|
|
13259
|
+
[data.code, id]
|
|
13260
|
+
);
|
|
13261
|
+
if (existingCode) {
|
|
13262
|
+
throw new ConflictException(`A cost type with code '${data.code}' already exists.`);
|
|
13263
|
+
}
|
|
13264
|
+
}
|
|
13265
|
+
|
|
13266
|
+
const sets: string[] = [];
|
|
13267
|
+
const params: unknown[] = [];
|
|
13268
|
+
|
|
13269
|
+
if (data.category_id !== undefined) sets.push(`category_id = ${this.param(params, data.category_id)}`);
|
|
13270
|
+
if (data.slug !== undefined) sets.push(`slug = ${this.param(params, data.slug)}`);
|
|
13271
|
+
if (data.code !== undefined) sets.push(`code = ${this.param(params, data.code)}`);
|
|
13272
|
+
if (data.default_unit !== undefined) sets.push(`default_unit = ${this.param(params, data.default_unit)}`);
|
|
13273
|
+
if (data.default_calculation_type !== undefined) sets.push(`default_calculation_type = ${this.param(params, data.default_calculation_type)}`);
|
|
13274
|
+
if (data.is_recurring_allowed !== undefined) sets.push(`is_recurring_allowed = ${this.param(params, data.is_recurring_allowed)}`);
|
|
13275
|
+
if (data.is_active !== undefined) sets.push(`is_active = ${this.param(params, data.is_active)}`);
|
|
13276
|
+
if (data.sort_order !== undefined) sets.push(`sort_order = ${this.param(params, data.sort_order)}`);
|
|
13277
|
+
|
|
13278
|
+
if (sets.length > 0) {
|
|
13279
|
+
sets.push(`updated_at = NOW()`);
|
|
13280
|
+
await this.prisma.$queryRawUnsafe(
|
|
13281
|
+
`UPDATE operations_project_cost_type SET ${sets.join(', ')} WHERE id = ${this.param(params, id)}`,
|
|
13282
|
+
...params
|
|
13283
|
+
);
|
|
13284
|
+
}
|
|
13285
|
+
|
|
13286
|
+
if (data.name !== undefined || data.description !== undefined) {
|
|
13287
|
+
const localeId = await this.resolvePreferredLocaleId();
|
|
13288
|
+
if (localeId) {
|
|
13289
|
+
const name = typeof data.name === 'string' ? data.name : (data.name ? JSON.stringify(data.name) : undefined);
|
|
13290
|
+
const description = typeof data.description === 'string' ? data.description : (data.description ? JSON.stringify(data.description) : null);
|
|
13291
|
+
const existing = await this.querySingle<{ id: number }>(
|
|
13292
|
+
`SELECT id FROM operations_project_cost_type_locale WHERE operations_project_cost_type_id = $1 AND locale_id = $2 LIMIT 1`,
|
|
13293
|
+
[id, localeId]
|
|
13294
|
+
);
|
|
13295
|
+
if (existing) {
|
|
13296
|
+
const localeSets: string[] = [];
|
|
13297
|
+
const localeParams: unknown[] = [];
|
|
13298
|
+
if (name !== undefined) localeSets.push(`name = ${this.param(localeParams, name)}`);
|
|
13299
|
+
if (description !== undefined) localeSets.push(`description = ${this.param(localeParams, description)}`);
|
|
13300
|
+
if (localeSets.length > 0) {
|
|
13301
|
+
await this.prisma.$queryRawUnsafe(
|
|
13302
|
+
`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)}`,
|
|
13303
|
+
...localeParams
|
|
13304
|
+
);
|
|
13305
|
+
}
|
|
13306
|
+
} else if (name) {
|
|
13307
|
+
await this.prisma.$queryRawUnsafe(
|
|
13308
|
+
`INSERT INTO operations_project_cost_type_locale (operations_project_cost_type_id, locale_id, name, description) VALUES ($1, $2, $3, $4)`,
|
|
13309
|
+
id, localeId, name, description ?? null
|
|
13310
|
+
);
|
|
13311
|
+
}
|
|
13312
|
+
}
|
|
13313
|
+
}
|
|
13314
|
+
|
|
13315
|
+
return this.querySingle<{ id: number; slug: string }>(
|
|
13316
|
+
`SELECT id, slug FROM operations_project_cost_type WHERE id = $1`,
|
|
13317
|
+
[id]
|
|
13318
|
+
);
|
|
13319
|
+
}
|
|
13320
|
+
|
|
13321
|
+
async deleteProjectCostType(userId: number, id: number) {
|
|
13322
|
+
const actor = await this.getActorContext(userId);
|
|
13323
|
+
this.ensureDirector(actor);
|
|
13324
|
+
|
|
13325
|
+
const costType = await this.querySingle<{ id: number }>(
|
|
13326
|
+
`SELECT id FROM operations_project_cost_type WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
13327
|
+
[id]
|
|
13328
|
+
);
|
|
13329
|
+
if (!costType) {
|
|
13330
|
+
throw new NotFoundException('Project cost type not found.');
|
|
13331
|
+
}
|
|
13332
|
+
|
|
13333
|
+
await this.prisma.$queryRawUnsafe(
|
|
13334
|
+
`UPDATE operations_project_cost_type SET deleted_at = NOW() WHERE id = $1`,
|
|
13335
|
+
id
|
|
13336
|
+
);
|
|
13337
|
+
|
|
13338
|
+
return { success: true };
|
|
13339
|
+
}
|
|
13340
|
+
|
|
13341
|
+
async getProjectCostType(userId: number, id: number) {
|
|
13342
|
+
await this.getActorContext(userId);
|
|
13343
|
+
const localeId = await this.resolvePreferredLocaleId();
|
|
13344
|
+
|
|
13345
|
+
const row = await this.querySingle<{
|
|
13346
|
+
id: number;
|
|
13347
|
+
slug: string;
|
|
13348
|
+
code: string;
|
|
13349
|
+
name: string | null;
|
|
13350
|
+
description: string | null;
|
|
13351
|
+
default_unit: string | null;
|
|
13352
|
+
default_calculation_type: string | null;
|
|
13353
|
+
is_recurring_allowed: boolean;
|
|
13354
|
+
is_active: boolean;
|
|
13355
|
+
sort_order: number;
|
|
13356
|
+
category_id: number | null;
|
|
13357
|
+
category: { id: number; slug: string; name: string | null; color: string | null; icon: string | null } | null;
|
|
13358
|
+
}>(
|
|
13359
|
+
`SELECT pct.id,
|
|
13360
|
+
pct.slug,
|
|
13361
|
+
pct.code,
|
|
13362
|
+
COALESCE(pctl.name, pct.slug) AS name,
|
|
13363
|
+
pctl.description,
|
|
13364
|
+
pct.default_unit,
|
|
13365
|
+
pct.default_calculation_type,
|
|
13366
|
+
pct.is_recurring_allowed,
|
|
13367
|
+
pct.is_active,
|
|
13368
|
+
pct.sort_order,
|
|
13369
|
+
pct.category_id,
|
|
13370
|
+
CASE WHEN pcc.id IS NOT NULL THEN
|
|
13371
|
+
jsonb_build_object(
|
|
13372
|
+
'id', pcc.id,
|
|
13373
|
+
'slug', pcc.slug,
|
|
13374
|
+
'name', COALESCE(pccl.name, pcc.slug),
|
|
13375
|
+
'color', pcc.color,
|
|
13376
|
+
'icon', pcc.icon
|
|
13377
|
+
)
|
|
13378
|
+
ELSE NULL END AS category
|
|
13379
|
+
FROM operations_project_cost_type pct
|
|
13380
|
+
LEFT JOIN operations_project_cost_category pcc
|
|
13381
|
+
ON pcc.id = pct.category_id AND pcc.deleted_at IS NULL
|
|
13382
|
+
LEFT JOIN LATERAL (
|
|
13383
|
+
SELECT l.name, l.description
|
|
13384
|
+
FROM operations_project_cost_category_locale l
|
|
13385
|
+
WHERE l.operations_project_cost_category_id = pcc.id
|
|
13386
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
13387
|
+
l.id ASC
|
|
13388
|
+
LIMIT 1
|
|
13389
|
+
) pccl ON TRUE
|
|
13390
|
+
LEFT JOIN LATERAL (
|
|
13391
|
+
SELECT l.name, l.description
|
|
13392
|
+
FROM operations_project_cost_type_locale l
|
|
13393
|
+
WHERE l.operations_project_cost_type_id = pct.id
|
|
13394
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
13395
|
+
l.id ASC
|
|
13396
|
+
LIMIT 1
|
|
13397
|
+
) pctl ON TRUE
|
|
13398
|
+
WHERE pct.id = $2 AND pct.deleted_at IS NULL`,
|
|
13399
|
+
[localeId, id]
|
|
13400
|
+
);
|
|
13401
|
+
|
|
13402
|
+
if (!row) {
|
|
13403
|
+
throw new NotFoundException('Project cost type not found.');
|
|
13404
|
+
}
|
|
13405
|
+
|
|
13406
|
+
return row;
|
|
13407
|
+
}
|
|
13408
|
+
|
|
13409
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
13410
|
+
// Project Costs
|
|
13411
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
13412
|
+
|
|
13413
|
+
async listProjectCosts(userId: number, projectId: number, filters: {
|
|
13414
|
+
search?: string;
|
|
13415
|
+
cost_type_id?: number;
|
|
13416
|
+
category_id?: number;
|
|
13417
|
+
recurrence_type?: string;
|
|
13418
|
+
calculation_type?: string;
|
|
13419
|
+
status?: string;
|
|
13420
|
+
is_billable?: boolean;
|
|
13421
|
+
is_reimbursable?: boolean;
|
|
13422
|
+
date_from?: string;
|
|
13423
|
+
date_to?: string;
|
|
13424
|
+
} = {}) {
|
|
13425
|
+
await this.getActorContext(userId);
|
|
13426
|
+
const localeId = await this.resolvePreferredLocaleId();
|
|
13427
|
+
|
|
13428
|
+
const params: unknown[] = [localeId, projectId];
|
|
13429
|
+
const where: string[] = ['pc.deleted_at IS NULL', 'pc.project_id = $2'];
|
|
13430
|
+
|
|
13431
|
+
if (filters.cost_type_id) {
|
|
13432
|
+
where.push(`pc.cost_type_id = ${this.param(params, filters.cost_type_id)}`);
|
|
13433
|
+
}
|
|
13434
|
+
|
|
13435
|
+
if (filters.category_id) {
|
|
13436
|
+
where.push(`COALESCE(pc.category_id, pct.category_id) = ${this.param(params, filters.category_id)}`);
|
|
13437
|
+
}
|
|
13438
|
+
|
|
13439
|
+
if (filters.recurrence_type) {
|
|
13440
|
+
where.push(`pc.recurrence_type = ${this.param(params, filters.recurrence_type)}`);
|
|
13441
|
+
}
|
|
13442
|
+
|
|
13443
|
+
if (filters.calculation_type) {
|
|
13444
|
+
where.push(`pc.calculation_type = ${this.param(params, filters.calculation_type)}`);
|
|
13445
|
+
}
|
|
13446
|
+
|
|
13447
|
+
if (filters.status) {
|
|
13448
|
+
where.push(`pc.status = ${this.param(params, filters.status)}`);
|
|
13449
|
+
}
|
|
13450
|
+
|
|
13451
|
+
if (filters.is_billable !== undefined) {
|
|
13452
|
+
where.push(`pc.is_billable = ${this.param(params, filters.is_billable)}`);
|
|
13453
|
+
}
|
|
13454
|
+
|
|
13455
|
+
if (filters.is_reimbursable !== undefined) {
|
|
13456
|
+
where.push(`pc.is_reimbursable = ${this.param(params, filters.is_reimbursable)}`);
|
|
13457
|
+
}
|
|
13458
|
+
|
|
13459
|
+
if (filters.date_from) {
|
|
13460
|
+
where.push(`pc.cost_date >= ${this.param(params, filters.date_from)}::date`);
|
|
13461
|
+
}
|
|
13462
|
+
|
|
13463
|
+
if (filters.date_to) {
|
|
13464
|
+
where.push(`pc.cost_date <= ${this.param(params, filters.date_to)}::date`);
|
|
13465
|
+
}
|
|
13466
|
+
|
|
13467
|
+
if (filters.search?.trim()) {
|
|
13468
|
+
const p = this.param(params, `%${filters.search.trim()}%`);
|
|
13469
|
+
where.push(`(COALESCE(pc.description, '') ILIKE ${p} OR COALESCE(pc.notes, '') ILIKE ${p})`);
|
|
13470
|
+
}
|
|
13471
|
+
|
|
13472
|
+
const whereClause = `WHERE ${where.join(' AND ')}`;
|
|
13473
|
+
|
|
13474
|
+
const rows = await this.queryRows<{
|
|
13475
|
+
id: number;
|
|
13476
|
+
projectId: number;
|
|
13477
|
+
costTypeId: number | null;
|
|
13478
|
+
costTypeSlug: string | null;
|
|
13479
|
+
costTypeCode: string | null;
|
|
13480
|
+
costTypeName: string | null;
|
|
13481
|
+
categoryId: number | null;
|
|
13482
|
+
resolvedCategoryId: number | null;
|
|
13483
|
+
categorySlug: string | null;
|
|
13484
|
+
categoryName: string | null;
|
|
13485
|
+
categoryColor: string | null;
|
|
13486
|
+
categoryIcon: string | null;
|
|
13487
|
+
description: string | null;
|
|
13488
|
+
amount: string;
|
|
13489
|
+
quantity: string;
|
|
13490
|
+
unitAmount: string | null;
|
|
13491
|
+
currency: string;
|
|
13492
|
+
costDate: string | null;
|
|
13493
|
+
periodStart: string | null;
|
|
13494
|
+
periodEnd: string | null;
|
|
13495
|
+
calculationType: string;
|
|
13496
|
+
recurrenceType: string;
|
|
13497
|
+
isBillable: boolean;
|
|
13498
|
+
isReimbursable: boolean;
|
|
13499
|
+
notes: string | null;
|
|
13500
|
+
status: string;
|
|
13501
|
+
createdAt: string;
|
|
13502
|
+
}>(
|
|
13503
|
+
`SELECT pc.id,
|
|
13504
|
+
pc.project_id AS "projectId",
|
|
13505
|
+
pc.cost_type_id AS "costTypeId",
|
|
13506
|
+
pct.slug AS "costTypeSlug",
|
|
13507
|
+
pct.code AS "costTypeCode",
|
|
13508
|
+
COALESCE(pctl.name, pct.slug) AS "costTypeName",
|
|
13509
|
+
pc.category_id AS "categoryId",
|
|
13510
|
+
COALESCE(pc.category_id, pct.category_id) AS "resolvedCategoryId",
|
|
13511
|
+
pcc.slug AS "categorySlug",
|
|
13512
|
+
COALESCE(pccl.name, pcc.slug) AS "categoryName",
|
|
13513
|
+
pcc.color AS "categoryColor",
|
|
13514
|
+
pcc.icon AS "categoryIcon",
|
|
13515
|
+
pc.description,
|
|
13516
|
+
pc.amount::text AS amount,
|
|
13517
|
+
pc.quantity::text AS quantity,
|
|
13518
|
+
pc.unit_amount::text AS "unitAmount",
|
|
13519
|
+
pc.currency,
|
|
13520
|
+
TO_CHAR(pc.cost_date, 'YYYY-MM-DD') AS "costDate",
|
|
13521
|
+
TO_CHAR(pc.period_start, 'YYYY-MM-DD') AS "periodStart",
|
|
13522
|
+
TO_CHAR(pc.period_end, 'YYYY-MM-DD') AS "periodEnd",
|
|
13523
|
+
pc.calculation_type AS "calculationType",
|
|
13524
|
+
pc.recurrence_type AS "recurrenceType",
|
|
13525
|
+
pc.is_billable AS "isBillable",
|
|
13526
|
+
pc.is_reimbursable AS "isReimbursable",
|
|
13527
|
+
pc.notes,
|
|
13528
|
+
pc.status,
|
|
13529
|
+
pc.created_at AS "createdAt"
|
|
13530
|
+
FROM operations_project_cost pc
|
|
13531
|
+
LEFT JOIN operations_project_cost_type pct
|
|
13532
|
+
ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
|
|
13533
|
+
LEFT JOIN operations_project_cost_category pcc
|
|
13534
|
+
ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
|
|
13535
|
+
LEFT JOIN LATERAL (
|
|
13536
|
+
SELECT l.name
|
|
13537
|
+
FROM operations_project_cost_type_locale l
|
|
13538
|
+
WHERE l.operations_project_cost_type_id = pct.id
|
|
13539
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
13540
|
+
l.id ASC
|
|
13541
|
+
LIMIT 1
|
|
13542
|
+
) pctl ON TRUE
|
|
13543
|
+
LEFT JOIN LATERAL (
|
|
13544
|
+
SELECT l.name
|
|
13545
|
+
FROM operations_project_cost_category_locale l
|
|
13546
|
+
WHERE l.operations_project_cost_category_id = pcc.id
|
|
13547
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
13548
|
+
l.id ASC
|
|
13549
|
+
LIMIT 1
|
|
13550
|
+
) pccl ON TRUE
|
|
13551
|
+
${whereClause}
|
|
13552
|
+
ORDER BY pc.created_at DESC`,
|
|
13553
|
+
params
|
|
13554
|
+
);
|
|
13555
|
+
|
|
13556
|
+
return rows.map((row) => ({
|
|
13557
|
+
id: row.id,
|
|
13558
|
+
project_id: row.projectId,
|
|
13559
|
+
cost_type_id: row.costTypeId,
|
|
13560
|
+
category_id: row.categoryId,
|
|
13561
|
+
description: row.description,
|
|
13562
|
+
amount: row.amount,
|
|
13563
|
+
quantity: row.quantity,
|
|
13564
|
+
unit_amount: row.unitAmount,
|
|
13565
|
+
currency: row.currency,
|
|
13566
|
+
cost_date: row.costDate,
|
|
13567
|
+
period_start: row.periodStart,
|
|
13568
|
+
period_end: row.periodEnd,
|
|
13569
|
+
calculation_type: row.calculationType,
|
|
13570
|
+
recurrence_type: row.recurrenceType,
|
|
13571
|
+
is_billable: row.isBillable,
|
|
13572
|
+
is_reimbursable: row.isReimbursable,
|
|
13573
|
+
notes: row.notes,
|
|
13574
|
+
status: row.status,
|
|
13575
|
+
created_at: row.createdAt,
|
|
13576
|
+
cost_type: row.costTypeId
|
|
13577
|
+
? { id: row.costTypeId, slug: row.costTypeSlug, name: row.costTypeName, code: row.costTypeCode }
|
|
13578
|
+
: null,
|
|
13579
|
+
category: row.resolvedCategoryId
|
|
13580
|
+
? { id: row.resolvedCategoryId, slug: row.categorySlug, name: row.categoryName, color: row.categoryColor, icon: row.categoryIcon }
|
|
13581
|
+
: null,
|
|
13582
|
+
}));
|
|
13583
|
+
}
|
|
13584
|
+
|
|
13585
|
+
async getProjectCostsSummaryGrouped(userId: number, projectId: number) {
|
|
13586
|
+
const items = await this.listProjectCosts(userId, projectId, {});
|
|
13587
|
+
|
|
13588
|
+
// Group by resolved category
|
|
13589
|
+
const categoryMap = new Map<
|
|
13590
|
+
number | null,
|
|
13591
|
+
{
|
|
13592
|
+
category: { id: number; slug: string | null; name: string | null; color: string | null; icon: string | null } | null;
|
|
13593
|
+
items: typeof items;
|
|
13594
|
+
total_amount: number;
|
|
13595
|
+
}
|
|
13596
|
+
>();
|
|
13597
|
+
|
|
13598
|
+
for (const cost of items) {
|
|
13599
|
+
const cat = cost.category ?? null;
|
|
13600
|
+
const key = cat?.id ?? null;
|
|
13601
|
+
if (!categoryMap.has(key)) {
|
|
13602
|
+
categoryMap.set(key, { category: cat, items: [], total_amount: 0 });
|
|
13603
|
+
}
|
|
13604
|
+
const group = categoryMap.get(key)!;
|
|
13605
|
+
group.items.push(cost);
|
|
13606
|
+
group.total_amount += (parseFloat(String(cost.amount)) || 0) * (parseFloat(String(cost.quantity)) || 1);
|
|
13607
|
+
}
|
|
13608
|
+
|
|
13609
|
+
const grand_total = Array.from(categoryMap.values()).reduce(
|
|
13610
|
+
(sum, g) => sum + g.total_amount,
|
|
13611
|
+
0,
|
|
13612
|
+
);
|
|
13613
|
+
|
|
13614
|
+
return {
|
|
13615
|
+
categories: Array.from(categoryMap.values()).map((g) => ({
|
|
13616
|
+
category: g.category,
|
|
13617
|
+
items: g.items,
|
|
13618
|
+
total_amount: Math.round(g.total_amount * 100) / 100,
|
|
13619
|
+
count: g.items.length,
|
|
13620
|
+
})),
|
|
13621
|
+
grand_total: Math.round(grand_total * 100) / 100,
|
|
13622
|
+
};
|
|
13623
|
+
}
|
|
13624
|
+
|
|
13625
|
+
async getProjectCost(userId: number, projectId: number, id: number) {
|
|
13626
|
+
const rows = await this.listProjectCosts(userId, projectId, {});
|
|
13627
|
+
const cost = rows.find((r) => r.id === id);
|
|
13628
|
+
if (!cost) {
|
|
13629
|
+
throw new NotFoundException('Project cost not found.');
|
|
13630
|
+
}
|
|
13631
|
+
return cost;
|
|
13632
|
+
}
|
|
13633
|
+
|
|
13634
|
+
async getProjectCostsSummary(userId: number, projectId: number) {
|
|
13635
|
+
await this.getActorContext(userId);
|
|
13636
|
+
const localeId = await this.resolvePreferredLocaleId();
|
|
13637
|
+
|
|
13638
|
+
// ── 1. Verify project exists and fetch budget_amount ──────────────────
|
|
13639
|
+
const project = await this.querySingle<{ id: number; budgetAmount: string | null }>(
|
|
13640
|
+
`SELECT id, budget_amount::text AS "budgetAmount"
|
|
13641
|
+
FROM operations_project
|
|
13642
|
+
WHERE id = $1 AND deleted_at IS NULL
|
|
13643
|
+
LIMIT 1`,
|
|
13644
|
+
[projectId]
|
|
13645
|
+
);
|
|
13646
|
+
if (!project) {
|
|
13647
|
+
throw new NotFoundException('Project not found.');
|
|
13648
|
+
}
|
|
13649
|
+
|
|
13650
|
+
const budgetAmount = parseFloat(project.budgetAmount ?? '0') || 0;
|
|
13651
|
+
|
|
13652
|
+
// ── 2. Aggregated cost totals ─────────────────────────────────────────
|
|
13653
|
+
const totals = await this.querySingle<{
|
|
13654
|
+
extraCostTotal: string;
|
|
13655
|
+
plannedTotal: string;
|
|
13656
|
+
approvedTotal: string;
|
|
13657
|
+
realizedTotal: string;
|
|
13658
|
+
cancelledTotal: string;
|
|
13659
|
+
billableTotal: string;
|
|
13660
|
+
nonBillableTotal: string;
|
|
13661
|
+
reimbursableTotal: string;
|
|
13662
|
+
}>(
|
|
13663
|
+
`SELECT
|
|
13664
|
+
COALESCE(SUM(CASE WHEN status != 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "extraCostTotal",
|
|
13665
|
+
COALESCE(SUM(CASE WHEN status = 'planned' THEN amount * quantity ELSE 0 END), 0)::text AS "plannedTotal",
|
|
13666
|
+
COALESCE(SUM(CASE WHEN status = 'approved' THEN amount * quantity ELSE 0 END), 0)::text AS "approvedTotal",
|
|
13667
|
+
COALESCE(SUM(CASE WHEN status = 'realized' THEN amount * quantity ELSE 0 END), 0)::text AS "realizedTotal",
|
|
13668
|
+
COALESCE(SUM(CASE WHEN status = 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "cancelledTotal",
|
|
13669
|
+
COALESCE(SUM(CASE WHEN is_billable = true AND status != 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "billableTotal",
|
|
13670
|
+
COALESCE(SUM(CASE WHEN is_billable = false AND status != 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "nonBillableTotal",
|
|
13671
|
+
COALESCE(SUM(CASE WHEN is_reimbursable = true AND status != 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "reimbursableTotal"
|
|
13672
|
+
FROM operations_project_cost
|
|
13673
|
+
WHERE deleted_at IS NULL
|
|
13674
|
+
AND project_id = $1`,
|
|
13675
|
+
[projectId]
|
|
13676
|
+
);
|
|
13677
|
+
|
|
13678
|
+
const extraCostTotal = Math.round((parseFloat(totals?.extraCostTotal ?? '0') || 0) * 100) / 100;
|
|
13679
|
+
const plannedTotal = Math.round((parseFloat(totals?.plannedTotal ?? '0') || 0) * 100) / 100;
|
|
13680
|
+
const approvedTotal = Math.round((parseFloat(totals?.approvedTotal ?? '0') || 0) * 100) / 100;
|
|
13681
|
+
const realizedTotal = Math.round((parseFloat(totals?.realizedTotal ?? '0') || 0) * 100) / 100;
|
|
13682
|
+
const cancelledTotal = Math.round((parseFloat(totals?.cancelledTotal ?? '0') || 0) * 100) / 100;
|
|
13683
|
+
const billableTotal = Math.round((parseFloat(totals?.billableTotal ?? '0') || 0) * 100) / 100;
|
|
13684
|
+
const nonBillableTotal = Math.round((parseFloat(totals?.nonBillableTotal ?? '0') || 0) * 100) / 100;
|
|
13685
|
+
const reimbursableTotal = Math.round((parseFloat(totals?.reimbursableTotal ?? '0') || 0) * 100) / 100;
|
|
13686
|
+
|
|
13687
|
+
const teamCostTotal = 0;
|
|
13688
|
+
const totalProjectCost = Math.round((teamCostTotal + extraCostTotal) * 100) / 100;
|
|
13689
|
+
const remainingBudget = Math.round((budgetAmount - totalProjectCost) * 100) / 100;
|
|
13690
|
+
const budgetUsagePercent = budgetAmount > 0
|
|
13691
|
+
? Math.round((totalProjectCost / budgetAmount) * 10000) / 100
|
|
13692
|
+
: 0;
|
|
13693
|
+
|
|
13694
|
+
// ── 3. cost_by_category ───────────────────────────────────────────────
|
|
13695
|
+
const costByCategory = await this.queryRows<{
|
|
13696
|
+
categoryId: number | null;
|
|
13697
|
+
categorySlug: string | null;
|
|
13698
|
+
categoryName: string | null;
|
|
13699
|
+
categoryColor: string | null;
|
|
13700
|
+
categoryIcon: string | null;
|
|
13701
|
+
total: string;
|
|
13702
|
+
count: number;
|
|
13703
|
+
}>(
|
|
13704
|
+
`SELECT
|
|
13705
|
+
COALESCE(pc.category_id, pct.category_id) AS "categoryId",
|
|
13706
|
+
pcc.slug AS "categorySlug",
|
|
13707
|
+
COALESCE(pccl.name, pcc.slug) AS "categoryName",
|
|
13708
|
+
pcc.color AS "categoryColor",
|
|
13709
|
+
pcc.icon AS "categoryIcon",
|
|
13710
|
+
SUM(pc.amount * pc.quantity)::text AS total,
|
|
13711
|
+
COUNT(*)::int AS count
|
|
13712
|
+
FROM operations_project_cost pc
|
|
13713
|
+
LEFT JOIN operations_project_cost_type pct
|
|
13714
|
+
ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
|
|
13715
|
+
LEFT JOIN operations_project_cost_category pcc
|
|
13716
|
+
ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
|
|
13717
|
+
LEFT JOIN LATERAL (
|
|
13718
|
+
SELECT l.name
|
|
13719
|
+
FROM operations_project_cost_category_locale l
|
|
13720
|
+
WHERE l.operations_project_cost_category_id = pcc.id
|
|
13721
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
13722
|
+
l.id ASC
|
|
13723
|
+
LIMIT 1
|
|
13724
|
+
) pccl ON TRUE
|
|
13725
|
+
WHERE pc.deleted_at IS NULL
|
|
13726
|
+
AND pc.project_id = $2
|
|
13727
|
+
AND pc.status != 'cancelled'
|
|
13728
|
+
GROUP BY COALESCE(pc.category_id, pct.category_id), pcc.slug, pcc.color, pcc.icon, pccl.name
|
|
13729
|
+
ORDER BY SUM(pc.amount * pc.quantity) DESC`,
|
|
13730
|
+
[localeId, projectId]
|
|
13731
|
+
);
|
|
13732
|
+
|
|
13733
|
+
// ── 4. cost_by_type ───────────────────────────────────────────────────
|
|
13734
|
+
const costByType = await this.queryRows<{
|
|
13735
|
+
costTypeId: number | null;
|
|
13736
|
+
costTypeSlug: string | null;
|
|
13737
|
+
costTypeName: string | null;
|
|
13738
|
+
costTypeCode: string | null;
|
|
13739
|
+
total: string;
|
|
13740
|
+
count: number;
|
|
13741
|
+
}>(
|
|
13742
|
+
`SELECT
|
|
13743
|
+
pc.cost_type_id AS "costTypeId",
|
|
13744
|
+
pct.slug AS "costTypeSlug",
|
|
13745
|
+
COALESCE(pctl.name, pct.slug) AS "costTypeName",
|
|
13746
|
+
pct.code AS "costTypeCode",
|
|
13747
|
+
SUM(pc.amount * pc.quantity)::text AS total,
|
|
13748
|
+
COUNT(*)::int AS count
|
|
13749
|
+
FROM operations_project_cost pc
|
|
13750
|
+
LEFT JOIN operations_project_cost_type pct
|
|
13751
|
+
ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
|
|
13752
|
+
LEFT JOIN LATERAL (
|
|
13753
|
+
SELECT l.name
|
|
13754
|
+
FROM operations_project_cost_type_locale l
|
|
13755
|
+
WHERE l.operations_project_cost_type_id = pct.id
|
|
13756
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
|
|
13757
|
+
l.id ASC
|
|
13758
|
+
LIMIT 1
|
|
13759
|
+
) pctl ON TRUE
|
|
13760
|
+
WHERE pc.deleted_at IS NULL
|
|
13761
|
+
AND pc.project_id = $2
|
|
13762
|
+
AND pc.status != 'cancelled'
|
|
13763
|
+
GROUP BY pc.cost_type_id, pct.slug, pct.code, pctl.name
|
|
13764
|
+
ORDER BY SUM(pc.amount * pc.quantity) DESC`,
|
|
13765
|
+
[localeId, projectId]
|
|
13766
|
+
);
|
|
13767
|
+
|
|
13768
|
+
// ── 5. cost_by_month ──────────────────────────────────────────────────
|
|
13769
|
+
const costByMonth = await this.queryRows<{
|
|
13770
|
+
month: string;
|
|
13771
|
+
total: string;
|
|
13772
|
+
count: number;
|
|
13773
|
+
}>(
|
|
13774
|
+
`SELECT
|
|
13775
|
+
TO_CHAR(COALESCE(pc.cost_date, pc.created_at), 'YYYY-MM') AS month,
|
|
13776
|
+
SUM(pc.amount * pc.quantity)::text AS total,
|
|
13777
|
+
COUNT(*)::int AS count
|
|
13778
|
+
FROM operations_project_cost pc
|
|
13779
|
+
WHERE pc.deleted_at IS NULL
|
|
13780
|
+
AND pc.project_id = $1
|
|
13781
|
+
AND pc.status != 'cancelled'
|
|
13782
|
+
GROUP BY TO_CHAR(COALESCE(pc.cost_date, pc.created_at), 'YYYY-MM')
|
|
13783
|
+
ORDER BY month ASC`,
|
|
13784
|
+
[projectId]
|
|
13785
|
+
);
|
|
13786
|
+
|
|
13787
|
+
// ── 6. top_cost_types (top 5) ─────────────────────────────────────────
|
|
13788
|
+
const topCostTypes = costByType.slice(0, 5).map((ct) => {
|
|
13789
|
+
const typeTotal = Math.round((parseFloat(ct.total) || 0) * 100) / 100;
|
|
13790
|
+
const percentage = extraCostTotal > 0
|
|
13791
|
+
? Math.round((typeTotal / extraCostTotal) * 10000) / 100
|
|
13792
|
+
: 0;
|
|
13793
|
+
return {
|
|
13794
|
+
cost_type_id: ct.costTypeId,
|
|
13795
|
+
cost_type_slug: ct.costTypeSlug,
|
|
13796
|
+
cost_type_name: ct.costTypeName,
|
|
13797
|
+
cost_type_code: ct.costTypeCode,
|
|
13798
|
+
total: typeTotal,
|
|
13799
|
+
percentage,
|
|
13800
|
+
};
|
|
13801
|
+
});
|
|
13802
|
+
|
|
13803
|
+
return {
|
|
13804
|
+
project_id: projectId,
|
|
13805
|
+
budget_amount: budgetAmount,
|
|
13806
|
+
team_cost_total: teamCostTotal,
|
|
13807
|
+
extra_cost_total: extraCostTotal,
|
|
13808
|
+
total_project_cost: totalProjectCost,
|
|
13809
|
+
remaining_budget: remainingBudget,
|
|
13810
|
+
budget_usage_percent: budgetUsagePercent,
|
|
13811
|
+
planned_total: plannedTotal,
|
|
13812
|
+
approved_total: approvedTotal,
|
|
13813
|
+
realized_total: realizedTotal,
|
|
13814
|
+
cancelled_total: cancelledTotal,
|
|
13815
|
+
billable_total: billableTotal,
|
|
13816
|
+
non_billable_total: nonBillableTotal,
|
|
13817
|
+
reimbursable_total: reimbursableTotal,
|
|
13818
|
+
cost_by_category: costByCategory.map((c) => ({
|
|
13819
|
+
category_id: c.categoryId,
|
|
13820
|
+
category_slug: c.categorySlug,
|
|
13821
|
+
category_name: c.categoryName,
|
|
13822
|
+
category_color: c.categoryColor,
|
|
13823
|
+
category_icon: c.categoryIcon,
|
|
13824
|
+
total: Math.round((parseFloat(c.total) || 0) * 100) / 100,
|
|
13825
|
+
count: Number(c.count),
|
|
13826
|
+
})),
|
|
13827
|
+
cost_by_type: costByType.map((t) => ({
|
|
13828
|
+
cost_type_id: t.costTypeId,
|
|
13829
|
+
cost_type_slug: t.costTypeSlug,
|
|
13830
|
+
cost_type_name: t.costTypeName,
|
|
13831
|
+
cost_type_code: t.costTypeCode,
|
|
13832
|
+
total: Math.round((parseFloat(t.total) || 0) * 100) / 100,
|
|
13833
|
+
count: Number(t.count),
|
|
13834
|
+
})),
|
|
13835
|
+
cost_by_month: costByMonth.map((m) => ({
|
|
13836
|
+
month: m.month,
|
|
13837
|
+
total: Math.round((parseFloat(m.total) || 0) * 100) / 100,
|
|
13838
|
+
count: Number(m.count),
|
|
13839
|
+
})),
|
|
13840
|
+
top_cost_types: topCostTypes,
|
|
13841
|
+
};
|
|
13842
|
+
}
|
|
13843
|
+
|
|
13844
|
+
async getProjectCostReport(
|
|
13845
|
+
userId: number,
|
|
13846
|
+
projectId: number,
|
|
13847
|
+
filters: {
|
|
13848
|
+
date_from?: string;
|
|
13849
|
+
date_to?: string;
|
|
13850
|
+
category_id?: number;
|
|
13851
|
+
cost_type_id?: number;
|
|
13852
|
+
status?: string;
|
|
13853
|
+
is_billable?: boolean;
|
|
13854
|
+
is_reimbursable?: boolean;
|
|
13855
|
+
},
|
|
13856
|
+
) {
|
|
13857
|
+
await this.getActorContext(userId);
|
|
13858
|
+
const localeId = await this.resolvePreferredLocaleId();
|
|
13859
|
+
|
|
13860
|
+
// ── Verify project ───────────────────────────────────────────────────
|
|
13861
|
+
const project = await this.querySingle<{ id: number; budgetAmount: string | null }>(
|
|
13862
|
+
`SELECT id, budget_amount::text AS "budgetAmount"
|
|
13863
|
+
FROM operations_project
|
|
13864
|
+
WHERE id = $1 AND deleted_at IS NULL
|
|
13865
|
+
LIMIT 1`,
|
|
13866
|
+
[projectId],
|
|
13867
|
+
);
|
|
13868
|
+
if (!project) {
|
|
13869
|
+
throw new NotFoundException('Project not found.');
|
|
13870
|
+
}
|
|
13871
|
+
const budgetAmount = parseFloat(project.budgetAmount ?? '0') || 0;
|
|
13872
|
+
|
|
13873
|
+
// ── Build dynamic WHERE clause ────────────────────────────────────────
|
|
13874
|
+
const conditions: string[] = [
|
|
13875
|
+
'pc.deleted_at IS NULL',
|
|
13876
|
+
'pc.project_id = $1',
|
|
13877
|
+
];
|
|
13878
|
+
const params: unknown[] = [projectId];
|
|
13879
|
+
|
|
13880
|
+
if (filters.date_from) {
|
|
13881
|
+
params.push(filters.date_from);
|
|
13882
|
+
conditions.push(`COALESCE(pc.cost_date, pc.created_at::date) >= $${params.length}::date`);
|
|
13883
|
+
}
|
|
13884
|
+
if (filters.date_to) {
|
|
13885
|
+
params.push(filters.date_to);
|
|
13886
|
+
conditions.push(`COALESCE(pc.cost_date, pc.created_at::date) <= $${params.length}::date`);
|
|
13887
|
+
}
|
|
13888
|
+
if (filters.category_id !== undefined) {
|
|
13889
|
+
params.push(filters.category_id);
|
|
13890
|
+
conditions.push(
|
|
13891
|
+
`(pc.category_id = $${params.length} OR (pc.category_id IS NULL AND EXISTS (
|
|
13892
|
+
SELECT 1 FROM operations_project_cost_type pct2
|
|
13893
|
+
WHERE pct2.id = pc.cost_type_id AND pct2.category_id = $${params.length} AND pct2.deleted_at IS NULL
|
|
13894
|
+
)))`,
|
|
13895
|
+
);
|
|
13896
|
+
}
|
|
13897
|
+
if (filters.cost_type_id !== undefined) {
|
|
13898
|
+
params.push(filters.cost_type_id);
|
|
13899
|
+
conditions.push(`pc.cost_type_id = $${params.length}`);
|
|
13900
|
+
}
|
|
13901
|
+
if (filters.status !== undefined) {
|
|
13902
|
+
params.push(filters.status);
|
|
13903
|
+
conditions.push(`pc.status = $${params.length}`);
|
|
13904
|
+
}
|
|
13905
|
+
if (filters.is_billable !== undefined) {
|
|
13906
|
+
params.push(filters.is_billable);
|
|
13907
|
+
conditions.push(`pc.is_billable = $${params.length}`);
|
|
13908
|
+
}
|
|
13909
|
+
if (filters.is_reimbursable !== undefined) {
|
|
13910
|
+
params.push(filters.is_reimbursable);
|
|
13911
|
+
conditions.push(`pc.is_reimbursable = $${params.length}`);
|
|
13912
|
+
}
|
|
13913
|
+
|
|
13914
|
+
const whereClause = conditions.join(' AND ');
|
|
13915
|
+
|
|
13916
|
+
// ── Totals ────────────────────────────────────────────────────────────
|
|
13917
|
+
const totals = await this.querySingle<{
|
|
13918
|
+
grandTotal: string;
|
|
13919
|
+
plannedTotal: string;
|
|
13920
|
+
approvedTotal: string;
|
|
13921
|
+
realizedTotal: string;
|
|
13922
|
+
cancelledTotal: string;
|
|
13923
|
+
billableTotal: string;
|
|
13924
|
+
nonBillableTotal: string;
|
|
13925
|
+
reimbursableTotal: string;
|
|
13926
|
+
totalCount: number;
|
|
13927
|
+
}>(
|
|
13928
|
+
`SELECT
|
|
13929
|
+
COALESCE(SUM(pc.amount * pc.quantity), 0)::text AS "grandTotal",
|
|
13930
|
+
COALESCE(SUM(CASE WHEN pc.status = 'planned' THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "plannedTotal",
|
|
13931
|
+
COALESCE(SUM(CASE WHEN pc.status = 'approved' THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "approvedTotal",
|
|
13932
|
+
COALESCE(SUM(CASE WHEN pc.status = 'realized' THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "realizedTotal",
|
|
13933
|
+
COALESCE(SUM(CASE WHEN pc.status = 'cancelled' THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "cancelledTotal",
|
|
13934
|
+
COALESCE(SUM(CASE WHEN pc.is_billable = true THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "billableTotal",
|
|
13935
|
+
COALESCE(SUM(CASE WHEN pc.is_billable = false THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "nonBillableTotal",
|
|
13936
|
+
COALESCE(SUM(CASE WHEN pc.is_reimbursable = true THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "reimbursableTotal",
|
|
13937
|
+
COUNT(*)::int AS "totalCount"
|
|
13938
|
+
FROM operations_project_cost pc
|
|
13939
|
+
WHERE ${whereClause}`,
|
|
13940
|
+
params,
|
|
13941
|
+
);
|
|
13942
|
+
|
|
13943
|
+
const round2 = (v: string | null | undefined) =>
|
|
13944
|
+
Math.round((parseFloat(v ?? '0') || 0) * 100) / 100;
|
|
13945
|
+
|
|
13946
|
+
const grandTotal = round2(totals?.grandTotal);
|
|
13947
|
+
const plannedTotal = round2(totals?.plannedTotal);
|
|
13948
|
+
const approvedTotal = round2(totals?.approvedTotal);
|
|
13949
|
+
const realizedTotal = round2(totals?.realizedTotal);
|
|
13950
|
+
const cancelledTotal = round2(totals?.cancelledTotal);
|
|
13951
|
+
const billableTotal = round2(totals?.billableTotal);
|
|
13952
|
+
const nonBillableTotal = round2(totals?.nonBillableTotal);
|
|
13953
|
+
const reimbursableTotal= round2(totals?.reimbursableTotal);
|
|
13954
|
+
|
|
13955
|
+
// ── By category ───────────────────────────────────────────────────────
|
|
13956
|
+
const costByCategory = await this.queryRows<{
|
|
13957
|
+
categoryId: number | null;
|
|
13958
|
+
categorySlug: string | null;
|
|
13959
|
+
categoryName: string | null;
|
|
13960
|
+
categoryColor: string | null;
|
|
13961
|
+
categoryIcon: string | null;
|
|
13962
|
+
total: string;
|
|
13963
|
+
count: number;
|
|
13964
|
+
plannedSubtotal: string;
|
|
13965
|
+
realizedSubtotal: string;
|
|
13966
|
+
}>(
|
|
13967
|
+
`SELECT
|
|
13968
|
+
COALESCE(pc.category_id, pct.category_id) AS "categoryId",
|
|
13969
|
+
pcc.slug AS "categorySlug",
|
|
13970
|
+
COALESCE(pccl.name, pcc.slug) AS "categoryName",
|
|
13971
|
+
pcc.color AS "categoryColor",
|
|
13972
|
+
pcc.icon AS "categoryIcon",
|
|
13973
|
+
SUM(pc.amount * pc.quantity)::text AS total,
|
|
13974
|
+
COUNT(*)::int AS count,
|
|
13975
|
+
COALESCE(SUM(CASE WHEN pc.status='planned' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "plannedSubtotal",
|
|
13976
|
+
COALESCE(SUM(CASE WHEN pc.status='realized' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "realizedSubtotal"
|
|
13977
|
+
FROM operations_project_cost pc
|
|
13978
|
+
LEFT JOIN operations_project_cost_type pct
|
|
13979
|
+
ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
|
|
13980
|
+
LEFT JOIN operations_project_cost_category pcc
|
|
13981
|
+
ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
|
|
13982
|
+
LEFT JOIN LATERAL (
|
|
13983
|
+
SELECT l.name
|
|
13984
|
+
FROM operations_project_cost_category_locale l
|
|
13985
|
+
WHERE l.operations_project_cost_category_id = pcc.id
|
|
13986
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
|
|
13987
|
+
LIMIT 1
|
|
13988
|
+
) pccl ON TRUE
|
|
13989
|
+
WHERE ${whereClause.replace(/\$(\d+)/g, (m, n) => '$' + (Number(n) + 1))}
|
|
13990
|
+
GROUP BY COALESCE(pc.category_id, pct.category_id), pcc.slug, pcc.color, pcc.icon, pccl.name
|
|
13991
|
+
ORDER BY SUM(pc.amount * pc.quantity) DESC`,
|
|
13992
|
+
[localeId, ...params],
|
|
13993
|
+
);
|
|
13994
|
+
|
|
13995
|
+
// ── By type ───────────────────────────────────────────────────────────
|
|
13996
|
+
const costByType = await this.queryRows<{
|
|
13997
|
+
costTypeId: number | null;
|
|
13998
|
+
costTypeSlug: string | null;
|
|
13999
|
+
costTypeName: string | null;
|
|
14000
|
+
costTypeCode: string | null;
|
|
14001
|
+
total: string;
|
|
14002
|
+
count: number;
|
|
14003
|
+
plannedSubtotal: string;
|
|
14004
|
+
realizedSubtotal: string;
|
|
14005
|
+
}>(
|
|
14006
|
+
`SELECT
|
|
14007
|
+
pc.cost_type_id AS "costTypeId",
|
|
14008
|
+
pct.slug AS "costTypeSlug",
|
|
14009
|
+
COALESCE(pctl.name, pct.slug) AS "costTypeName",
|
|
14010
|
+
pct.code AS "costTypeCode",
|
|
14011
|
+
SUM(pc.amount * pc.quantity)::text AS total,
|
|
14012
|
+
COUNT(*)::int AS count,
|
|
14013
|
+
COALESCE(SUM(CASE WHEN pc.status='planned' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "plannedSubtotal",
|
|
14014
|
+
COALESCE(SUM(CASE WHEN pc.status='realized' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "realizedSubtotal"
|
|
14015
|
+
FROM operations_project_cost pc
|
|
14016
|
+
LEFT JOIN operations_project_cost_type pct
|
|
14017
|
+
ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
|
|
14018
|
+
LEFT JOIN LATERAL (
|
|
14019
|
+
SELECT l.name
|
|
14020
|
+
FROM operations_project_cost_type_locale l
|
|
14021
|
+
WHERE l.operations_project_cost_type_id = pct.id
|
|
14022
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
|
|
14023
|
+
LIMIT 1
|
|
14024
|
+
) pctl ON TRUE
|
|
14025
|
+
WHERE ${whereClause.replace(/\$(\d+)/g, (m, n) => '$' + (Number(n) + 1))}
|
|
14026
|
+
GROUP BY pc.cost_type_id, pct.slug, pct.code, pctl.name
|
|
14027
|
+
ORDER BY SUM(pc.amount * pc.quantity) DESC`,
|
|
14028
|
+
[localeId, ...params],
|
|
14029
|
+
);
|
|
14030
|
+
|
|
14031
|
+
// ── By month ──────────────────────────────────────────────────────────
|
|
14032
|
+
const costByMonth = await this.queryRows<{
|
|
14033
|
+
month: string;
|
|
14034
|
+
total: string;
|
|
14035
|
+
plannedSubtotal: string;
|
|
14036
|
+
realizedSubtotal: string;
|
|
14037
|
+
count: number;
|
|
14038
|
+
}>(
|
|
14039
|
+
`SELECT
|
|
14040
|
+
TO_CHAR(COALESCE(pc.cost_date, pc.created_at::date), 'YYYY-MM') AS month,
|
|
14041
|
+
SUM(pc.amount * pc.quantity)::text AS total,
|
|
14042
|
+
COALESCE(SUM(CASE WHEN pc.status='planned' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "plannedSubtotal",
|
|
14043
|
+
COALESCE(SUM(CASE WHEN pc.status='realized' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "realizedSubtotal",
|
|
14044
|
+
COUNT(*)::int AS count
|
|
14045
|
+
FROM operations_project_cost pc
|
|
14046
|
+
WHERE ${whereClause}
|
|
14047
|
+
GROUP BY TO_CHAR(COALESCE(pc.cost_date, pc.created_at::date), 'YYYY-MM')
|
|
14048
|
+
ORDER BY month ASC`,
|
|
14049
|
+
params,
|
|
14050
|
+
);
|
|
14051
|
+
|
|
14052
|
+
// ── Top 5 individual costs ────────────────────────────────────────────
|
|
14053
|
+
const top5Costs = await this.queryRows<{
|
|
14054
|
+
id: number;
|
|
14055
|
+
description: string | null;
|
|
14056
|
+
amount: string;
|
|
14057
|
+
quantity: string;
|
|
14058
|
+
status: string;
|
|
14059
|
+
costTypeName: string | null;
|
|
14060
|
+
categoryName: string | null;
|
|
14061
|
+
categoryColor: string | null;
|
|
14062
|
+
costDate: string | null;
|
|
14063
|
+
}>(
|
|
14064
|
+
`SELECT
|
|
14065
|
+
pc.id,
|
|
14066
|
+
pc.description,
|
|
14067
|
+
pc.amount::text,
|
|
14068
|
+
pc.quantity::text,
|
|
14069
|
+
pc.status,
|
|
14070
|
+
pc.cost_date AS "costDate",
|
|
14071
|
+
COALESCE(pctl.name, pct.slug) AS "costTypeName",
|
|
14072
|
+
COALESCE(pccl.name, pcc.slug) AS "categoryName",
|
|
14073
|
+
pcc.color AS "categoryColor"
|
|
14074
|
+
FROM operations_project_cost pc
|
|
14075
|
+
LEFT JOIN operations_project_cost_type pct
|
|
14076
|
+
ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
|
|
14077
|
+
LEFT JOIN LATERAL (
|
|
14078
|
+
SELECT l.name
|
|
14079
|
+
FROM operations_project_cost_type_locale l
|
|
14080
|
+
WHERE l.operations_project_cost_type_id = pct.id
|
|
14081
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
|
|
14082
|
+
LIMIT 1
|
|
14083
|
+
) pctl ON TRUE
|
|
14084
|
+
LEFT JOIN operations_project_cost_category pcc
|
|
14085
|
+
ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
|
|
14086
|
+
LEFT JOIN LATERAL (
|
|
14087
|
+
SELECT l.name
|
|
14088
|
+
FROM operations_project_cost_category_locale l
|
|
14089
|
+
WHERE l.operations_project_cost_category_id = pcc.id
|
|
14090
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
|
|
14091
|
+
LIMIT 1
|
|
14092
|
+
) pccl ON TRUE
|
|
14093
|
+
WHERE ${whereClause.replace(/\$(\d+)/g, (m, n) => '$' + (Number(n) + 1))}
|
|
14094
|
+
ORDER BY (pc.amount * pc.quantity) DESC
|
|
14095
|
+
LIMIT 5`,
|
|
14096
|
+
[localeId, ...params],
|
|
14097
|
+
);
|
|
14098
|
+
|
|
14099
|
+
// ── Detailed list ─────────────────────────────────────────────────────
|
|
14100
|
+
const detailedList = await this.queryRows<{
|
|
14101
|
+
id: number;
|
|
14102
|
+
description: string | null;
|
|
14103
|
+
amount: string;
|
|
14104
|
+
quantity: string;
|
|
14105
|
+
unitAmount: string | null;
|
|
14106
|
+
currency: string | null;
|
|
14107
|
+
calculationType: string | null;
|
|
14108
|
+
recurrenceType: string | null;
|
|
14109
|
+
status: string;
|
|
14110
|
+
isBillable: boolean;
|
|
14111
|
+
isReimbursable: boolean;
|
|
14112
|
+
costDate: string | null;
|
|
14113
|
+
periodStart: string | null;
|
|
14114
|
+
periodEnd: string | null;
|
|
14115
|
+
notes: string | null;
|
|
14116
|
+
costTypeId: number | null;
|
|
14117
|
+
costTypeName: string | null;
|
|
14118
|
+
costTypeCode: string | null;
|
|
14119
|
+
categoryId: number | null;
|
|
14120
|
+
categoryName: string | null;
|
|
14121
|
+
categoryColor: string | null;
|
|
14122
|
+
createdAt: string;
|
|
14123
|
+
}>(
|
|
14124
|
+
`SELECT
|
|
14125
|
+
pc.id,
|
|
14126
|
+
pc.description,
|
|
14127
|
+
pc.amount::text,
|
|
14128
|
+
pc.quantity::text,
|
|
14129
|
+
pc.unit_amount::text AS "unitAmount",
|
|
14130
|
+
pc.currency,
|
|
14131
|
+
pc.calculation_type AS "calculationType",
|
|
14132
|
+
pc.recurrence_type AS "recurrenceType",
|
|
14133
|
+
pc.status,
|
|
14134
|
+
pc.is_billable AS "isBillable",
|
|
14135
|
+
pc.is_reimbursable AS "isReimbursable",
|
|
14136
|
+
pc.cost_date AS "costDate",
|
|
14137
|
+
pc.period_start AS "periodStart",
|
|
14138
|
+
pc.period_end AS "periodEnd",
|
|
14139
|
+
pc.notes,
|
|
14140
|
+
pc.cost_type_id AS "costTypeId",
|
|
14141
|
+
COALESCE(pctl.name, pct.slug) AS "costTypeName",
|
|
14142
|
+
pct.code AS "costTypeCode",
|
|
14143
|
+
COALESCE(pc.category_id, pct.category_id) AS "categoryId",
|
|
14144
|
+
COALESCE(pccl.name, pcc.slug) AS "categoryName",
|
|
14145
|
+
pcc.color AS "categoryColor",
|
|
14146
|
+
pc.created_at::text AS "createdAt"
|
|
14147
|
+
FROM operations_project_cost pc
|
|
14148
|
+
LEFT JOIN operations_project_cost_type pct
|
|
14149
|
+
ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
|
|
14150
|
+
LEFT JOIN LATERAL (
|
|
14151
|
+
SELECT l.name
|
|
14152
|
+
FROM operations_project_cost_type_locale l
|
|
14153
|
+
WHERE l.operations_project_cost_type_id = pct.id
|
|
14154
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
|
|
14155
|
+
LIMIT 1
|
|
14156
|
+
) pctl ON TRUE
|
|
14157
|
+
LEFT JOIN operations_project_cost_category pcc
|
|
14158
|
+
ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
|
|
14159
|
+
LEFT JOIN LATERAL (
|
|
14160
|
+
SELECT l.name
|
|
14161
|
+
FROM operations_project_cost_category_locale l
|
|
14162
|
+
WHERE l.operations_project_cost_category_id = pcc.id
|
|
14163
|
+
ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
|
|
14164
|
+
LIMIT 1
|
|
14165
|
+
) pccl ON TRUE
|
|
14166
|
+
WHERE ${whereClause.replace(/\$(\d+)/g, (m, n) => '$' + (Number(n) + 1))}
|
|
14167
|
+
ORDER BY (pc.amount * pc.quantity) DESC, pc.cost_date DESC NULLS LAST`,
|
|
14168
|
+
[localeId, ...params],
|
|
14169
|
+
);
|
|
14170
|
+
|
|
14171
|
+
return {
|
|
14172
|
+
project_id: projectId,
|
|
14173
|
+
budget_amount: budgetAmount,
|
|
14174
|
+
filters_applied: {
|
|
14175
|
+
date_from: filters.date_from ?? null,
|
|
14176
|
+
date_to: filters.date_to ?? null,
|
|
14177
|
+
category_id: filters.category_id ?? null,
|
|
14178
|
+
cost_type_id: filters.cost_type_id ?? null,
|
|
14179
|
+
status: filters.status ?? null,
|
|
14180
|
+
is_billable: filters.is_billable ?? null,
|
|
14181
|
+
is_reimbursable:filters.is_reimbursable ?? null,
|
|
14182
|
+
},
|
|
14183
|
+
totals: {
|
|
14184
|
+
grand_total: grandTotal,
|
|
14185
|
+
planned_total: plannedTotal,
|
|
14186
|
+
approved_total: approvedTotal,
|
|
14187
|
+
realized_total: realizedTotal,
|
|
14188
|
+
cancelled_total: cancelledTotal,
|
|
14189
|
+
billable_total: billableTotal,
|
|
14190
|
+
non_billable_total: nonBillableTotal,
|
|
14191
|
+
reimbursable_total: reimbursableTotal,
|
|
14192
|
+
total_count: Number(totals?.totalCount ?? 0),
|
|
14193
|
+
},
|
|
14194
|
+
cost_by_category: costByCategory.map((c) => ({
|
|
14195
|
+
category_id: c.categoryId,
|
|
14196
|
+
category_slug: c.categorySlug,
|
|
14197
|
+
category_name: c.categoryName,
|
|
14198
|
+
category_color: c.categoryColor,
|
|
14199
|
+
category_icon: c.categoryIcon,
|
|
14200
|
+
total: round2(c.total),
|
|
14201
|
+
count: Number(c.count),
|
|
14202
|
+
planned_subtotal: round2(c.plannedSubtotal),
|
|
14203
|
+
realized_subtotal: round2(c.realizedSubtotal),
|
|
14204
|
+
})),
|
|
14205
|
+
cost_by_type: costByType.map((t) => ({
|
|
14206
|
+
cost_type_id: t.costTypeId,
|
|
14207
|
+
cost_type_slug: t.costTypeSlug,
|
|
14208
|
+
cost_type_name: t.costTypeName,
|
|
14209
|
+
cost_type_code: t.costTypeCode,
|
|
14210
|
+
total: round2(t.total),
|
|
14211
|
+
count: Number(t.count),
|
|
14212
|
+
planned_subtotal: round2(t.plannedSubtotal),
|
|
14213
|
+
realized_subtotal: round2(t.realizedSubtotal),
|
|
14214
|
+
})),
|
|
14215
|
+
cost_by_month: costByMonth.map((m) => ({
|
|
14216
|
+
month: m.month,
|
|
14217
|
+
total: round2(m.total),
|
|
14218
|
+
planned_subtotal: round2(m.plannedSubtotal),
|
|
14219
|
+
realized_subtotal: round2(m.realizedSubtotal),
|
|
14220
|
+
count: Number(m.count),
|
|
14221
|
+
})),
|
|
14222
|
+
top_5_costs: top5Costs.map((c) => ({
|
|
14223
|
+
id: c.id,
|
|
14224
|
+
description: c.description,
|
|
14225
|
+
total: round2(String(parseFloat(c.amount) * parseFloat(c.quantity))),
|
|
14226
|
+
amount: round2(c.amount),
|
|
14227
|
+
quantity: parseFloat(c.quantity),
|
|
14228
|
+
status: c.status,
|
|
14229
|
+
cost_type_name: c.costTypeName,
|
|
14230
|
+
category_name: c.categoryName,
|
|
14231
|
+
category_color: c.categoryColor,
|
|
14232
|
+
cost_date: c.costDate,
|
|
14233
|
+
})),
|
|
14234
|
+
detailed_list: detailedList.map((c) => ({
|
|
14235
|
+
id: c.id,
|
|
14236
|
+
description: c.description,
|
|
14237
|
+
amount: round2(c.amount),
|
|
14238
|
+
quantity: parseFloat(c.quantity),
|
|
14239
|
+
unit_amount: c.unitAmount ? round2(c.unitAmount) : null,
|
|
14240
|
+
total: round2(String(parseFloat(c.amount) * parseFloat(c.quantity))),
|
|
14241
|
+
currency: c.currency,
|
|
14242
|
+
calculation_type: c.calculationType,
|
|
14243
|
+
recurrence_type: c.recurrenceType,
|
|
14244
|
+
status: c.status,
|
|
14245
|
+
is_billable: c.isBillable,
|
|
14246
|
+
is_reimbursable: c.isReimbursable,
|
|
14247
|
+
cost_date: c.costDate,
|
|
14248
|
+
period_start: c.periodStart,
|
|
14249
|
+
period_end: c.periodEnd,
|
|
14250
|
+
notes: c.notes,
|
|
14251
|
+
cost_type_id: c.costTypeId,
|
|
14252
|
+
cost_type_name: c.costTypeName,
|
|
14253
|
+
cost_type_code: c.costTypeCode,
|
|
14254
|
+
category_id: c.categoryId,
|
|
14255
|
+
category_name: c.categoryName,
|
|
14256
|
+
category_color: c.categoryColor,
|
|
14257
|
+
created_at: c.createdAt,
|
|
14258
|
+
})),
|
|
14259
|
+
};
|
|
14260
|
+
}
|
|
14261
|
+
|
|
14262
|
+
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 }) {
|
|
14263
|
+
const actor = await this.getActorContext(userId);
|
|
14264
|
+
this.ensureSupervisor(actor);
|
|
14265
|
+
|
|
14266
|
+
const project = await this.querySingle<{ id: number }>(
|
|
14267
|
+
`SELECT id FROM operations_project WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
14268
|
+
[projectId]
|
|
14269
|
+
);
|
|
14270
|
+
if (!project) {
|
|
14271
|
+
throw new NotFoundException('Project not found.');
|
|
14272
|
+
}
|
|
14273
|
+
|
|
14274
|
+
if (data.cost_type_id) {
|
|
14275
|
+
const costType = await this.querySingle<{ id: number }>(
|
|
14276
|
+
`SELECT id FROM operations_project_cost_type WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
14277
|
+
[data.cost_type_id]
|
|
14278
|
+
);
|
|
14279
|
+
if (!costType) {
|
|
14280
|
+
throw new BadRequestException(`Cost type with id ${data.cost_type_id} not found.`);
|
|
14281
|
+
}
|
|
14282
|
+
}
|
|
14283
|
+
|
|
14284
|
+
if (data.category_id) {
|
|
14285
|
+
const category = await this.querySingle<{ id: number }>(
|
|
14286
|
+
`SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
14287
|
+
[data.category_id]
|
|
14288
|
+
);
|
|
14289
|
+
if (!category) {
|
|
14290
|
+
throw new BadRequestException(`Cost category with id ${data.category_id} not found.`);
|
|
14291
|
+
}
|
|
14292
|
+
}
|
|
14293
|
+
|
|
14294
|
+
const calcType = data.calculation_type ?? 'fixed';
|
|
14295
|
+
let effectiveAmount = data.amount;
|
|
14296
|
+
if (['unit', 'hourly', 'monthly'].includes(calcType) && data.unit_amount !== undefined && data.unit_amount !== null) {
|
|
14297
|
+
const qty = data.quantity ?? 1;
|
|
14298
|
+
effectiveAmount = Math.round(qty * data.unit_amount * 100) / 100;
|
|
14299
|
+
}
|
|
14300
|
+
|
|
14301
|
+
const created = await this.querySingle<{ id: number }>(
|
|
14302
|
+
`INSERT INTO operations_project_cost
|
|
14303
|
+
(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)
|
|
14304
|
+
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())
|
|
14305
|
+
RETURNING id`,
|
|
14306
|
+
[
|
|
14307
|
+
projectId,
|
|
14308
|
+
data.cost_type_id ?? null,
|
|
14309
|
+
data.category_id ?? null,
|
|
14310
|
+
data.description ?? null,
|
|
14311
|
+
effectiveAmount,
|
|
14312
|
+
data.quantity ?? 1,
|
|
14313
|
+
data.unit_amount ?? null,
|
|
14314
|
+
data.currency ?? 'BRL',
|
|
14315
|
+
data.cost_date ?? null,
|
|
14316
|
+
data.period_start ?? null,
|
|
14317
|
+
data.period_end ?? null,
|
|
14318
|
+
calcType,
|
|
14319
|
+
data.recurrence_type ?? 'none',
|
|
14320
|
+
data.is_billable ?? false,
|
|
14321
|
+
data.is_reimbursable ?? false,
|
|
14322
|
+
data.notes ?? null,
|
|
14323
|
+
data.status ?? 'planned',
|
|
14324
|
+
]
|
|
14325
|
+
);
|
|
14326
|
+
|
|
14327
|
+
if (!created?.id) {
|
|
14328
|
+
throw new BadRequestException('Unable to create project cost.');
|
|
14329
|
+
}
|
|
14330
|
+
|
|
14331
|
+
const rows = await this.listProjectCosts(userId, projectId, {});
|
|
14332
|
+
return rows.find((r) => r.id === created.id) ?? null;
|
|
14333
|
+
}
|
|
14334
|
+
|
|
14335
|
+
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 }>) {
|
|
14336
|
+
const actor = await this.getActorContext(userId);
|
|
14337
|
+
this.ensureSupervisor(actor);
|
|
14338
|
+
|
|
14339
|
+
const cost = await this.querySingle<{ id: number; projectId: number; calculationType: string; unitAmount: string | null; quantity: string }>(
|
|
14340
|
+
`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`,
|
|
14341
|
+
[id]
|
|
14342
|
+
);
|
|
14343
|
+
if (!cost) {
|
|
14344
|
+
throw new NotFoundException('Project cost not found.');
|
|
14345
|
+
}
|
|
14346
|
+
|
|
14347
|
+
// Auto-calculate amount when applicable
|
|
14348
|
+
const effectiveCalcType = data.calculation_type ?? cost.calculationType;
|
|
14349
|
+
if (['unit', 'hourly', 'monthly'].includes(effectiveCalcType)) {
|
|
14350
|
+
const ua = data.unit_amount !== undefined ? data.unit_amount : (cost.unitAmount !== null ? parseFloat(cost.unitAmount) : null);
|
|
14351
|
+
const qty = data.quantity !== undefined ? data.quantity : parseFloat(cost.quantity);
|
|
14352
|
+
if (ua !== null && ua !== undefined) {
|
|
14353
|
+
data = { ...data, amount: Math.round(qty * ua * 100) / 100 };
|
|
14354
|
+
}
|
|
14355
|
+
}
|
|
14356
|
+
|
|
14357
|
+
const sets: string[] = [];
|
|
14358
|
+
const params: unknown[] = [];
|
|
14359
|
+
|
|
14360
|
+
if (data.cost_type_id !== undefined) sets.push(`cost_type_id = ${this.param(params, data.cost_type_id)}`);
|
|
14361
|
+
if (data.category_id !== undefined) sets.push(`category_id = ${this.param(params, data.category_id)}`);
|
|
14362
|
+
if (data.description !== undefined) sets.push(`description = ${this.param(params, data.description)}`);
|
|
14363
|
+
if (data.amount !== undefined) sets.push(`amount = ${this.param(params, data.amount)}`);
|
|
14364
|
+
if (data.currency !== undefined) sets.push(`currency = ${this.param(params, data.currency)}`);
|
|
14365
|
+
if (data.quantity !== undefined) sets.push(`quantity = ${this.param(params, data.quantity)}`);
|
|
14366
|
+
if (data.unit_amount !== undefined) sets.push(`unit_amount = ${this.param(params, data.unit_amount)}`);
|
|
14367
|
+
if (data.calculation_type !== undefined) sets.push(`calculation_type = ${this.param(params, data.calculation_type)}::operations_project_cost_calculation_type_134cdfb49c_enum`);
|
|
14368
|
+
if (data.recurrence_type !== undefined) sets.push(`recurrence_type = ${this.param(params, data.recurrence_type)}::operations_project_cost_recurrence_type_09baf0f043_enum`);
|
|
14369
|
+
if (data.is_billable !== undefined) sets.push(`is_billable = ${this.param(params, data.is_billable)}`);
|
|
14370
|
+
if (data.is_reimbursable !== undefined) sets.push(`is_reimbursable = ${this.param(params, data.is_reimbursable)}`);
|
|
14371
|
+
if (data.cost_date !== undefined) sets.push(`cost_date = ${this.param(params, data.cost_date)}::date`);
|
|
14372
|
+
if (data.period_start !== undefined) sets.push(`period_start = ${this.param(params, data.period_start)}::date`);
|
|
14373
|
+
if (data.period_end !== undefined) sets.push(`period_end = ${this.param(params, data.period_end)}::date`);
|
|
14374
|
+
if (data.notes !== undefined) sets.push(`notes = ${this.param(params, data.notes)}`);
|
|
14375
|
+
if (data.status !== undefined) sets.push(`status = ${this.param(params, data.status)}::operations_project_cost_status_153e8592ce_enum`);
|
|
14376
|
+
|
|
14377
|
+
if (sets.length === 0) {
|
|
14378
|
+
const rows = await this.listProjectCosts(userId, cost.projectId, {});
|
|
14379
|
+
return rows.find((r) => r.id === id) ?? null;
|
|
14380
|
+
}
|
|
14381
|
+
|
|
14382
|
+
sets.push(`updated_at = NOW()`);
|
|
14383
|
+
await this.prisma.$queryRawUnsafe(
|
|
14384
|
+
`UPDATE operations_project_cost SET ${sets.join(', ')} WHERE id = ${this.param(params, id)}`,
|
|
14385
|
+
...params
|
|
14386
|
+
);
|
|
14387
|
+
|
|
14388
|
+
const rows = await this.listProjectCosts(userId, cost.projectId, {});
|
|
14389
|
+
return rows.find((r) => r.id === id) ?? null;
|
|
14390
|
+
}
|
|
14391
|
+
|
|
14392
|
+
async deleteProjectCost(userId: number, id: number) {
|
|
14393
|
+
const actor = await this.getActorContext(userId);
|
|
14394
|
+
this.ensureSupervisor(actor);
|
|
14395
|
+
|
|
14396
|
+
const cost = await this.querySingle<{ id: number }>(
|
|
14397
|
+
`SELECT id FROM operations_project_cost WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
|
|
14398
|
+
[id]
|
|
14399
|
+
);
|
|
14400
|
+
if (!cost) {
|
|
14401
|
+
throw new NotFoundException('Project cost not found.');
|
|
14402
|
+
}
|
|
14403
|
+
|
|
14404
|
+
await this.prisma.$queryRawUnsafe(
|
|
14405
|
+
`UPDATE operations_project_cost SET deleted_at = NOW() WHERE id = $1`,
|
|
14406
|
+
id
|
|
14407
|
+
);
|
|
14408
|
+
|
|
14409
|
+
return { success: true };
|
|
14410
|
+
}
|
|
12176
14411
|
}
|