@hed-hog/operations 0.0.321 → 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-contracts.controller.d.ts +9 -9
- 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 +64 -0
- package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
- package/dist/controllers/operations-tasks.controller.js +85 -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/create-task.dto.d.ts.map +1 -1
- package/dist/dto/create-task.dto.js +0 -1
- package/dist/dto/create-task.dto.js.map +1 -1
- 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/dto/update-task.dto.d.ts.map +1 -1
- package/dist/dto/update-task.dto.js +0 -1
- package/dist/dto/update-task.dto.js.map +1 -1
- 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 +584 -0
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +1734 -69
- 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 +313 -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 -826
- 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 +3390 -889
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +118 -79
- package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +297 -2
- package/hedhog/frontend/app/_components/task-file-attachments.tsx.ejs +388 -0
- 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 +197 -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 +340 -133
- 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/projects/page.tsx.ejs +105 -22
- 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 +421 -11
- 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 +426 -14
- 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/hedhog/table/operations_task_file.yaml +23 -0
- package/package.json +5 -5
- 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 +92 -9
- 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/create-task.dto.ts +0 -1
- 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/dto/update-task.dto.ts +0 -1
- package/src/operations.module.ts +2 -0
- package/src/operations.service.ts +2421 -64
|
@@ -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
|
|
@@ -2570,6 +2672,7 @@ export class OperationsService {
|
|
|
2570
2672
|
p.code,
|
|
2571
2673
|
p.name,
|
|
2572
2674
|
p.client_name AS "clientName",
|
|
2675
|
+
cp.avatar_id AS "clientAvatarId",
|
|
2573
2676
|
p.summary,
|
|
2574
2677
|
p.status,
|
|
2575
2678
|
p.progress_percent AS "progressPercent",
|
|
@@ -2580,17 +2683,20 @@ export class OperationsService {
|
|
|
2580
2683
|
c.name AS "contractName",
|
|
2581
2684
|
c.status AS "contractStatus",
|
|
2582
2685
|
m.display_name AS "managerName",
|
|
2686
|
+
mp.avatar_id AS "managerAvatarId",
|
|
2583
2687
|
${ownAssignmentSelect}
|
|
2584
2688
|
COUNT(DISTINCT pa.id)::int AS "teamSize"
|
|
2585
2689
|
FROM operations_project p
|
|
2586
2690
|
LEFT JOIN operations_contract c ON c.id = p.contract_id
|
|
2587
2691
|
LEFT JOIN operations_collaborator m ON m.id = p.manager_collaborator_id
|
|
2692
|
+
LEFT JOIN person cp ON cp.id = p.client_person_id
|
|
2693
|
+
LEFT JOIN person mp ON mp.id = m.person_id
|
|
2588
2694
|
LEFT JOIN operations_project_assignment pa
|
|
2589
2695
|
ON pa.project_id = p.id
|
|
2590
2696
|
AND pa.deleted_at IS NULL
|
|
2591
2697
|
AND pa.status IN ('planned', 'active')
|
|
2592
2698
|
WHERE ${whereClause}
|
|
2593
|
-
GROUP BY p.id, c.id, m.id`;
|
|
2699
|
+
GROUP BY p.id, c.id, m.id, cp.id, mp.id`;
|
|
2594
2700
|
|
|
2595
2701
|
if (!pagination) {
|
|
2596
2702
|
return this.queryRows(`${baseQuery} ORDER BY p.name ASC`, params);
|
|
@@ -2751,6 +2857,7 @@ export class OperationsService {
|
|
|
2751
2857
|
status?: string;
|
|
2752
2858
|
myOnly?: boolean;
|
|
2753
2859
|
archived?: boolean;
|
|
2860
|
+
collaboratorId?: number;
|
|
2754
2861
|
}
|
|
2755
2862
|
) {
|
|
2756
2863
|
const actor = await this.getActorContext(userId);
|
|
@@ -2824,6 +2931,13 @@ export class OperationsService {
|
|
|
2824
2931
|
filters.push(`t.status::text = ${this.param(params, paginationParams.status)}`);
|
|
2825
2932
|
}
|
|
2826
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
|
+
|
|
2827
2941
|
const whereClause = filters.join(' AND ');
|
|
2828
2942
|
const totalRow = await this.querySingle<{ total: string }>(
|
|
2829
2943
|
`SELECT COUNT(*)::text AS total
|
|
@@ -2864,6 +2978,8 @@ export class OperationsService {
|
|
|
2864
2978
|
assigneeName: string | null;
|
|
2865
2979
|
assigneeUserPhotoId: number | null;
|
|
2866
2980
|
assigneePersonAvatarId: number | null;
|
|
2981
|
+
commentCount: number;
|
|
2982
|
+
fileCount: number;
|
|
2867
2983
|
createdAt: string;
|
|
2868
2984
|
deletedAt: string | null;
|
|
2869
2985
|
}>(
|
|
@@ -2882,6 +2998,8 @@ export class OperationsService {
|
|
|
2882
2998
|
ac.display_name AS "assigneeName",
|
|
2883
2999
|
au.photo_id AS "assigneeUserPhotoId",
|
|
2884
3000
|
ap.avatar_id AS "assigneePersonAvatarId",
|
|
3001
|
+
COALESCE(task_comments.count, 0)::int AS "commentCount",
|
|
3002
|
+
COALESCE(task_files.count, 0)::int AS "fileCount",
|
|
2885
3003
|
t.created_at AS "createdAt",
|
|
2886
3004
|
t.deleted_at AS "deletedAt"
|
|
2887
3005
|
FROM operations_task t
|
|
@@ -2894,6 +3012,16 @@ export class OperationsService {
|
|
|
2894
3012
|
ON au.id = ac.user_id
|
|
2895
3013
|
LEFT JOIN person ap
|
|
2896
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
|
|
2897
3025
|
JOIN operations_project p
|
|
2898
3026
|
ON p.id = COALESCE(t.project_id, pa.project_id)
|
|
2899
3027
|
WHERE ${whereClause}
|
|
@@ -2924,29 +3052,39 @@ export class OperationsService {
|
|
|
2924
3052
|
this.requireFields(data as Record<string, unknown>, ['name']);
|
|
2925
3053
|
|
|
2926
3054
|
let assignmentId: number | null = null;
|
|
2927
|
-
let projectId: number | null = null;
|
|
3055
|
+
let projectId: number | null = data.projectId ?? null;
|
|
2928
3056
|
|
|
2929
3057
|
if (data.projectId || data.projectAssignmentId) {
|
|
2930
|
-
|
|
2931
|
-
this.
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3058
|
+
if (actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
3059
|
+
const assignment = await this.resolveProjectAssignmentForActor(
|
|
3060
|
+
this.prisma,
|
|
3061
|
+
actor,
|
|
3062
|
+
{
|
|
3063
|
+
projectId: data.projectId ?? null,
|
|
3064
|
+
projectAssignmentId: data.projectAssignmentId ?? null,
|
|
3065
|
+
}
|
|
3066
|
+
);
|
|
3067
|
+
await this.assertProjectAccess(actor, assignment.projectId);
|
|
3068
|
+
assignmentId = assignment.id;
|
|
3069
|
+
projectId = assignment.projectId;
|
|
3070
|
+
} else {
|
|
3071
|
+
if (data.projectId) {
|
|
3072
|
+
await this.assertProjectAccess(actor, data.projectId);
|
|
3073
|
+
projectId = data.projectId;
|
|
2936
3074
|
}
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
3075
|
+
if (data.projectAssignmentId) {
|
|
3076
|
+
const assignment = await this.resolveProjectAssignmentForActor(
|
|
3077
|
+
this.prisma,
|
|
3078
|
+
actor,
|
|
3079
|
+
{
|
|
3080
|
+
projectId: data.projectId ?? null,
|
|
3081
|
+
projectAssignmentId: data.projectAssignmentId,
|
|
3082
|
+
}
|
|
3083
|
+
);
|
|
3084
|
+
assignmentId = assignment.id;
|
|
3085
|
+
projectId = assignment.projectId;
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
2950
3088
|
}
|
|
2951
3089
|
|
|
2952
3090
|
const name = this.normalizeOptionalText(data.name);
|
|
@@ -2989,7 +3127,7 @@ export class OperationsService {
|
|
|
2989
3127
|
[
|
|
2990
3128
|
projectId,
|
|
2991
3129
|
assignmentId,
|
|
2992
|
-
data.assigneeCollaboratorId ??
|
|
3130
|
+
data.assigneeCollaboratorId ?? null,
|
|
2993
3131
|
name,
|
|
2994
3132
|
this.normalizeOptionalText(data.description),
|
|
2995
3133
|
data.priority ?? 'medium',
|
|
@@ -3140,6 +3278,291 @@ export class OperationsService {
|
|
|
3140
3278
|
return { success: true };
|
|
3141
3279
|
}
|
|
3142
3280
|
|
|
3281
|
+
async listTaskFiles(userId: number, taskId: number) {
|
|
3282
|
+
const actor = await this.getActorContext(userId);
|
|
3283
|
+
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
3284
|
+
throw new ForbiddenException(
|
|
3285
|
+
'Operations collaborator access is required.'
|
|
3286
|
+
);
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
const current = await this.getTaskRecordForActor(
|
|
3290
|
+
this.prisma,
|
|
3291
|
+
actor,
|
|
3292
|
+
taskId
|
|
3293
|
+
);
|
|
3294
|
+
await this.assertProjectAccess(actor, current.projectId);
|
|
3295
|
+
|
|
3296
|
+
const rows = (await (this.prisma as any).$queryRawUnsafe(
|
|
3297
|
+
`SELECT tf.id, tf.file_id, f.filename, f.size, m.name AS mimetype, tf.created_at
|
|
3298
|
+
FROM operations_task_file tf
|
|
3299
|
+
JOIN file f ON f.id = tf.file_id
|
|
3300
|
+
JOIN file_mimetype m ON m.id = f.mimetype_id
|
|
3301
|
+
WHERE tf.operations_task_id = $1
|
|
3302
|
+
ORDER BY tf.created_at ASC`,
|
|
3303
|
+
taskId
|
|
3304
|
+
)) as Array<{
|
|
3305
|
+
id: number;
|
|
3306
|
+
file_id: number;
|
|
3307
|
+
filename: string;
|
|
3308
|
+
size: number;
|
|
3309
|
+
mimetype: string;
|
|
3310
|
+
created_at: Date;
|
|
3311
|
+
}>;
|
|
3312
|
+
|
|
3313
|
+
return rows;
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
async addTaskFile(
|
|
3317
|
+
userId: number,
|
|
3318
|
+
taskId: number,
|
|
3319
|
+
file: MulterFile
|
|
3320
|
+
) {
|
|
3321
|
+
const actor = await this.getActorContext(userId);
|
|
3322
|
+
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
3323
|
+
throw new ForbiddenException(
|
|
3324
|
+
'Operations collaborator access is required.'
|
|
3325
|
+
);
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
const current = await this.getTaskRecordForActor(
|
|
3329
|
+
this.prisma,
|
|
3330
|
+
actor,
|
|
3331
|
+
taskId
|
|
3332
|
+
);
|
|
3333
|
+
await this.assertProjectAccess(actor, current.projectId);
|
|
3334
|
+
|
|
3335
|
+
const uploaded = await this.fileService.upload(
|
|
3336
|
+
`operations/tasks/${taskId}`,
|
|
3337
|
+
file
|
|
3338
|
+
);
|
|
3339
|
+
|
|
3340
|
+
await (this.prisma as any).$executeRawUnsafe(
|
|
3341
|
+
`INSERT INTO operations_task_file (operations_task_id, file_id, created_at, updated_at)
|
|
3342
|
+
VALUES ($1, $2, NOW(), NOW())`,
|
|
3343
|
+
taskId,
|
|
3344
|
+
uploaded.id
|
|
3345
|
+
);
|
|
3346
|
+
|
|
3347
|
+
return uploaded;
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
async removeTaskFile(userId: number, taskId: number, fileRelationId: number) {
|
|
3351
|
+
const actor = await this.getActorContext(userId);
|
|
3352
|
+
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
3353
|
+
throw new ForbiddenException(
|
|
3354
|
+
'Operations collaborator access is required.'
|
|
3355
|
+
);
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
const current = await this.getTaskRecordForActor(
|
|
3359
|
+
this.prisma,
|
|
3360
|
+
actor,
|
|
3361
|
+
taskId
|
|
3362
|
+
);
|
|
3363
|
+
await this.assertProjectAccess(actor, current.projectId);
|
|
3364
|
+
|
|
3365
|
+
const rows = (await (this.prisma as any).$queryRawUnsafe(
|
|
3366
|
+
`SELECT file_id FROM operations_task_file WHERE id = $1 AND operations_task_id = $2`,
|
|
3367
|
+
fileRelationId,
|
|
3368
|
+
taskId
|
|
3369
|
+
)) as Array<{ file_id: number }>;
|
|
3370
|
+
|
|
3371
|
+
if (!rows.length) {
|
|
3372
|
+
throw new NotFoundException('Task file attachment not found.');
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
const fileId = rows[0].file_id;
|
|
3376
|
+
|
|
3377
|
+
await (this.prisma as any).$executeRawUnsafe(
|
|
3378
|
+
`DELETE FROM operations_task_file WHERE id = $1`,
|
|
3379
|
+
fileRelationId
|
|
3380
|
+
);
|
|
3381
|
+
|
|
3382
|
+
if (fileId) {
|
|
3383
|
+
await this.fileService.delete('en', { ids: [fileId] });
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
return { success: true };
|
|
3387
|
+
}
|
|
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
|
+
|
|
3143
3566
|
async listTimesheetEntries(
|
|
3144
3567
|
userId: number,
|
|
3145
3568
|
paginationParams: {
|
|
@@ -4977,6 +5400,7 @@ export class OperationsService {
|
|
|
4977
5400
|
status?: string;
|
|
4978
5401
|
dateFrom?: string;
|
|
4979
5402
|
dateTo?: string;
|
|
5403
|
+
collaboratorId?: number;
|
|
4980
5404
|
} = {}
|
|
4981
5405
|
) {
|
|
4982
5406
|
const actor = await this.getActorContext(userId);
|
|
@@ -5003,6 +5427,10 @@ export class OperationsService {
|
|
|
5003
5427
|
where.push(`t.week_start_date <= ${this.param(params, filters.dateTo)}::date`);
|
|
5004
5428
|
}
|
|
5005
5429
|
|
|
5430
|
+
if (filters.collaboratorId) {
|
|
5431
|
+
where.push(`t.collaborator_id = ${this.param(params, filters.collaboratorId)}`);
|
|
5432
|
+
}
|
|
5433
|
+
|
|
5006
5434
|
if (pagination?.search) {
|
|
5007
5435
|
const searchPlaceholder = this.param(params, `%${pagination.search}%`);
|
|
5008
5436
|
where.push(`(
|
|
@@ -5042,6 +5470,7 @@ export class OperationsService {
|
|
|
5042
5470
|
reviewedAt: string | null;
|
|
5043
5471
|
notes: string | null;
|
|
5044
5472
|
decisionNote: string | null;
|
|
5473
|
+
approvalId: number | null;
|
|
5045
5474
|
}>(
|
|
5046
5475
|
`SELECT t.id,
|
|
5047
5476
|
t.collaborator_id AS "collaboratorId",
|
|
@@ -5055,7 +5484,8 @@ export class OperationsService {
|
|
|
5055
5484
|
t.submitted_at AS "submittedAt",
|
|
5056
5485
|
t.reviewed_at AS "reviewedAt",
|
|
5057
5486
|
t.notes,
|
|
5058
|
-
approval.decision_note AS "decisionNote"
|
|
5487
|
+
approval.decision_note AS "decisionNote",
|
|
5488
|
+
approval.id AS "approvalId"
|
|
5059
5489
|
FROM operations_timesheet t
|
|
5060
5490
|
JOIN operations_collaborator c ON c.id = t.collaborator_id
|
|
5061
5491
|
LEFT JOIN operations_collaborator a ON a.id = t.approver_collaborator_id
|
|
@@ -7904,6 +8334,7 @@ export class OperationsService {
|
|
|
7904
8334
|
assigneeUserPhotoId: number | null;
|
|
7905
8335
|
assigneePersonAvatarId: number | null;
|
|
7906
8336
|
projectAssignmentId: number | null;
|
|
8337
|
+
commentCount: number;
|
|
7907
8338
|
createdAt: string;
|
|
7908
8339
|
}>(
|
|
7909
8340
|
`SELECT t.id,
|
|
@@ -7920,6 +8351,7 @@ export class OperationsService {
|
|
|
7920
8351
|
au.photo_id AS "assigneeUserPhotoId",
|
|
7921
8352
|
ap.avatar_id AS "assigneePersonAvatarId",
|
|
7922
8353
|
t.project_assignment_id AS "projectAssignmentId",
|
|
8354
|
+
COALESCE(task_comments.count, 0)::int AS "commentCount",
|
|
7923
8355
|
t.created_at AS "createdAt"
|
|
7924
8356
|
FROM operations_task t
|
|
7925
8357
|
LEFT JOIN operations_collaborator ac
|
|
@@ -7928,6 +8360,11 @@ export class OperationsService {
|
|
|
7928
8360
|
ON au.id = ac.user_id
|
|
7929
8361
|
LEFT JOIN person ap
|
|
7930
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
|
|
7931
8368
|
WHERE COALESCE(t.project_id, (
|
|
7932
8369
|
SELECT pa.project_id FROM operations_project_assignment pa
|
|
7933
8370
|
WHERE pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
|
|
@@ -7958,6 +8395,7 @@ export class OperationsService {
|
|
|
7958
8395
|
assigneePersonAvatarId: number | null;
|
|
7959
8396
|
projectAssignmentId: number | null;
|
|
7960
8397
|
projectId: number | null;
|
|
8398
|
+
commentCount: number;
|
|
7961
8399
|
createdAt: string;
|
|
7962
8400
|
deletedAt: string | null;
|
|
7963
8401
|
}>(
|
|
@@ -7976,6 +8414,7 @@ export class OperationsService {
|
|
|
7976
8414
|
ap.avatar_id AS "assigneePersonAvatarId",
|
|
7977
8415
|
t.project_assignment_id AS "projectAssignmentId",
|
|
7978
8416
|
COALESCE(t.project_id, pa.project_id) AS "projectId",
|
|
8417
|
+
COALESCE(task_comments.count, 0)::int AS "commentCount",
|
|
7979
8418
|
t.created_at AS "createdAt",
|
|
7980
8419
|
t.deleted_at AS "deletedAt"
|
|
7981
8420
|
FROM operations_task t
|
|
@@ -7985,6 +8424,11 @@ export class OperationsService {
|
|
|
7985
8424
|
LEFT JOIN person ap ON ap.id = ac.person_id
|
|
7986
8425
|
LEFT JOIN operations_project_assignment pa
|
|
7987
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
|
|
7988
8432
|
WHERE t.id = $1`,
|
|
7989
8433
|
[taskId]
|
|
7990
8434
|
);
|
|
@@ -10930,12 +11374,18 @@ export class OperationsService {
|
|
|
10930
11374
|
au.photo_id AS "assigneeUserPhotoId",
|
|
10931
11375
|
ap.avatar_id AS "assigneePersonAvatarId",
|
|
10932
11376
|
t.project_assignment_id AS "projectAssignmentId",
|
|
11377
|
+
COALESCE(task_comments.count, 0)::int AS "commentCount",
|
|
10933
11378
|
t.created_at AS "createdAt"
|
|
10934
11379
|
FROM operations_task t
|
|
10935
11380
|
LEFT JOIN operations_collaborator ac
|
|
10936
11381
|
ON ac.id = t.assignee_collaborator_id AND ac.deleted_at IS NULL
|
|
10937
11382
|
LEFT JOIN "user" au ON au.id = ac.user_id
|
|
10938
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
|
|
10939
11389
|
WHERE COALESCE(t.project_id, (
|
|
10940
11390
|
SELECT pa.project_id FROM operations_project_assignment pa
|
|
10941
11391
|
WHERE pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
|
|
@@ -10999,7 +11449,9 @@ export class OperationsService {
|
|
|
10999
11449
|
];
|
|
11000
11450
|
|
|
11001
11451
|
if (actor.collaboratorId) {
|
|
11002
|
-
|
|
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})`);
|
|
11003
11455
|
}
|
|
11004
11456
|
|
|
11005
11457
|
if (pagination.search) {
|
|
@@ -11056,6 +11508,7 @@ export class OperationsService {
|
|
|
11056
11508
|
assigneeName: string | null;
|
|
11057
11509
|
assigneeUserPhotoId: number | null;
|
|
11058
11510
|
assigneePersonAvatarId: number | null;
|
|
11511
|
+
commentCount: number;
|
|
11059
11512
|
createdAt: string;
|
|
11060
11513
|
deletedAt: string | null;
|
|
11061
11514
|
}>(
|
|
@@ -11074,6 +11527,7 @@ export class OperationsService {
|
|
|
11074
11527
|
ac.display_name AS "assigneeName",
|
|
11075
11528
|
au.photo_id AS "assigneeUserPhotoId",
|
|
11076
11529
|
ap.avatar_id AS "assigneePersonAvatarId",
|
|
11530
|
+
COALESCE(task_comments.count, 0)::int AS "commentCount",
|
|
11077
11531
|
t.created_at AS "createdAt",
|
|
11078
11532
|
t.deleted_at AS "deletedAt"
|
|
11079
11533
|
FROM operations_task t
|
|
@@ -11086,6 +11540,11 @@ export class OperationsService {
|
|
|
11086
11540
|
ON au.id = ac.user_id
|
|
11087
11541
|
LEFT JOIN person ap
|
|
11088
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
|
|
11089
11548
|
JOIN operations_project p
|
|
11090
11549
|
ON p.id = COALESCE(t.project_id, pa.project_id)
|
|
11091
11550
|
WHERE ${whereClause}
|
|
@@ -11320,10 +11779,19 @@ export class OperationsService {
|
|
|
11320
11779
|
? { revenue: 0.9, cost: 0.96, backlog: 0.82 }
|
|
11321
11780
|
: { revenue: 1, cost: 1, backlog: 1 };
|
|
11322
11781
|
|
|
11323
|
-
const
|
|
11324
|
-
const
|
|
11325
|
-
|
|
11326
|
-
|
|
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];
|
|
11792
|
+
const where = [
|
|
11793
|
+
'p.deleted_at IS NULL',
|
|
11794
|
+
'(p.end_date IS NULL OR p.end_date >= $1::date)',
|
|
11327
11795
|
'(p.start_date IS NULL OR p.start_date <= $2::date)',
|
|
11328
11796
|
];
|
|
11329
11797
|
if (filters.client && filters.client !== 'all') {
|
|
@@ -11347,6 +11815,8 @@ export class OperationsService {
|
|
|
11347
11815
|
weeklyHours: string | null;
|
|
11348
11816
|
actualHours: string | null;
|
|
11349
11817
|
billableHours: string | null;
|
|
11818
|
+
realizedCost: string | null;
|
|
11819
|
+
allocatedCost: string | null;
|
|
11350
11820
|
openTasks: string | null;
|
|
11351
11821
|
backlogHours: string | null;
|
|
11352
11822
|
futureDeliveries: string | null;
|
|
@@ -11365,6 +11835,8 @@ export class OperationsService {
|
|
|
11365
11835
|
COALESCE(assignment_stats.weekly_hours, 0)::text AS "weeklyHours",
|
|
11366
11836
|
COALESCE(time_stats.actual_hours, 0)::text AS "actualHours",
|
|
11367
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",
|
|
11368
11840
|
COALESCE(task_stats.open_tasks, 0)::text AS "openTasks",
|
|
11369
11841
|
COALESCE(task_stats.backlog_hours, 0)::text AS "backlogHours",
|
|
11370
11842
|
COALESCE(task_stats.future_deliveries, 0)::text AS "futureDeliveries"
|
|
@@ -11393,6 +11865,146 @@ export class OperationsService {
|
|
|
11393
11865
|
AND entry.deleted_at IS NULL
|
|
11394
11866
|
AND entry.work_date BETWEEN $1::date AND $2::date
|
|
11395
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
|
|
11396
12008
|
LEFT JOIN LATERAL (
|
|
11397
12009
|
SELECT COUNT(*) FILTER (WHERE task.status IN ('todo', 'doing', 'review')) AS open_tasks,
|
|
11398
12010
|
COALESCE(SUM(task.estimate_hours) FILTER (WHERE task.status IN ('todo', 'doing', 'review')), 0) AS backlog_hours,
|
|
@@ -11406,12 +12018,6 @@ export class OperationsService {
|
|
|
11406
12018
|
params
|
|
11407
12019
|
);
|
|
11408
12020
|
|
|
11409
|
-
const fromDate = new Date(`${from}T00:00:00`);
|
|
11410
|
-
const toDate = new Date(`${to}T00:00:00`);
|
|
11411
|
-
const periodWeeks = Math.max(
|
|
11412
|
-
1,
|
|
11413
|
-
Math.ceil((toDate.getTime() - fromDate.getTime()) / 604800000)
|
|
11414
|
-
);
|
|
11415
12021
|
const rows = dbRows
|
|
11416
12022
|
.map((row) => {
|
|
11417
12023
|
const progress = Number(row.progressPercent ?? 0);
|
|
@@ -11419,7 +12025,12 @@ export class OperationsService {
|
|
|
11419
12025
|
const recognizedRevenue = contractedRevenue * (progress / 100);
|
|
11420
12026
|
const actualHours = Number(row.actualHours ?? 0);
|
|
11421
12027
|
const plannedHours = Math.max(Number(row.weeklyHours ?? 0) * periodWeeks, actualHours);
|
|
11422
|
-
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);
|
|
11423
12034
|
const reportStatus =
|
|
11424
12035
|
row.status === 'paused'
|
|
11425
12036
|
? 'paused'
|
|
@@ -11454,7 +12065,7 @@ export class OperationsService {
|
|
|
11454
12065
|
contractedRevenue,
|
|
11455
12066
|
recognizedRevenue,
|
|
11456
12067
|
realizedCost,
|
|
11457
|
-
forecastCost: realizedCost,
|
|
12068
|
+
forecastCost: realizedCost * multiplier.cost,
|
|
11458
12069
|
teamCost: realizedCost,
|
|
11459
12070
|
infraCost: 0,
|
|
11460
12071
|
licenseCost: 0,
|
|
@@ -11470,6 +12081,10 @@ export class OperationsService {
|
|
|
11470
12081
|
financialProgress: contractedRevenue ? (recognizedRevenue / contractedRevenue) * 100 : 0,
|
|
11471
12082
|
backlogValue: Math.max(contractedRevenue - recognizedRevenue, 0),
|
|
11472
12083
|
futureDeliveries: Number(row.futureDeliveries ?? 0),
|
|
12084
|
+
allocatedCost,
|
|
12085
|
+
consumedHoursCost,
|
|
12086
|
+
idlenessRate,
|
|
12087
|
+
idlenessCost,
|
|
11473
12088
|
risk,
|
|
11474
12089
|
recommendation:
|
|
11475
12090
|
risk === 'alto'
|
|
@@ -11495,6 +12110,9 @@ export class OperationsService {
|
|
|
11495
12110
|
acc.avgDeadline += row.physicalProgress;
|
|
11496
12111
|
acc.avgAllocation += row.allocatedCapacity;
|
|
11497
12112
|
acc.atRisk += row.risk === 'alto' ? 1 : 0;
|
|
12113
|
+
acc.allocatedCost += row.allocatedCost;
|
|
12114
|
+
acc.consumedHoursCost += row.consumedHoursCost;
|
|
12115
|
+
acc.idlenessCost += row.idlenessCost;
|
|
11498
12116
|
return acc;
|
|
11499
12117
|
},
|
|
11500
12118
|
{
|
|
@@ -11513,6 +12131,11 @@ export class OperationsService {
|
|
|
11513
12131
|
avgAllocation: 0,
|
|
11514
12132
|
atRisk: 0,
|
|
11515
12133
|
burnRate: 0,
|
|
12134
|
+
allocatedCost: 0,
|
|
12135
|
+
consumedHoursCost: 0,
|
|
12136
|
+
idlenessCost: 0,
|
|
12137
|
+
idlenessRate: 0,
|
|
12138
|
+
plannedProfit: 0,
|
|
11516
12139
|
}
|
|
11517
12140
|
);
|
|
11518
12141
|
summary.profit = summary.recognizedRevenue - summary.realizedCost;
|
|
@@ -11520,6 +12143,10 @@ export class OperationsService {
|
|
|
11520
12143
|
summary.avgDeadline = rows.length ? summary.avgDeadline / rows.length : 0;
|
|
11521
12144
|
summary.avgAllocation = rows.length ? summary.avgAllocation / rows.length : 0;
|
|
11522
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;
|
|
11523
12150
|
|
|
11524
12151
|
const forecast = Array.from({ length: 12 }, (_, index) => {
|
|
11525
12152
|
const monthDate = new Date(fromDate);
|
|
@@ -11622,6 +12249,15 @@ export class OperationsService {
|
|
|
11622
12249
|
: scenario === 'conservative'
|
|
11623
12250
|
? { revenue: 0.9, cost: 0.96, capacity: 0.94 }
|
|
11624
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
|
+
|
|
11625
12261
|
const params: unknown[] = [from, to];
|
|
11626
12262
|
const where = [
|
|
11627
12263
|
'c.deleted_at IS NULL',
|
|
@@ -11652,6 +12288,11 @@ export class OperationsService {
|
|
|
11652
12288
|
taxesCost: string | null;
|
|
11653
12289
|
toolsCost: string | null;
|
|
11654
12290
|
billableValue: string | null;
|
|
12291
|
+
plannedAllocatedHours: string | null;
|
|
12292
|
+
plannedBillableHours: string | null;
|
|
12293
|
+
openTaskHours: string | null;
|
|
12294
|
+
openTaskBillableHours: string | null;
|
|
12295
|
+
openTasks: string | null;
|
|
11655
12296
|
allocatedHours: string | null;
|
|
11656
12297
|
billableHours: string | null;
|
|
11657
12298
|
projects: string | null;
|
|
@@ -11670,9 +12311,14 @@ export class OperationsService {
|
|
|
11670
12311
|
COALESCE(cost_stats.taxes_cost, 0)::text AS "taxesCost",
|
|
11671
12312
|
COALESCE(cost_stats.tools_cost, 0)::text AS "toolsCost",
|
|
11672
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",
|
|
11673
12319
|
COALESCE(value_stats.allocated_hours, 0)::text AS "allocatedHours",
|
|
11674
12320
|
COALESCE(value_stats.billable_hours, 0)::text AS "billableHours",
|
|
11675
|
-
COALESCE(
|
|
12321
|
+
COALESCE(assignment_stats.projects, 0)::text AS projects
|
|
11676
12322
|
FROM operations_collaborator c
|
|
11677
12323
|
LEFT JOIN person person_record ON person_record.id = c.person_id
|
|
11678
12324
|
LEFT JOIN operations_department department_record
|
|
@@ -11685,16 +12331,85 @@ export class OperationsService {
|
|
|
11685
12331
|
ON collaborator_type.id = c.collaborator_type_id
|
|
11686
12332
|
AND collaborator_type.deleted_at IS NULL
|
|
11687
12333
|
LEFT JOIN LATERAL (
|
|
11688
|
-
SELECT COALESCE(
|
|
11689
|
-
|
|
11690
|
-
|
|
11691
|
-
|
|
11692
|
-
FROM
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
|
|
11696
|
-
|
|
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
|
|
11697
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
|
|
11698
12413
|
LEFT JOIN LATERAL (
|
|
11699
12414
|
SELECT COALESCE(SUM(entry.hours), 0) AS allocated_hours,
|
|
11700
12415
|
COALESCE(SUM(entry.hours) FILTER (WHERE pa.is_billable = true), 0) AS billable_hours,
|
|
@@ -11708,31 +12423,50 @@ export class OperationsService {
|
|
|
11708
12423
|
AND entry.work_date BETWEEN $1::date AND $2::date
|
|
11709
12424
|
) value_stats ON TRUE
|
|
11710
12425
|
LEFT JOIN LATERAL (
|
|
11711
|
-
SELECT COUNT(
|
|
11712
|
-
|
|
11713
|
-
|
|
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
|
|
11714
12435
|
AND pa.deleted_at IS NULL
|
|
11715
|
-
|
|
11716
|
-
|
|
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
|
|
11717
12443
|
WHERE ${where.join(' AND ')}
|
|
11718
12444
|
ORDER BY name ASC`,
|
|
11719
12445
|
params
|
|
11720
12446
|
);
|
|
11721
12447
|
|
|
11722
|
-
const fromDate = new Date(`${from}T00:00:00`);
|
|
11723
|
-
const toDate = new Date(`${to}T00:00:00`);
|
|
11724
|
-
const periodWeeks = Math.max(
|
|
11725
|
-
1,
|
|
11726
|
-
Math.ceil((toDate.getTime() - fromDate.getTime()) / 604800000)
|
|
11727
|
-
);
|
|
11728
12448
|
const rows = dbRows.map((row) => {
|
|
11729
|
-
const salaryCost = Number(row.salaryCost ?? 0);
|
|
11730
|
-
const benefitsCost = Number(row.benefitsCost ?? 0);
|
|
11731
|
-
const taxesCost = Number(row.taxesCost ?? 0);
|
|
11732
|
-
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;
|
|
11733
12453
|
const availableHours = Number(row.weeklyCapacityHours ?? 40) * periodWeeks;
|
|
11734
|
-
const
|
|
11735
|
-
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
|
+
);
|
|
11736
12470
|
const allocation = availableHours ? (allocatedHours / availableHours) * 100 : 0;
|
|
11737
12471
|
const risk = allocation >= 98 ? 'alto' : allocation < 75 ? 'médio' : 'baixo';
|
|
11738
12472
|
return {
|
|
@@ -11807,7 +12541,11 @@ export class OperationsService {
|
|
|
11807
12541
|
summary.freeHours = Math.max(summary.availableHours - summary.allocatedHours, 0);
|
|
11808
12542
|
summary.allocation = summary.availableHours ? (summary.allocatedHours / summary.availableHours) * 100 : 0;
|
|
11809
12543
|
summary.utilization = summary.availableHours ? (summary.billableHours / summary.availableHours) * 100 : 0;
|
|
11810
|
-
summary.hourlyCost = summary.allocatedHours
|
|
12544
|
+
summary.hourlyCost = summary.allocatedHours
|
|
12545
|
+
? summary.cost / summary.allocatedHours
|
|
12546
|
+
: summary.availableHours
|
|
12547
|
+
? summary.cost / summary.availableHours
|
|
12548
|
+
: 0;
|
|
11811
12549
|
|
|
11812
12550
|
const forecast = Array.from({ length: 12 }, (_, index) => {
|
|
11813
12551
|
const monthDate = new Date(fromDate);
|
|
@@ -12051,4 +12789,1623 @@ export class OperationsService {
|
|
|
12051
12789
|
|
|
12052
12790
|
return { success: true };
|
|
12053
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
|
+
}
|
|
12054
14411
|
}
|