@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
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { EmptyState, Page } from '@/components/entity-list';
|
|
4
|
+
import { RichTextEditor } from '@/components/rich-text-editor';
|
|
4
5
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
5
6
|
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
6
8
|
import {
|
|
7
9
|
ChartContainer,
|
|
8
10
|
ChartTooltip,
|
|
@@ -17,8 +19,8 @@ import {
|
|
|
17
19
|
DialogTitle,
|
|
18
20
|
} from '@/components/ui/dialog';
|
|
19
21
|
import { Input } from '@/components/ui/input';
|
|
20
|
-
import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
|
|
21
22
|
import { Label } from '@/components/ui/label';
|
|
23
|
+
import { Progress } from '@/components/ui/progress';
|
|
22
24
|
import {
|
|
23
25
|
Select,
|
|
24
26
|
SelectContent,
|
|
@@ -33,6 +35,7 @@ import {
|
|
|
33
35
|
SheetHeader,
|
|
34
36
|
SheetTitle,
|
|
35
37
|
} from '@/components/ui/sheet';
|
|
38
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
36
39
|
import {
|
|
37
40
|
Table,
|
|
38
41
|
TableBody,
|
|
@@ -41,43 +44,81 @@ import {
|
|
|
41
44
|
TableHeader,
|
|
42
45
|
TableRow,
|
|
43
46
|
} from '@/components/ui/table';
|
|
44
|
-
import {
|
|
47
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
48
|
+
import {
|
|
49
|
+
Tooltip,
|
|
50
|
+
TooltipContent,
|
|
51
|
+
TooltipTrigger,
|
|
52
|
+
} from '@/components/ui/tooltip';
|
|
45
53
|
import {
|
|
46
54
|
closestCenter,
|
|
47
55
|
DndContext,
|
|
56
|
+
DragOverlay,
|
|
48
57
|
PointerSensor,
|
|
58
|
+
pointerWithin,
|
|
49
59
|
useDraggable,
|
|
50
60
|
useDroppable,
|
|
51
61
|
useSensor,
|
|
52
62
|
useSensors,
|
|
63
|
+
type CollisionDetection,
|
|
53
64
|
type DragEndEvent,
|
|
65
|
+
type DragStartEvent,
|
|
54
66
|
type UniqueIdentifier,
|
|
55
67
|
} from '@dnd-kit/core';
|
|
56
68
|
import { CSS } from '@dnd-kit/utilities';
|
|
57
69
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
70
|
+
import { AnimatePresence, motion } from 'framer-motion';
|
|
58
71
|
import {
|
|
59
72
|
AlarmClock,
|
|
73
|
+
AlertTriangle,
|
|
60
74
|
Archive,
|
|
61
75
|
ArchiveRestore,
|
|
76
|
+
BarChart2,
|
|
62
77
|
BarChart3,
|
|
78
|
+
CalendarClock,
|
|
79
|
+
CalendarDays,
|
|
80
|
+
CheckCircle2,
|
|
81
|
+
ChevronRight,
|
|
82
|
+
ClipboardList,
|
|
63
83
|
FileText,
|
|
64
84
|
FolderKanban,
|
|
85
|
+
Gauge,
|
|
86
|
+
GitCommitHorizontal,
|
|
87
|
+
HeartPulse,
|
|
88
|
+
LineChart as LineChartIcon,
|
|
89
|
+
Loader2,
|
|
90
|
+
MessageSquare,
|
|
91
|
+
Paperclip,
|
|
65
92
|
Pencil,
|
|
66
93
|
Plus,
|
|
67
94
|
Rocket,
|
|
68
|
-
|
|
95
|
+
Search,
|
|
96
|
+
SlidersHorizontal,
|
|
97
|
+
Timer,
|
|
69
98
|
Trash2,
|
|
99
|
+
TrendingUp,
|
|
100
|
+
Users,
|
|
101
|
+
type LucideIcon,
|
|
70
102
|
} from 'lucide-react';
|
|
71
103
|
import { useTranslations } from 'next-intl';
|
|
72
104
|
import Link from 'next/link';
|
|
73
105
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
106
|
+
import type { ReactNode } from 'react';
|
|
74
107
|
import { useCallback, useMemo, useState } from 'react';
|
|
75
108
|
import {
|
|
109
|
+
Area,
|
|
110
|
+
AreaChart,
|
|
76
111
|
Bar,
|
|
77
112
|
BarChart,
|
|
78
113
|
CartesianGrid,
|
|
114
|
+
Cell,
|
|
79
115
|
Line,
|
|
80
116
|
LineChart,
|
|
117
|
+
Pie,
|
|
118
|
+
PieChart,
|
|
119
|
+
PolarAngleAxis,
|
|
120
|
+
RadialBar,
|
|
121
|
+
RadialBarChart,
|
|
81
122
|
XAxis,
|
|
82
123
|
YAxis,
|
|
83
124
|
} from 'recharts';
|
|
@@ -97,10 +138,12 @@ import {
|
|
|
97
138
|
getStatusBadgeClass,
|
|
98
139
|
} from '../_lib/utils/format';
|
|
99
140
|
import { OperationsHeader } from './operations-header';
|
|
141
|
+
import { ProjectCostsSection } from './project-costs-section';
|
|
100
142
|
import { ProjectFormScreen } from './project-form-screen';
|
|
101
143
|
import { SectionCard } from './section-card';
|
|
102
144
|
import { StatusBadge } from './status-badge';
|
|
103
145
|
import { TaskDetailSheet, type TaskDetailSheetData } from './task-detail-sheet';
|
|
146
|
+
import { TaskFileAttachments } from './task-file-attachments';
|
|
104
147
|
|
|
105
148
|
type BoardColumnId = 'todo' | 'doing' | 'review' | 'done';
|
|
106
149
|
|
|
@@ -118,6 +161,16 @@ type BoardTask = {
|
|
|
118
161
|
assigneeUserPhotoId: number | null;
|
|
119
162
|
assigneePersonAvatarId: number | null;
|
|
120
163
|
projectAssignmentId: number | null;
|
|
164
|
+
createdAt: string | null;
|
|
165
|
+
commentCount: number;
|
|
166
|
+
fileCount: number;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
type ApiBoardTask = Partial<BoardTask> & {
|
|
170
|
+
id: number;
|
|
171
|
+
name: string;
|
|
172
|
+
status?: string | null;
|
|
173
|
+
priority?: BoardTask['priority'] | null;
|
|
121
174
|
};
|
|
122
175
|
|
|
123
176
|
type TaskFormState = {
|
|
@@ -156,7 +209,15 @@ const KANBAN_COLUMNS: Array<{ id: BoardColumnId; label: string }> = [
|
|
|
156
209
|
{ id: 'done', label: 'Concluído' },
|
|
157
210
|
];
|
|
158
211
|
|
|
159
|
-
|
|
212
|
+
// Prefer pointer-within so any column the cursor enters triggers a drop target.
|
|
213
|
+
// Falls back to closestCenter for the gap between columns.
|
|
214
|
+
const kanbanCollision: CollisionDetection = (args) => {
|
|
215
|
+
const within = pointerWithin(args);
|
|
216
|
+
if (within.length > 0) return within;
|
|
217
|
+
return closestCenter(args);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
function apiTaskToBoardTask(row: ApiBoardTask): BoardTask {
|
|
160
221
|
const status = KANBAN_COLUMNS.some((c) => c.id === row.status)
|
|
161
222
|
? (row.status as BoardColumnId)
|
|
162
223
|
: 'todo';
|
|
@@ -174,6 +235,12 @@ function apiTaskToBoardTask(row: any): BoardTask {
|
|
|
174
235
|
assigneeUserPhotoId: row.assigneeUserPhotoId ?? null,
|
|
175
236
|
assigneePersonAvatarId: row.assigneePersonAvatarId ?? null,
|
|
176
237
|
projectAssignmentId: row.projectAssignmentId ?? null,
|
|
238
|
+
createdAt: row.createdAt ?? null,
|
|
239
|
+
commentCount:
|
|
240
|
+
((row as BoardTask & Record<string, unknown>).commentCount as number) ??
|
|
241
|
+
0,
|
|
242
|
+
fileCount:
|
|
243
|
+
((row as BoardTask & Record<string, unknown>).fileCount as number) ?? 0,
|
|
177
244
|
};
|
|
178
245
|
}
|
|
179
246
|
|
|
@@ -189,6 +256,13 @@ function splitTasksByColumn(tasks: BoardTask[]): BoardColumns {
|
|
|
189
256
|
const boardChartConfig = {
|
|
190
257
|
allocation: { label: 'Alocacao', color: 'hsl(201 96% 32%)' },
|
|
191
258
|
loggedHours: { label: 'Horas', color: 'hsl(166 72% 28%)' },
|
|
259
|
+
progress: { label: 'Progresso', color: 'hsl(262 83% 58%)' },
|
|
260
|
+
planned: { label: 'Planejado', color: 'hsl(215 16% 47%)' },
|
|
261
|
+
todo: { label: 'Backlog', color: 'hsl(215 16% 47%)' },
|
|
262
|
+
doing: { label: 'Em execucao', color: 'hsl(201 96% 32%)' },
|
|
263
|
+
review: { label: 'Revisao', color: 'hsl(38 92% 50%)' },
|
|
264
|
+
done: { label: 'Concluido', color: 'hsl(166 72% 28%)' },
|
|
265
|
+
health: { label: 'Saude', color: 'hsl(166 72% 28%)' },
|
|
192
266
|
} satisfies ChartConfig;
|
|
193
267
|
|
|
194
268
|
function taskDragId(taskId: number) {
|
|
@@ -248,24 +322,20 @@ function DraggableTaskCard({
|
|
|
248
322
|
}: {
|
|
249
323
|
task: BoardTask;
|
|
250
324
|
disabled?: boolean;
|
|
251
|
-
children: (isDragging: boolean) =>
|
|
325
|
+
children: (isDragging: boolean) => ReactNode;
|
|
252
326
|
}) {
|
|
253
|
-
if (disabled) {
|
|
254
|
-
return <div>{children(false)}</div>;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
327
|
const { attributes, listeners, setNodeRef, transform, isDragging } =
|
|
258
|
-
useDraggable({ id: taskDragId(task.id) });
|
|
328
|
+
useDraggable({ id: taskDragId(task.id), disabled });
|
|
259
329
|
|
|
260
330
|
return (
|
|
261
331
|
<div
|
|
262
332
|
ref={setNodeRef}
|
|
263
333
|
style={{ transform: CSS.Translate.toString(transform) }}
|
|
264
|
-
{...listeners}
|
|
265
|
-
{...attributes}
|
|
334
|
+
{...(disabled ? {} : listeners)}
|
|
335
|
+
{...(disabled ? {} : attributes)}
|
|
266
336
|
className={isDragging ? 'z-20' : undefined}
|
|
267
337
|
>
|
|
268
|
-
{children(isDragging)}
|
|
338
|
+
{children(disabled ? false : isDragging)}
|
|
269
339
|
</div>
|
|
270
340
|
);
|
|
271
341
|
}
|
|
@@ -324,6 +394,698 @@ function normalizeDateInputValue(value?: string | null) {
|
|
|
324
394
|
return parsedDate.toISOString().slice(0, 10);
|
|
325
395
|
}
|
|
326
396
|
|
|
397
|
+
function clampPercent(value?: number | null) {
|
|
398
|
+
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
399
|
+
return 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function isPastDue(value?: string | null) {
|
|
406
|
+
if (!value) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const date = new Date(value);
|
|
411
|
+
if (Number.isNaN(date.getTime())) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const today = new Date();
|
|
416
|
+
today.setHours(0, 0, 0, 0);
|
|
417
|
+
date.setHours(0, 0, 0, 0);
|
|
418
|
+
|
|
419
|
+
return date < today;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function getTaskProgress(status: BoardColumnId) {
|
|
423
|
+
const progressByStatus: Record<BoardColumnId, number> = {
|
|
424
|
+
todo: 12,
|
|
425
|
+
doing: 48,
|
|
426
|
+
review: 76,
|
|
427
|
+
done: 100,
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
return progressByStatus[status];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function getPriorityClassName(priority: BoardTask['priority']) {
|
|
434
|
+
if (priority === 'high') {
|
|
435
|
+
return 'border-rose-500/30 bg-rose-500/10 text-rose-700 dark:text-rose-300';
|
|
436
|
+
}
|
|
437
|
+
if (priority === 'medium') {
|
|
438
|
+
return 'border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300';
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300';
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function getColumnClassName(columnId: BoardColumnId) {
|
|
445
|
+
const styles: Record<BoardColumnId, string> = {
|
|
446
|
+
todo: 'from-slate-500/20 via-slate-500/5 to-transparent',
|
|
447
|
+
doing: 'from-sky-500/20 via-cyan-500/5 to-transparent',
|
|
448
|
+
review: 'from-amber-500/20 via-yellow-500/5 to-transparent',
|
|
449
|
+
done: 'from-emerald-500/20 via-green-500/5 to-transparent',
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
return styles[columnId];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function getColumnDotClassName(columnId: BoardColumnId) {
|
|
456
|
+
const styles: Record<BoardColumnId, string> = {
|
|
457
|
+
todo: 'bg-slate-500',
|
|
458
|
+
doing: 'bg-sky-500',
|
|
459
|
+
review: 'bg-amber-500',
|
|
460
|
+
done: 'bg-emerald-500',
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
return styles[columnId];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function getTaskTags(task: BoardTask) {
|
|
467
|
+
return String(task.tags ?? '')
|
|
468
|
+
.split(',')
|
|
469
|
+
.map((tag) => tag.trim())
|
|
470
|
+
.filter(Boolean);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function getTaskCommentCount(task: BoardTask) {
|
|
474
|
+
return task.commentCount ?? 0;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function getTaskAttachmentCount(task: BoardTask) {
|
|
478
|
+
return task.fileCount ?? 0;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function getAllocationTone(allocation: number) {
|
|
482
|
+
if (allocation > 100) {
|
|
483
|
+
return {
|
|
484
|
+
labelKey: 'overload',
|
|
485
|
+
text: 'text-rose-700 dark:text-rose-300',
|
|
486
|
+
border: 'border-rose-500/30',
|
|
487
|
+
bg: 'bg-rose-500/10',
|
|
488
|
+
progress: '[&>div]:bg-rose-500',
|
|
489
|
+
icon: AlertTriangle,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (allocation >= 85) {
|
|
494
|
+
return {
|
|
495
|
+
labelKey: 'high',
|
|
496
|
+
text: 'text-amber-700 dark:text-amber-300',
|
|
497
|
+
border: 'border-amber-500/30',
|
|
498
|
+
bg: 'bg-amber-500/10',
|
|
499
|
+
progress: '[&>div]:bg-amber-500',
|
|
500
|
+
icon: Gauge,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
labelKey: 'available',
|
|
506
|
+
text: 'text-emerald-700 dark:text-emerald-300',
|
|
507
|
+
border: 'border-emerald-500/30',
|
|
508
|
+
bg: 'bg-emerald-500/10',
|
|
509
|
+
progress: '[&>div]:bg-emerald-500',
|
|
510
|
+
icon: CheckCircle2,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
type TimelineEventType =
|
|
515
|
+
| 'task'
|
|
516
|
+
| 'timesheet'
|
|
517
|
+
| 'approval'
|
|
518
|
+
| 'comment'
|
|
519
|
+
| 'status';
|
|
520
|
+
|
|
521
|
+
type OperationalTimelineEvent = {
|
|
522
|
+
id: string;
|
|
523
|
+
type: TimelineEventType;
|
|
524
|
+
title: string;
|
|
525
|
+
description: string;
|
|
526
|
+
timestamp: string;
|
|
527
|
+
actorName?: string | null;
|
|
528
|
+
actorAvatarId?: number | null;
|
|
529
|
+
actorUserPhotoId?: number | null;
|
|
530
|
+
icon: LucideIcon;
|
|
531
|
+
toneClassName: string;
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
function getValidTimestamp(value?: string | null) {
|
|
535
|
+
if (!value) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const date = new Date(value);
|
|
540
|
+
if (Number.isNaN(date.getTime())) {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return date.toISOString();
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function formatRelativeTime(value: string, locale: string) {
|
|
548
|
+
const date = new Date(value);
|
|
549
|
+
if (Number.isNaN(date.getTime())) {
|
|
550
|
+
return '';
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const diffSeconds = Math.round((date.getTime() - Date.now()) / 1000);
|
|
554
|
+
const units: Array<[Intl.RelativeTimeFormatUnit, number]> = [
|
|
555
|
+
['year', 60 * 60 * 24 * 365],
|
|
556
|
+
['month', 60 * 60 * 24 * 30],
|
|
557
|
+
['day', 60 * 60 * 24],
|
|
558
|
+
['hour', 60 * 60],
|
|
559
|
+
['minute', 60],
|
|
560
|
+
];
|
|
561
|
+
const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
|
|
562
|
+
|
|
563
|
+
for (const [unit, seconds] of units) {
|
|
564
|
+
if (Math.abs(diffSeconds) >= seconds) {
|
|
565
|
+
return formatter.format(Math.round(diffSeconds / seconds), unit);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return formatter.format(diffSeconds, 'second');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function getTimelineDayKey(value: string) {
|
|
573
|
+
const date = new Date(value);
|
|
574
|
+
if (Number.isNaN(date.getTime())) {
|
|
575
|
+
return value;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return date.toISOString().slice(0, 10);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
type ProjectHealth = {
|
|
582
|
+
value: number;
|
|
583
|
+
labelKey: 'good' | 'warning' | 'danger';
|
|
584
|
+
tone: 'good' | 'warning' | 'danger';
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
function getProjectHealthScore({
|
|
588
|
+
progress,
|
|
589
|
+
averageAllocation,
|
|
590
|
+
overdueTasks,
|
|
591
|
+
pendingTimesheets,
|
|
592
|
+
}: {
|
|
593
|
+
progress?: number | null;
|
|
594
|
+
averageAllocation?: number | null;
|
|
595
|
+
overdueTasks: number;
|
|
596
|
+
pendingTimesheets: number;
|
|
597
|
+
}): ProjectHealth {
|
|
598
|
+
const progressScore = clampPercent(progress);
|
|
599
|
+
const allocation =
|
|
600
|
+
typeof averageAllocation === 'number' && !Number.isNaN(averageAllocation)
|
|
601
|
+
? Math.round(averageAllocation)
|
|
602
|
+
: 0;
|
|
603
|
+
const allocationPenalty = allocation > 100 ? 16 : allocation > 85 ? 6 : 0;
|
|
604
|
+
const overduePenalty = Math.min(overdueTasks * 9, 32);
|
|
605
|
+
const timesheetPenalty = Math.min(pendingTimesheets * 4, 20);
|
|
606
|
+
const value = clampPercent(
|
|
607
|
+
72 +
|
|
608
|
+
progressScore * 0.18 -
|
|
609
|
+
allocationPenalty -
|
|
610
|
+
overduePenalty -
|
|
611
|
+
timesheetPenalty
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
if (value >= 75) {
|
|
615
|
+
return { value, labelKey: 'good', tone: 'good' };
|
|
616
|
+
}
|
|
617
|
+
if (value >= 50) {
|
|
618
|
+
return { value, labelKey: 'warning', tone: 'warning' };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return { value, labelKey: 'danger', tone: 'danger' };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
type ProjectKpiTone = 'positive' | 'warning' | 'critical' | 'info' | 'neutral';
|
|
625
|
+
|
|
626
|
+
type ProjectKpiWidgetItem = {
|
|
627
|
+
key: string;
|
|
628
|
+
title: ReactNode;
|
|
629
|
+
value: ReactNode;
|
|
630
|
+
subtitle: ReactNode;
|
|
631
|
+
trend: ReactNode;
|
|
632
|
+
indicator: number;
|
|
633
|
+
icon: LucideIcon;
|
|
634
|
+
tone: ProjectKpiTone;
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const kpiToneStyles: Record<
|
|
638
|
+
ProjectKpiTone,
|
|
639
|
+
{
|
|
640
|
+
accent: string;
|
|
641
|
+
icon: string;
|
|
642
|
+
value: string;
|
|
643
|
+
indicator: string;
|
|
644
|
+
trend: string;
|
|
645
|
+
}
|
|
646
|
+
> = {
|
|
647
|
+
positive: {
|
|
648
|
+
accent: 'from-emerald-500/25 via-teal-500/10 to-transparent',
|
|
649
|
+
icon: 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-300',
|
|
650
|
+
value: 'text-emerald-700 dark:text-emerald-300',
|
|
651
|
+
indicator: 'bg-emerald-500',
|
|
652
|
+
trend: 'text-emerald-700 dark:text-emerald-300',
|
|
653
|
+
},
|
|
654
|
+
warning: {
|
|
655
|
+
accent: 'from-amber-500/25 via-yellow-500/10 to-transparent',
|
|
656
|
+
icon: 'bg-amber-500/10 text-amber-700 dark:text-amber-300',
|
|
657
|
+
value: 'text-amber-700 dark:text-amber-300',
|
|
658
|
+
indicator: 'bg-amber-500',
|
|
659
|
+
trend: 'text-amber-700 dark:text-amber-300',
|
|
660
|
+
},
|
|
661
|
+
critical: {
|
|
662
|
+
accent: 'from-rose-500/25 via-red-500/10 to-transparent',
|
|
663
|
+
icon: 'bg-rose-500/10 text-rose-700 dark:text-rose-300',
|
|
664
|
+
value: 'text-rose-700 dark:text-rose-300',
|
|
665
|
+
indicator: 'bg-rose-500',
|
|
666
|
+
trend: 'text-rose-700 dark:text-rose-300',
|
|
667
|
+
},
|
|
668
|
+
info: {
|
|
669
|
+
accent: 'from-sky-500/25 via-cyan-500/10 to-transparent',
|
|
670
|
+
icon: 'bg-sky-500/10 text-sky-700 dark:text-sky-300',
|
|
671
|
+
value: 'text-sky-700 dark:text-sky-300',
|
|
672
|
+
indicator: 'bg-sky-500',
|
|
673
|
+
trend: 'text-sky-700 dark:text-sky-300',
|
|
674
|
+
},
|
|
675
|
+
neutral: {
|
|
676
|
+
accent: 'from-violet-500/25 via-indigo-500/10 to-transparent',
|
|
677
|
+
icon: 'bg-violet-500/10 text-violet-700 dark:text-violet-300',
|
|
678
|
+
value: 'text-foreground',
|
|
679
|
+
indicator: 'bg-violet-500',
|
|
680
|
+
trend: 'text-muted-foreground',
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
function ProjectKpiWidget({
|
|
685
|
+
item,
|
|
686
|
+
indicatorLabel,
|
|
687
|
+
index = 0,
|
|
688
|
+
}: {
|
|
689
|
+
item: ProjectKpiWidgetItem;
|
|
690
|
+
indicatorLabel: ReactNode;
|
|
691
|
+
index?: number;
|
|
692
|
+
}) {
|
|
693
|
+
const Icon = item.icon;
|
|
694
|
+
const tone = kpiToneStyles[item.tone];
|
|
695
|
+
|
|
696
|
+
return (
|
|
697
|
+
<motion.div
|
|
698
|
+
initial={{ opacity: 0, y: 12 }}
|
|
699
|
+
animate={{ opacity: 1, y: 0 }}
|
|
700
|
+
transition={{
|
|
701
|
+
type: 'spring',
|
|
702
|
+
stiffness: 340,
|
|
703
|
+
damping: 26,
|
|
704
|
+
delay: index * 0.07,
|
|
705
|
+
}}
|
|
706
|
+
whileHover={{ y: -4 }}
|
|
707
|
+
className="min-w-0"
|
|
708
|
+
>
|
|
709
|
+
<Card className="group relative h-full overflow-hidden border-border/70 bg-card py-0 shadow-xs transition-shadow hover:shadow-md">
|
|
710
|
+
<div
|
|
711
|
+
className={[
|
|
712
|
+
'absolute inset-x-0 top-0 h-20 bg-linear-to-br',
|
|
713
|
+
tone.accent,
|
|
714
|
+
].join(' ')}
|
|
715
|
+
/>
|
|
716
|
+
<CardContent className="relative flex h-full flex-col gap-5 p-4">
|
|
717
|
+
<div className="flex items-start justify-between gap-3">
|
|
718
|
+
<div
|
|
719
|
+
className={[
|
|
720
|
+
'flex size-10 items-center justify-center rounded-2xl transition-transform group-hover:scale-105',
|
|
721
|
+
tone.icon,
|
|
722
|
+
].join(' ')}
|
|
723
|
+
>
|
|
724
|
+
<Icon className="size-5" />
|
|
725
|
+
</div>
|
|
726
|
+
<span
|
|
727
|
+
className={[
|
|
728
|
+
'rounded-full border bg-background/80 px-2 py-0.5 text-[11px] font-medium',
|
|
729
|
+
tone.trend,
|
|
730
|
+
].join(' ')}
|
|
731
|
+
>
|
|
732
|
+
{item.trend}
|
|
733
|
+
</span>
|
|
734
|
+
</div>
|
|
735
|
+
|
|
736
|
+
<div className="min-w-0">
|
|
737
|
+
<div
|
|
738
|
+
className={[
|
|
739
|
+
'truncate text-3xl font-semibold tracking-tight tabular-nums',
|
|
740
|
+
tone.value,
|
|
741
|
+
].join(' ')}
|
|
742
|
+
>
|
|
743
|
+
{item.value}
|
|
744
|
+
</div>
|
|
745
|
+
<div className="mt-1 text-sm font-medium text-foreground">
|
|
746
|
+
{item.title}
|
|
747
|
+
</div>
|
|
748
|
+
<div className="mt-1 line-clamp-2 text-xs leading-5 text-muted-foreground">
|
|
749
|
+
{item.subtitle}
|
|
750
|
+
</div>
|
|
751
|
+
</div>
|
|
752
|
+
|
|
753
|
+
<div className="mt-auto space-y-2">
|
|
754
|
+
<div className="flex items-center justify-between text-[11px] text-muted-foreground">
|
|
755
|
+
<span>{indicatorLabel}</span>
|
|
756
|
+
<span>{clampPercent(item.indicator)}%</span>
|
|
757
|
+
</div>
|
|
758
|
+
<div className="h-1.5 overflow-hidden rounded-full bg-muted">
|
|
759
|
+
<div
|
|
760
|
+
className={[
|
|
761
|
+
'h-full rounded-full transition-all duration-500',
|
|
762
|
+
tone.indicator,
|
|
763
|
+
].join(' ')}
|
|
764
|
+
style={{ width: `${clampPercent(item.indicator)}%` }}
|
|
765
|
+
/>
|
|
766
|
+
</div>
|
|
767
|
+
</div>
|
|
768
|
+
</CardContent>
|
|
769
|
+
</Card>
|
|
770
|
+
</motion.div>
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function ChartEmptyState({
|
|
775
|
+
icon: Icon,
|
|
776
|
+
title,
|
|
777
|
+
description,
|
|
778
|
+
}: {
|
|
779
|
+
icon: LucideIcon;
|
|
780
|
+
title: ReactNode;
|
|
781
|
+
description: ReactNode;
|
|
782
|
+
}) {
|
|
783
|
+
return (
|
|
784
|
+
<div className="flex h-72 flex-col items-center justify-center rounded-xl border border-dashed bg-muted/10 p-6 text-center">
|
|
785
|
+
<div className="flex size-11 items-center justify-center rounded-2xl bg-muted text-muted-foreground">
|
|
786
|
+
<Icon className="size-5" />
|
|
787
|
+
</div>
|
|
788
|
+
<div className="mt-3 text-sm font-medium">{title}</div>
|
|
789
|
+
<div className="mt-1 max-w-xs text-xs leading-5 text-muted-foreground">
|
|
790
|
+
{description}
|
|
791
|
+
</div>
|
|
792
|
+
</div>
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function ProjectChartCard({
|
|
797
|
+
title,
|
|
798
|
+
description,
|
|
799
|
+
icon: Icon,
|
|
800
|
+
metric,
|
|
801
|
+
children,
|
|
802
|
+
className,
|
|
803
|
+
isLoading = false,
|
|
804
|
+
}: {
|
|
805
|
+
title: ReactNode;
|
|
806
|
+
description?: ReactNode;
|
|
807
|
+
icon: LucideIcon;
|
|
808
|
+
metric?: ReactNode;
|
|
809
|
+
children: ReactNode;
|
|
810
|
+
className?: string;
|
|
811
|
+
isLoading?: boolean;
|
|
812
|
+
}) {
|
|
813
|
+
return (
|
|
814
|
+
<motion.div
|
|
815
|
+
initial={{ opacity: 0, y: 8 }}
|
|
816
|
+
animate={{ opacity: 1, y: 0 }}
|
|
817
|
+
transition={{ duration: 0.22 }}
|
|
818
|
+
className={className}
|
|
819
|
+
>
|
|
820
|
+
<Card className="h-full overflow-hidden border-border/70 bg-card py-0 shadow-sm">
|
|
821
|
+
<CardContent className="flex h-full flex-col gap-4 p-4">
|
|
822
|
+
<div className="flex items-start justify-between gap-4">
|
|
823
|
+
<div className="flex min-w-0 items-start gap-3">
|
|
824
|
+
<div className="flex size-10 shrink-0 items-center justify-center rounded-2xl bg-muted text-foreground">
|
|
825
|
+
<Icon className="size-5" />
|
|
826
|
+
</div>
|
|
827
|
+
<div className="min-w-0">
|
|
828
|
+
<div className="text-sm font-semibold">{title}</div>
|
|
829
|
+
{description ? (
|
|
830
|
+
<div className="mt-1 text-xs leading-5 text-muted-foreground">
|
|
831
|
+
{description}
|
|
832
|
+
</div>
|
|
833
|
+
) : null}
|
|
834
|
+
</div>
|
|
835
|
+
</div>
|
|
836
|
+
{metric ? (
|
|
837
|
+
<div className="shrink-0 rounded-full border bg-background px-3 py-1 text-xs font-medium text-muted-foreground">
|
|
838
|
+
{metric}
|
|
839
|
+
</div>
|
|
840
|
+
) : null}
|
|
841
|
+
</div>
|
|
842
|
+
{isLoading ? <Skeleton className="h-72 rounded-xl" /> : children}
|
|
843
|
+
</CardContent>
|
|
844
|
+
</Card>
|
|
845
|
+
</motion.div>
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function ProjectDetailsSkeleton() {
|
|
850
|
+
return (
|
|
851
|
+
<Page>
|
|
852
|
+
<div className="space-y-6">
|
|
853
|
+
{/* Hero header */}
|
|
854
|
+
<div className="overflow-hidden rounded-3xl border bg-card shadow-sm">
|
|
855
|
+
<div className="border-b p-5 sm:p-6">
|
|
856
|
+
<div className="space-y-4">
|
|
857
|
+
{/* Breadcrumb */}
|
|
858
|
+
<div className="flex items-center gap-2">
|
|
859
|
+
<Skeleton className="h-4 w-20 rounded-full" />
|
|
860
|
+
<Skeleton className="h-3 w-3 rounded-full" />
|
|
861
|
+
<Skeleton className="h-4 w-16 rounded-full" />
|
|
862
|
+
<Skeleton className="h-3 w-3 rounded-full" />
|
|
863
|
+
<Skeleton className="h-4 w-32 rounded-full" />
|
|
864
|
+
</div>
|
|
865
|
+
{/* Title row */}
|
|
866
|
+
<div className="flex items-start gap-4">
|
|
867
|
+
<Skeleton className="size-14 shrink-0 rounded-2xl" />
|
|
868
|
+
<div className="flex-1 space-y-2">
|
|
869
|
+
<div className="flex gap-2">
|
|
870
|
+
<Skeleton className="h-6 w-20 rounded-full" />
|
|
871
|
+
<Skeleton className="h-6 w-16 rounded-full" />
|
|
872
|
+
</div>
|
|
873
|
+
<Skeleton className="h-8 w-72" />
|
|
874
|
+
<Skeleton className="h-4 w-full max-w-md" />
|
|
875
|
+
</div>
|
|
876
|
+
<div className="hidden flex-shrink-0 gap-2 lg:flex">
|
|
877
|
+
<Skeleton className="h-9 w-20 rounded-lg" />
|
|
878
|
+
<Skeleton className="h-9 w-28 rounded-lg" />
|
|
879
|
+
<Skeleton className="h-9 w-9 rounded-lg" />
|
|
880
|
+
</div>
|
|
881
|
+
</div>
|
|
882
|
+
{/* Meta grid */}
|
|
883
|
+
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-7">
|
|
884
|
+
<Skeleton className="h-16 rounded-xl xl:col-span-2" />
|
|
885
|
+
<Skeleton className="h-16 rounded-xl" />
|
|
886
|
+
<Skeleton className="h-16 rounded-xl" />
|
|
887
|
+
<Skeleton className="h-16 rounded-xl" />
|
|
888
|
+
<Skeleton className="h-16 rounded-xl" />
|
|
889
|
+
<Skeleton className="h-16 rounded-xl" />
|
|
890
|
+
</div>
|
|
891
|
+
{/* Team row */}
|
|
892
|
+
<div className="rounded-2xl border p-4">
|
|
893
|
+
<div className="flex items-center justify-between gap-4">
|
|
894
|
+
<div className="flex items-center gap-3">
|
|
895
|
+
<div className="space-y-1">
|
|
896
|
+
<Skeleton className="h-3 w-12" />
|
|
897
|
+
<Skeleton className="h-5 w-24" />
|
|
898
|
+
</div>
|
|
899
|
+
<div className="flex -space-x-2">
|
|
900
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
901
|
+
<Skeleton
|
|
902
|
+
key={i}
|
|
903
|
+
className="size-10 rounded-full border-2 border-background"
|
|
904
|
+
/>
|
|
905
|
+
))}
|
|
906
|
+
</div>
|
|
907
|
+
</div>
|
|
908
|
+
<div className="grid grid-cols-3 gap-3">
|
|
909
|
+
<Skeleton className="h-16 w-28 rounded-xl" />
|
|
910
|
+
<Skeleton className="h-16 w-28 rounded-xl" />
|
|
911
|
+
<Skeleton className="h-16 w-28 rounded-xl" />
|
|
912
|
+
</div>
|
|
913
|
+
</div>
|
|
914
|
+
</div>
|
|
915
|
+
</div>
|
|
916
|
+
</div>
|
|
917
|
+
</div>
|
|
918
|
+
|
|
919
|
+
{/* KPI row */}
|
|
920
|
+
<div className="rounded-3xl border p-3 sm:p-4">
|
|
921
|
+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-6">
|
|
922
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
923
|
+
<div key={i} className="overflow-hidden rounded-xl border p-4">
|
|
924
|
+
<div className="flex items-start justify-between gap-3">
|
|
925
|
+
<Skeleton className="size-10 rounded-2xl" />
|
|
926
|
+
<Skeleton className="h-5 w-16 rounded-full" />
|
|
927
|
+
</div>
|
|
928
|
+
<div className="mt-5 space-y-2">
|
|
929
|
+
<Skeleton className="h-8 w-24" />
|
|
930
|
+
<Skeleton className="h-4 w-32" />
|
|
931
|
+
<Skeleton className="h-3 w-full" />
|
|
932
|
+
</div>
|
|
933
|
+
<div className="mt-auto pt-4 space-y-2">
|
|
934
|
+
<div className="flex justify-between">
|
|
935
|
+
<Skeleton className="h-3 w-16" />
|
|
936
|
+
<Skeleton className="h-3 w-8" />
|
|
937
|
+
</div>
|
|
938
|
+
<Skeleton className="h-1.5 w-full rounded-full" />
|
|
939
|
+
</div>
|
|
940
|
+
</div>
|
|
941
|
+
))}
|
|
942
|
+
</div>
|
|
943
|
+
</div>
|
|
944
|
+
|
|
945
|
+
{/* Overview */}
|
|
946
|
+
<div className="rounded-xl border p-4">
|
|
947
|
+
<Skeleton className="mb-4 h-5 w-28" />
|
|
948
|
+
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
949
|
+
{Array.from({ length: 9 }).map((_, i) => (
|
|
950
|
+
<div key={i} className="space-y-1.5">
|
|
951
|
+
<Skeleton className="h-3 w-20" />
|
|
952
|
+
<Skeleton className="h-5 w-32" />
|
|
953
|
+
</div>
|
|
954
|
+
))}
|
|
955
|
+
</div>
|
|
956
|
+
<div className="mt-6 space-y-1.5">
|
|
957
|
+
<Skeleton className="h-3 w-16" />
|
|
958
|
+
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
959
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
960
|
+
<div key={i} className="space-y-1.5">
|
|
961
|
+
<Skeleton className="h-3 w-24" />
|
|
962
|
+
<Skeleton className="h-5 w-36" />
|
|
963
|
+
</div>
|
|
964
|
+
))}
|
|
965
|
+
</div>
|
|
966
|
+
</div>
|
|
967
|
+
</div>
|
|
968
|
+
|
|
969
|
+
{/* Charts */}
|
|
970
|
+
<div className="rounded-3xl border p-4">
|
|
971
|
+
<Skeleton className="mb-4 h-5 w-36" />
|
|
972
|
+
<div className="grid gap-4 xl:grid-cols-12">
|
|
973
|
+
<Skeleton className="h-80 rounded-xl xl:col-span-8" />
|
|
974
|
+
<Skeleton className="h-80 rounded-xl xl:col-span-4" />
|
|
975
|
+
<Skeleton className="h-72 rounded-xl xl:col-span-5" />
|
|
976
|
+
<Skeleton className="h-72 rounded-xl xl:col-span-4" />
|
|
977
|
+
<Skeleton className="h-72 rounded-xl xl:col-span-3" />
|
|
978
|
+
</div>
|
|
979
|
+
</div>
|
|
980
|
+
|
|
981
|
+
{/* Kanban */}
|
|
982
|
+
<div className="rounded-3xl border p-4">
|
|
983
|
+
<div className="mb-4 flex items-center justify-between">
|
|
984
|
+
<Skeleton className="h-5 w-28" />
|
|
985
|
+
<Skeleton className="h-9 w-28 rounded-lg" />
|
|
986
|
+
</div>
|
|
987
|
+
<div className="mb-4 rounded-2xl border p-3">
|
|
988
|
+
<div className="flex gap-3">
|
|
989
|
+
<Skeleton className="h-10 flex-1 rounded-lg" />
|
|
990
|
+
<Skeleton className="h-10 w-44 rounded-lg" />
|
|
991
|
+
<Skeleton className="h-10 w-44 rounded-lg" />
|
|
992
|
+
</div>
|
|
993
|
+
</div>
|
|
994
|
+
<div className="grid gap-4 xl:grid-cols-4">
|
|
995
|
+
{Array.from({ length: 4 }).map((_, col) => (
|
|
996
|
+
<div
|
|
997
|
+
key={col}
|
|
998
|
+
className="min-h-48 rounded-3xl border p-3 space-y-3"
|
|
999
|
+
>
|
|
1000
|
+
<div className="rounded-2xl border bg-background/85 p-3 flex items-center justify-between">
|
|
1001
|
+
<div className="space-y-1">
|
|
1002
|
+
<div className="flex items-center gap-2">
|
|
1003
|
+
<Skeleton className="size-2.5 rounded-full" />
|
|
1004
|
+
<Skeleton className="h-4 w-20" />
|
|
1005
|
+
</div>
|
|
1006
|
+
<Skeleton className="h-3 w-12" />
|
|
1007
|
+
</div>
|
|
1008
|
+
<Skeleton className="size-5 rounded-full" />
|
|
1009
|
+
</div>
|
|
1010
|
+
{Array.from({ length: col === 0 ? 3 : col === 1 ? 2 : 1 }).map(
|
|
1011
|
+
(_, card) => (
|
|
1012
|
+
<div
|
|
1013
|
+
key={card}
|
|
1014
|
+
className="rounded-2xl border bg-card p-3 space-y-3"
|
|
1015
|
+
>
|
|
1016
|
+
<div className="flex items-start justify-between gap-2">
|
|
1017
|
+
<div className="flex-1 space-y-1">
|
|
1018
|
+
<Skeleton className="h-4 w-full" />
|
|
1019
|
+
<Skeleton className="h-3 w-3/4" />
|
|
1020
|
+
</div>
|
|
1021
|
+
<Skeleton className="h-5 w-12 rounded-full" />
|
|
1022
|
+
</div>
|
|
1023
|
+
<div className="grid grid-cols-2 gap-2">
|
|
1024
|
+
<Skeleton className="h-8 rounded-xl" />
|
|
1025
|
+
<Skeleton className="h-8 rounded-xl" />
|
|
1026
|
+
</div>
|
|
1027
|
+
<div className="space-y-1.5">
|
|
1028
|
+
<Skeleton className="h-1.5 w-full rounded-full" />
|
|
1029
|
+
</div>
|
|
1030
|
+
<div className="flex items-center justify-between border-t pt-3">
|
|
1031
|
+
<div className="flex items-center gap-2">
|
|
1032
|
+
<Skeleton className="size-7 rounded-full" />
|
|
1033
|
+
<Skeleton className="h-3 w-20" />
|
|
1034
|
+
</div>
|
|
1035
|
+
<div className="flex gap-2">
|
|
1036
|
+
<Skeleton className="h-3 w-6" />
|
|
1037
|
+
<Skeleton className="h-3 w-6" />
|
|
1038
|
+
</div>
|
|
1039
|
+
</div>
|
|
1040
|
+
</div>
|
|
1041
|
+
)
|
|
1042
|
+
)}
|
|
1043
|
+
</div>
|
|
1044
|
+
))}
|
|
1045
|
+
</div>
|
|
1046
|
+
</div>
|
|
1047
|
+
|
|
1048
|
+
{/* Timeline */}
|
|
1049
|
+
<div className="rounded-3xl border p-4">
|
|
1050
|
+
<Skeleton className="mb-4 h-5 w-24" />
|
|
1051
|
+
<div className="rounded-3xl border p-4 space-y-6">
|
|
1052
|
+
{Array.from({ length: 3 }).map((_, group) => (
|
|
1053
|
+
<div key={group} className="space-y-3">
|
|
1054
|
+
<Skeleton className="h-6 w-28 rounded-full" />
|
|
1055
|
+
{Array.from({ length: 2 }).map((_, event) => (
|
|
1056
|
+
<div key={event} className="grid grid-cols-[2rem_1fr] gap-3">
|
|
1057
|
+
<div className="flex flex-col items-center">
|
|
1058
|
+
<Skeleton className="size-8 rounded-full" />
|
|
1059
|
+
{event === 0 ? (
|
|
1060
|
+
<div className="w-px flex-1 bg-border mt-1" />
|
|
1061
|
+
) : null}
|
|
1062
|
+
</div>
|
|
1063
|
+
<div className="pb-5">
|
|
1064
|
+
<div className="rounded-2xl border p-4 space-y-3">
|
|
1065
|
+
<div className="flex justify-between">
|
|
1066
|
+
<div className="space-y-1.5">
|
|
1067
|
+
<Skeleton className="h-4 w-40" />
|
|
1068
|
+
<Skeleton className="h-3 w-56" />
|
|
1069
|
+
</div>
|
|
1070
|
+
<Skeleton className="h-3 w-16" />
|
|
1071
|
+
</div>
|
|
1072
|
+
<div className="flex items-center gap-2 border-t pt-3">
|
|
1073
|
+
<Skeleton className="size-7 rounded-full" />
|
|
1074
|
+
<Skeleton className="h-3 w-24" />
|
|
1075
|
+
</div>
|
|
1076
|
+
</div>
|
|
1077
|
+
</div>
|
|
1078
|
+
</div>
|
|
1079
|
+
))}
|
|
1080
|
+
</div>
|
|
1081
|
+
))}
|
|
1082
|
+
</div>
|
|
1083
|
+
</div>
|
|
1084
|
+
</div>
|
|
1085
|
+
</Page>
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
327
1089
|
export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
328
1090
|
const t = useTranslations('operations.ProjectDetailsPage');
|
|
329
1091
|
const commonT = useTranslations('operations.Common');
|
|
@@ -407,6 +1169,14 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
407
1169
|
updateSheetQuery(false);
|
|
408
1170
|
};
|
|
409
1171
|
|
|
1172
|
+
const activeTab = searchParams.get('tab') ?? 'tasks';
|
|
1173
|
+
|
|
1174
|
+
const setActiveTab = (tab: string) => {
|
|
1175
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
1176
|
+
params.set('tab', tab);
|
|
1177
|
+
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
|
|
1178
|
+
};
|
|
1179
|
+
|
|
410
1180
|
const getDeliveryModelLabel = (value?: string | null) => {
|
|
411
1181
|
if (!value) {
|
|
412
1182
|
return commonT('labels.notAvailable');
|
|
@@ -439,7 +1209,11 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
439
1209
|
return labels[value as keyof typeof labels] ?? formatEnumLabel(value);
|
|
440
1210
|
};
|
|
441
1211
|
|
|
442
|
-
const {
|
|
1212
|
+
const {
|
|
1213
|
+
data: project,
|
|
1214
|
+
refetch,
|
|
1215
|
+
isLoading: isProjectLoading,
|
|
1216
|
+
} = useQuery<OperationsProjectDetails>({
|
|
443
1217
|
queryKey: ['operations-project-details', currentLocaleCode, projectId],
|
|
444
1218
|
queryFn: () =>
|
|
445
1219
|
fetchOperations<OperationsProjectDetails>(
|
|
@@ -448,10 +1222,14 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
448
1222
|
),
|
|
449
1223
|
});
|
|
450
1224
|
|
|
451
|
-
const {
|
|
1225
|
+
const {
|
|
1226
|
+
data: rawTasks = [],
|
|
1227
|
+
refetch: refetchTasks,
|
|
1228
|
+
isLoading: isTasksLoading,
|
|
1229
|
+
} = useQuery<ApiBoardTask[]>({
|
|
452
1230
|
queryKey: ['operations-project-board-tasks', projectId],
|
|
453
1231
|
queryFn: () =>
|
|
454
|
-
fetchOperations<
|
|
1232
|
+
fetchOperations<ApiBoardTask[]>(
|
|
455
1233
|
request,
|
|
456
1234
|
`/operations/projects/${projectId}/tasks`
|
|
457
1235
|
),
|
|
@@ -469,7 +1247,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
469
1247
|
enabled: Boolean(project),
|
|
470
1248
|
});
|
|
471
1249
|
|
|
472
|
-
const { data: projectStats } = useQuery<{
|
|
1250
|
+
const { data: projectStats, isLoading: isProjectStatsLoading } = useQuery<{
|
|
473
1251
|
weeklyVelocity: Array<{ weekLabel: string; loggedHours: number }>;
|
|
474
1252
|
allocationByCollaborator: Array<{ name: string; allocation: number }>;
|
|
475
1253
|
quickRadar: {
|
|
@@ -495,6 +1273,23 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
495
1273
|
const [taskFormLoading, setTaskFormLoading] = useState(false);
|
|
496
1274
|
const [deletePromptTask, setDeletePromptTask] =
|
|
497
1275
|
useState<TaskDetailSheetData | null>(null);
|
|
1276
|
+
const [inlineCreateColumn, setInlineCreateColumn] =
|
|
1277
|
+
useState<BoardColumnId | null>(null);
|
|
1278
|
+
const [inlineCreateName, setInlineCreateName] = useState('');
|
|
1279
|
+
const [inlineCreateLoading, setInlineCreateLoading] = useState(false);
|
|
1280
|
+
const [boardSearch, setBoardSearch] = useState('');
|
|
1281
|
+
const [boardPriorityFilter, setBoardPriorityFilter] = useState<
|
|
1282
|
+
'all' | BoardTask['priority']
|
|
1283
|
+
>('all');
|
|
1284
|
+
const [boardGroupMode, setBoardGroupMode] = useState('status');
|
|
1285
|
+
const [timelineTypeFilter, setTimelineTypeFilter] = useState<
|
|
1286
|
+
'all' | TimelineEventType
|
|
1287
|
+
>('all');
|
|
1288
|
+
const [timelineVisibleCount, setTimelineVisibleCount] = useState(8);
|
|
1289
|
+
const [archivingTaskId, setArchivingTaskId] = useState<number | null>(null);
|
|
1290
|
+
const [restoringTaskId, setRestoringTaskId] = useState<number | null>(null);
|
|
1291
|
+
const [deletingTaskId, setDeletingTaskId] = useState<number | null>(null);
|
|
1292
|
+
const [activeDragTask, setActiveDragTask] = useState<BoardTask | null>(null);
|
|
498
1293
|
|
|
499
1294
|
const apiTasks = useMemo(() => rawTasks.map(apiTaskToBoardTask), [rawTasks]);
|
|
500
1295
|
const archivedTasks = useMemo(
|
|
@@ -512,6 +1307,36 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
512
1307
|
return splitTasksByColumn(apiTasks);
|
|
513
1308
|
}, [project, boardState, apiTasks]);
|
|
514
1309
|
|
|
1310
|
+
const filteredTaskColumns: BoardColumns = useMemo(() => {
|
|
1311
|
+
const normalizedSearch = boardSearch.trim().toLocaleLowerCase();
|
|
1312
|
+
const filterTask = (task: BoardTask) => {
|
|
1313
|
+
const matchesSearch =
|
|
1314
|
+
!normalizedSearch ||
|
|
1315
|
+
[
|
|
1316
|
+
task.name,
|
|
1317
|
+
task.description,
|
|
1318
|
+
task.assigneeName,
|
|
1319
|
+
task.tags,
|
|
1320
|
+
task.priority,
|
|
1321
|
+
]
|
|
1322
|
+
.filter(Boolean)
|
|
1323
|
+
.some((value) =>
|
|
1324
|
+
String(value).toLocaleLowerCase().includes(normalizedSearch)
|
|
1325
|
+
);
|
|
1326
|
+
const matchesPriority =
|
|
1327
|
+
boardPriorityFilter === 'all' || task.priority === boardPriorityFilter;
|
|
1328
|
+
|
|
1329
|
+
return matchesSearch && matchesPriority;
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
return {
|
|
1333
|
+
todo: taskColumns.todo.filter(filterTask),
|
|
1334
|
+
doing: taskColumns.doing.filter(filterTask),
|
|
1335
|
+
review: taskColumns.review.filter(filterTask),
|
|
1336
|
+
done: taskColumns.done.filter(filterTask),
|
|
1337
|
+
};
|
|
1338
|
+
}, [boardPriorityFilter, boardSearch, taskColumns]);
|
|
1339
|
+
|
|
515
1340
|
const taskAssigneeOptions = useMemo(() => {
|
|
516
1341
|
const seen = new Set<number>();
|
|
517
1342
|
return (
|
|
@@ -566,7 +1391,6 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
566
1391
|
setTaskFormLoading(true);
|
|
567
1392
|
try {
|
|
568
1393
|
const payload: Record<string, unknown> = {
|
|
569
|
-
projectId,
|
|
570
1394
|
name: taskFormData.name.trim(),
|
|
571
1395
|
description: taskFormData.description || null,
|
|
572
1396
|
priority: taskFormData.priority,
|
|
@@ -589,7 +1413,10 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
589
1413
|
payload
|
|
590
1414
|
);
|
|
591
1415
|
} else {
|
|
592
|
-
await mutateOperations(request, '/operations/tasks', 'POST',
|
|
1416
|
+
await mutateOperations(request, '/operations/tasks', 'POST', {
|
|
1417
|
+
projectId,
|
|
1418
|
+
...payload,
|
|
1419
|
+
});
|
|
593
1420
|
}
|
|
594
1421
|
setBoardState(null);
|
|
595
1422
|
await refetchTasks();
|
|
@@ -611,6 +1438,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
611
1438
|
|
|
612
1439
|
const handleArchiveTask = useCallback(
|
|
613
1440
|
async (taskId: number) => {
|
|
1441
|
+
setArchivingTaskId(taskId);
|
|
614
1442
|
try {
|
|
615
1443
|
await mutateOperations(
|
|
616
1444
|
request,
|
|
@@ -626,6 +1454,8 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
626
1454
|
await refetchArchivedTasks();
|
|
627
1455
|
} catch {
|
|
628
1456
|
// ignore
|
|
1457
|
+
} finally {
|
|
1458
|
+
setArchivingTaskId(null);
|
|
629
1459
|
}
|
|
630
1460
|
},
|
|
631
1461
|
[request, refetchTasks, refetchArchivedTasks]
|
|
@@ -633,6 +1463,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
633
1463
|
|
|
634
1464
|
const handleRestoreTask = useCallback(
|
|
635
1465
|
async (taskId: number) => {
|
|
1466
|
+
setRestoringTaskId(taskId);
|
|
636
1467
|
try {
|
|
637
1468
|
await mutateOperations(
|
|
638
1469
|
request,
|
|
@@ -647,13 +1478,43 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
647
1478
|
await refetchArchivedTasks();
|
|
648
1479
|
} catch {
|
|
649
1480
|
// ignore
|
|
1481
|
+
} finally {
|
|
1482
|
+
setRestoringTaskId(null);
|
|
650
1483
|
}
|
|
651
1484
|
},
|
|
652
1485
|
[request, refetchTasks, refetchArchivedTasks]
|
|
653
1486
|
);
|
|
654
1487
|
|
|
1488
|
+
const handleInlineCreateTask = useCallback(
|
|
1489
|
+
async (column: BoardColumnId, name: string) => {
|
|
1490
|
+
const trimmed = name.trim();
|
|
1491
|
+
if (!trimmed) {
|
|
1492
|
+
setInlineCreateColumn(null);
|
|
1493
|
+
setInlineCreateName('');
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
setInlineCreateLoading(true);
|
|
1497
|
+
try {
|
|
1498
|
+
await mutateOperations(request, '/operations/tasks', 'POST', {
|
|
1499
|
+
projectId,
|
|
1500
|
+
name: trimmed,
|
|
1501
|
+
status: column,
|
|
1502
|
+
priority: 'medium',
|
|
1503
|
+
});
|
|
1504
|
+
setBoardState(null);
|
|
1505
|
+
setInlineCreateColumn(null);
|
|
1506
|
+
setInlineCreateName('');
|
|
1507
|
+
await refetchTasks();
|
|
1508
|
+
} finally {
|
|
1509
|
+
setInlineCreateLoading(false);
|
|
1510
|
+
}
|
|
1511
|
+
},
|
|
1512
|
+
[projectId, request, refetchTasks]
|
|
1513
|
+
);
|
|
1514
|
+
|
|
655
1515
|
const handleDeleteTask = useCallback(
|
|
656
1516
|
async (taskId: number) => {
|
|
1517
|
+
setDeletingTaskId(taskId);
|
|
657
1518
|
try {
|
|
658
1519
|
await mutateOperations(
|
|
659
1520
|
request,
|
|
@@ -667,6 +1528,8 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
667
1528
|
await refetchArchivedTasks();
|
|
668
1529
|
} catch {
|
|
669
1530
|
// ignore
|
|
1531
|
+
} finally {
|
|
1532
|
+
setDeletingTaskId(null);
|
|
670
1533
|
}
|
|
671
1534
|
},
|
|
672
1535
|
[request, refetchTasks, refetchArchivedTasks]
|
|
@@ -705,13 +1568,16 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
705
1568
|
return [];
|
|
706
1569
|
}, [projectStats]);
|
|
707
1570
|
|
|
708
|
-
const findColumnByTask = (
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
1571
|
+
const findColumnByTask = useCallback(
|
|
1572
|
+
(taskId: number) => {
|
|
1573
|
+
const match = KANBAN_COLUMNS.find((column) =>
|
|
1574
|
+
taskColumns[column.id].some((task) => task.id === taskId)
|
|
1575
|
+
);
|
|
712
1576
|
|
|
713
|
-
|
|
714
|
-
|
|
1577
|
+
return match?.id ?? null;
|
|
1578
|
+
},
|
|
1579
|
+
[taskColumns]
|
|
1580
|
+
);
|
|
715
1581
|
|
|
716
1582
|
const moveTaskToColumn = useCallback(
|
|
717
1583
|
(taskId: number, targetColumn: BoardColumnId) => {
|
|
@@ -751,10 +1617,20 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
751
1617
|
void refetchTasks();
|
|
752
1618
|
});
|
|
753
1619
|
},
|
|
754
|
-
[taskColumns, project, request, refetchTasks]
|
|
755
|
-
);
|
|
1620
|
+
[findColumnByTask, taskColumns, project, request, refetchTasks]
|
|
1621
|
+
);
|
|
1622
|
+
|
|
1623
|
+
const onBoardDragStart = (event: DragStartEvent) => {
|
|
1624
|
+
const taskId = parseTaskId(event.active.id);
|
|
1625
|
+
if (!taskId) return;
|
|
1626
|
+
const col = findColumnByTask(taskId);
|
|
1627
|
+
if (!col) return;
|
|
1628
|
+
const task = taskColumns[col].find((t) => t.id === taskId) ?? null;
|
|
1629
|
+
setActiveDragTask(task);
|
|
1630
|
+
};
|
|
756
1631
|
|
|
757
1632
|
const onBoardDragEnd = (event: DragEndEvent) => {
|
|
1633
|
+
setActiveDragTask(null);
|
|
758
1634
|
const taskId = parseTaskId(event.active.id);
|
|
759
1635
|
const targetColumn = parseColumnId(event.over?.id);
|
|
760
1636
|
|
|
@@ -765,9 +1641,149 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
765
1641
|
moveTaskToColumn(taskId, targetColumn);
|
|
766
1642
|
};
|
|
767
1643
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1644
|
+
const timelineEvents = useMemo<OperationalTimelineEvent[]>(() => {
|
|
1645
|
+
if (!project) {
|
|
1646
|
+
return [];
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
const events: OperationalTimelineEvent[] = [];
|
|
1650
|
+
const projectStart = getValidTimestamp(project.startDate);
|
|
1651
|
+
const projectEnd = getValidTimestamp(project.endDate);
|
|
1652
|
+
|
|
1653
|
+
if (projectStart) {
|
|
1654
|
+
events.push({
|
|
1655
|
+
id: `project-start-${project.id}`,
|
|
1656
|
+
type: 'status',
|
|
1657
|
+
title: t('timeline.projectStarted'),
|
|
1658
|
+
description: t('timeline.projectStartedDescription', {
|
|
1659
|
+
project: project.name,
|
|
1660
|
+
}),
|
|
1661
|
+
timestamp: projectStart,
|
|
1662
|
+
actorName: project.managerName,
|
|
1663
|
+
actorAvatarId: project.managerAvatarId,
|
|
1664
|
+
icon: Rocket,
|
|
1665
|
+
toneClassName: 'bg-sky-500 text-white',
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
if (projectEnd) {
|
|
1670
|
+
events.push({
|
|
1671
|
+
id: `project-deadline-${project.id}`,
|
|
1672
|
+
type: 'status',
|
|
1673
|
+
title: t('timeline.targetDate'),
|
|
1674
|
+
description: t('timeline.targetDateDescription'),
|
|
1675
|
+
timestamp: projectEnd,
|
|
1676
|
+
actorName: project.managerName,
|
|
1677
|
+
actorAvatarId: project.managerAvatarId,
|
|
1678
|
+
icon: CalendarClock,
|
|
1679
|
+
toneClassName: isPastDue(project.endDate)
|
|
1680
|
+
? 'bg-rose-500 text-white'
|
|
1681
|
+
: 'bg-violet-500 text-white',
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
apiTasks.forEach((task) => {
|
|
1686
|
+
const taskCreatedAt =
|
|
1687
|
+
getValidTimestamp(task.createdAt) ??
|
|
1688
|
+
getValidTimestamp(task.dueDate) ??
|
|
1689
|
+
projectStart;
|
|
1690
|
+
const actorName = task.assigneeName || project.managerName;
|
|
1691
|
+
|
|
1692
|
+
if (taskCreatedAt) {
|
|
1693
|
+
events.push({
|
|
1694
|
+
id: `task-created-${task.id}`,
|
|
1695
|
+
type: 'task',
|
|
1696
|
+
title: t('timeline.taskCreated'),
|
|
1697
|
+
description: task.name,
|
|
1698
|
+
timestamp: taskCreatedAt,
|
|
1699
|
+
actorName,
|
|
1700
|
+
actorAvatarId: task.assigneePersonAvatarId,
|
|
1701
|
+
actorUserPhotoId: task.assigneeUserPhotoId,
|
|
1702
|
+
icon: Plus,
|
|
1703
|
+
toneClassName: 'bg-slate-500 text-white',
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
if (task.status === 'done') {
|
|
1708
|
+
events.push({
|
|
1709
|
+
id: `task-done-${task.id}`,
|
|
1710
|
+
type: 'status',
|
|
1711
|
+
title: t('timeline.taskCompleted'),
|
|
1712
|
+
description: task.name,
|
|
1713
|
+
timestamp:
|
|
1714
|
+
getValidTimestamp(task.dueDate) ??
|
|
1715
|
+
taskCreatedAt ??
|
|
1716
|
+
new Date().toISOString(),
|
|
1717
|
+
actorName,
|
|
1718
|
+
actorAvatarId: task.assigneePersonAvatarId,
|
|
1719
|
+
actorUserPhotoId: task.assigneeUserPhotoId,
|
|
1720
|
+
icon: CheckCircle2,
|
|
1721
|
+
toneClassName: 'bg-emerald-500 text-white',
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
if (task.description) {
|
|
1726
|
+
events.push({
|
|
1727
|
+
id: `task-comment-${task.id}`,
|
|
1728
|
+
type: 'comment',
|
|
1729
|
+
title: t('timeline.commentAdded'),
|
|
1730
|
+
description: task.name,
|
|
1731
|
+
timestamp: taskCreatedAt ?? new Date().toISOString(),
|
|
1732
|
+
actorName,
|
|
1733
|
+
actorAvatarId: task.assigneePersonAvatarId,
|
|
1734
|
+
actorUserPhotoId: task.assigneeUserPhotoId,
|
|
1735
|
+
icon: MessageSquare,
|
|
1736
|
+
toneClassName: 'bg-indigo-500 text-white',
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1741
|
+
if (project.timesheetSummary.totalTimesheets > 0) {
|
|
1742
|
+
events.push({
|
|
1743
|
+
id: `timesheets-${project.id}`,
|
|
1744
|
+
type: 'timesheet',
|
|
1745
|
+
title: t('timeline.timesheetLogged'),
|
|
1746
|
+
description: t('timeline.timesheetLoggedDescription', {
|
|
1747
|
+
count: project.timesheetSummary.totalTimesheets,
|
|
1748
|
+
hours: formatHours(project.timesheetSummary.totalHours),
|
|
1749
|
+
}),
|
|
1750
|
+
timestamp: new Date().toISOString(),
|
|
1751
|
+
actorName: project.managerName,
|
|
1752
|
+
actorAvatarId: project.managerAvatarId,
|
|
1753
|
+
icon: Timer,
|
|
1754
|
+
toneClassName: 'bg-cyan-500 text-white',
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
if (project.timesheetSummary.pendingTimesheets > 0) {
|
|
1759
|
+
events.push({
|
|
1760
|
+
id: `approvals-${project.id}`,
|
|
1761
|
+
type: 'approval',
|
|
1762
|
+
title: t('timeline.approvalPending'),
|
|
1763
|
+
description: t('timeline.approvalPendingDescription', {
|
|
1764
|
+
count: project.timesheetSummary.pendingTimesheets,
|
|
1765
|
+
}),
|
|
1766
|
+
timestamp: new Date().toISOString(),
|
|
1767
|
+
actorName: project.managerName,
|
|
1768
|
+
actorAvatarId: project.managerAvatarId,
|
|
1769
|
+
icon: GitCommitHorizontal,
|
|
1770
|
+
toneClassName: 'bg-amber-500 text-white',
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
return events.sort(
|
|
1775
|
+
(a, b) =>
|
|
1776
|
+
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
1777
|
+
);
|
|
1778
|
+
}, [apiTasks, project, t]);
|
|
1779
|
+
|
|
1780
|
+
if (isProjectLoading) {
|
|
1781
|
+
return <ProjectDetailsSkeleton />;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
if (!project) {
|
|
1785
|
+
return (
|
|
1786
|
+
<Page>
|
|
771
1787
|
<OperationsHeader
|
|
772
1788
|
title={t('title')}
|
|
773
1789
|
description={t('description')}
|
|
@@ -784,886 +1800,2333 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
784
1800
|
);
|
|
785
1801
|
}
|
|
786
1802
|
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1803
|
+
const totalTasks = apiTasks.length;
|
|
1804
|
+
const completedTasks = taskColumns.done.length;
|
|
1805
|
+
const pendingTasks = totalTasks - completedTasks;
|
|
1806
|
+
const activeCollaborators =
|
|
1807
|
+
project.operationalIndicators.activeAssignments ||
|
|
1808
|
+
project.assignments.filter((assignment) => assignment.status === 'active')
|
|
1809
|
+
.length ||
|
|
1810
|
+
project.assignments.length;
|
|
1811
|
+
const overdueTasks = apiTasks.filter(
|
|
1812
|
+
(task) => task.status !== 'done' && isPastDue(task.dueDate)
|
|
1813
|
+
).length;
|
|
1814
|
+
const projectProgress = clampPercent(project.progressPercent);
|
|
1815
|
+
const taskProgress =
|
|
1816
|
+
totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
|
|
1817
|
+
const displayedProgress = projectProgress || taskProgress;
|
|
1818
|
+
const averageAllocation = clampPercent(
|
|
1819
|
+
project.operationalIndicators.averageAllocation
|
|
1820
|
+
);
|
|
1821
|
+
const weeklyVelocity =
|
|
1822
|
+
projectStats?.weeklyVelocity?.at(-1)?.loggedHours ??
|
|
1823
|
+
projectStats?.quickRadar?.totalWeeklyHours ??
|
|
1824
|
+
project.operationalIndicators.totalWeeklyHours;
|
|
1825
|
+
const projectHealth = getProjectHealthScore({
|
|
1826
|
+
progress: displayedProgress,
|
|
1827
|
+
averageAllocation,
|
|
1828
|
+
overdueTasks,
|
|
1829
|
+
pendingTimesheets: project.timesheetSummary.pendingTimesheets,
|
|
1830
|
+
});
|
|
1831
|
+
const projectHealthLabel =
|
|
1832
|
+
projectHealth.labelKey === 'good'
|
|
1833
|
+
? t('health.good')
|
|
1834
|
+
: projectHealth.labelKey === 'warning'
|
|
1835
|
+
? t('health.warning')
|
|
1836
|
+
: t('health.danger');
|
|
1837
|
+
const projectHealthTrend =
|
|
1838
|
+
projectHealth.labelKey === 'good'
|
|
1839
|
+
? t('kpi.trends.health.good')
|
|
1840
|
+
: projectHealth.labelKey === 'warning'
|
|
1841
|
+
? t('kpi.trends.health.warning')
|
|
1842
|
+
: t('kpi.trends.health.danger');
|
|
1843
|
+
const teamPreview = project.assignments.slice(0, 5);
|
|
1844
|
+
const hiddenTeamCount = Math.max(
|
|
1845
|
+
project.assignments.length - teamPreview.length,
|
|
1846
|
+
0
|
|
1847
|
+
);
|
|
1848
|
+
const overloadedAssignments = project.assignments.filter(
|
|
1849
|
+
(assignment) => (assignment.allocationPercent ?? 0) > 100
|
|
1850
|
+
).length;
|
|
1851
|
+
const highAllocationAssignments = project.assignments.filter((assignment) => {
|
|
1852
|
+
const allocation = assignment.allocationPercent ?? 0;
|
|
1853
|
+
return allocation >= 85 && allocation <= 100;
|
|
1854
|
+
}).length;
|
|
1855
|
+
const availableAssignments = project.assignments.filter(
|
|
1856
|
+
(assignment) => (assignment.allocationPercent ?? 0) < 85
|
|
1857
|
+
).length;
|
|
1858
|
+
const burnupChartData =
|
|
1859
|
+
velocityChartData.length > 0
|
|
1860
|
+
? velocityChartData.reduce<
|
|
1861
|
+
Array<{ week: string; loggedHours: number; planned: number }>
|
|
1862
|
+
>((items, row, index) => {
|
|
1863
|
+
const previous = items[index - 1]?.loggedHours ?? 0;
|
|
1864
|
+
const loggedHours = previous + Number(row.loggedHours ?? 0);
|
|
1865
|
+
const planned =
|
|
1866
|
+
project.operationalIndicators.totalWeeklyHours > 0
|
|
1867
|
+
? project.operationalIndicators.totalWeeklyHours * (index + 1)
|
|
1868
|
+
: loggedHours;
|
|
1869
|
+
items.push({ week: row.week, loggedHours, planned });
|
|
1870
|
+
return items;
|
|
1871
|
+
}, [])
|
|
1872
|
+
: [
|
|
1873
|
+
{ week: t('charts.start'), loggedHours: 0, planned: 0 },
|
|
1874
|
+
{
|
|
1875
|
+
week: t('charts.current'),
|
|
1876
|
+
loggedHours: project.timesheetSummary.totalHours,
|
|
1877
|
+
planned:
|
|
1878
|
+
project.operationalIndicators.totalWeeklyHours ||
|
|
1879
|
+
project.timesheetSummary.totalHours,
|
|
1880
|
+
},
|
|
1881
|
+
];
|
|
1882
|
+
const taskDistributionData = KANBAN_COLUMNS.map((column) => ({
|
|
1883
|
+
key: column.id,
|
|
1884
|
+
name: column.label,
|
|
1885
|
+
value: taskColumns[column.id].length,
|
|
1886
|
+
fill: `var(--color-${column.id})`,
|
|
1887
|
+
})).filter((item) => item.value > 0);
|
|
1888
|
+
const healthChartData = [
|
|
794
1889
|
{
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1890
|
+
name: t('charts.healthScore'),
|
|
1891
|
+
value: projectHealth.value,
|
|
1892
|
+
fill:
|
|
1893
|
+
projectHealth.tone === 'danger'
|
|
1894
|
+
? 'hsl(0 84% 60%)'
|
|
1895
|
+
: projectHealth.tone === 'warning'
|
|
1896
|
+
? 'hsl(38 92% 50%)'
|
|
1897
|
+
: 'var(--color-health)',
|
|
801
1898
|
},
|
|
1899
|
+
];
|
|
1900
|
+
const chartDashboardLoading = isProjectStatsLoading || isTasksLoading;
|
|
1901
|
+
const filteredTimelineEvents = timelineEvents.filter(
|
|
1902
|
+
(event) => timelineTypeFilter === 'all' || event.type === timelineTypeFilter
|
|
1903
|
+
);
|
|
1904
|
+
const visibleTimelineEvents = filteredTimelineEvents.slice(
|
|
1905
|
+
0,
|
|
1906
|
+
timelineVisibleCount
|
|
1907
|
+
);
|
|
1908
|
+
const groupedTimelineEvents = visibleTimelineEvents.reduce<
|
|
1909
|
+
Array<{ dayKey: string; events: OperationalTimelineEvent[] }>
|
|
1910
|
+
>((groups, event) => {
|
|
1911
|
+
const dayKey = getTimelineDayKey(event.timestamp);
|
|
1912
|
+
const currentGroup = groups[groups.length - 1];
|
|
1913
|
+
if (currentGroup?.dayKey === dayKey) {
|
|
1914
|
+
currentGroup.events.push(event);
|
|
1915
|
+
} else {
|
|
1916
|
+
groups.push({ dayKey, events: [event] });
|
|
1917
|
+
}
|
|
1918
|
+
return groups;
|
|
1919
|
+
}, []);
|
|
1920
|
+
|
|
1921
|
+
const allocationTone: ProjectKpiTone =
|
|
1922
|
+
project.operationalIndicators.averageAllocation > 100
|
|
1923
|
+
? 'critical'
|
|
1924
|
+
: project.operationalIndicators.averageAllocation > 85
|
|
1925
|
+
? 'warning'
|
|
1926
|
+
: 'positive';
|
|
1927
|
+
const pendingTasksTone: ProjectKpiTone =
|
|
1928
|
+
overdueTasks > 0 ? 'critical' : pendingTasks > 0 ? 'warning' : 'positive';
|
|
1929
|
+
const velocityTone: ProjectKpiTone = weeklyVelocity > 0 ? 'positive' : 'info';
|
|
1930
|
+
|
|
1931
|
+
const kpiWidgets: ProjectKpiWidgetItem[] = [
|
|
802
1932
|
{
|
|
803
1933
|
key: 'hours',
|
|
804
1934
|
title: t('cards.loggedHours'),
|
|
805
1935
|
value: formatHours(project.timesheetSummary.totalHours),
|
|
806
|
-
|
|
1936
|
+
subtitle: t('cards.loggedHoursDescription'),
|
|
1937
|
+
trend: t('kpi.trends.hours', {
|
|
1938
|
+
count: project.timesheetSummary.totalTimesheets,
|
|
1939
|
+
}),
|
|
1940
|
+
indicator: Math.min(project.timesheetSummary.totalHours, 100),
|
|
1941
|
+
icon: Timer,
|
|
1942
|
+
tone: 'info',
|
|
1943
|
+
},
|
|
1944
|
+
{
|
|
1945
|
+
key: 'health',
|
|
1946
|
+
title: t('cards.projectHealth'),
|
|
1947
|
+
value: projectHealthLabel,
|
|
1948
|
+
subtitle: t('kpi.subtitles.health'),
|
|
1949
|
+
trend: projectHealthTrend,
|
|
1950
|
+
indicator: projectHealth.value,
|
|
1951
|
+
icon: HeartPulse,
|
|
1952
|
+
tone:
|
|
1953
|
+
projectHealth.tone === 'danger'
|
|
1954
|
+
? 'critical'
|
|
1955
|
+
: projectHealth.tone === 'warning'
|
|
1956
|
+
? 'warning'
|
|
1957
|
+
: 'positive',
|
|
1958
|
+
},
|
|
1959
|
+
{
|
|
1960
|
+
key: 'velocity',
|
|
1961
|
+
title: t('cards.weeklyVelocity'),
|
|
1962
|
+
value: formatHours(weeklyVelocity),
|
|
1963
|
+
subtitle: t('cards.weeklyVelocityDescription'),
|
|
1964
|
+
trend:
|
|
1965
|
+
weeklyVelocity > 0
|
|
1966
|
+
? t('kpi.trends.velocity.active')
|
|
1967
|
+
: t('kpi.trends.velocity.empty'),
|
|
1968
|
+
indicator: Math.min(weeklyVelocity, 100),
|
|
1969
|
+
icon: TrendingUp,
|
|
1970
|
+
tone: velocityTone,
|
|
807
1971
|
},
|
|
808
1972
|
{
|
|
809
1973
|
key: 'allocation',
|
|
810
1974
|
title: t('cards.allocation'),
|
|
811
1975
|
value: formatPercent(project.operationalIndicators.averageAllocation),
|
|
812
|
-
|
|
1976
|
+
subtitle: t('cards.allocationDescription'),
|
|
1977
|
+
trend:
|
|
1978
|
+
allocationTone === 'critical'
|
|
1979
|
+
? t('kpi.trends.allocation.critical')
|
|
1980
|
+
: allocationTone === 'warning'
|
|
1981
|
+
? t('kpi.trends.allocation.warning')
|
|
1982
|
+
: t('kpi.trends.allocation.good'),
|
|
1983
|
+
indicator: averageAllocation,
|
|
1984
|
+
icon: Gauge,
|
|
1985
|
+
tone: allocationTone,
|
|
1986
|
+
},
|
|
1987
|
+
{
|
|
1988
|
+
key: 'pendingTasks',
|
|
1989
|
+
title: t('cards.pendingTasks'),
|
|
1990
|
+
value: pendingTasks,
|
|
1991
|
+
subtitle: t('cards.pendingTasksDescription', { overdue: overdueTasks }),
|
|
1992
|
+
trend:
|
|
1993
|
+
pendingTasksTone === 'critical'
|
|
1994
|
+
? t('kpi.trends.tasks.critical', { count: overdueTasks })
|
|
1995
|
+
: pendingTasksTone === 'warning'
|
|
1996
|
+
? t('kpi.trends.tasks.warning')
|
|
1997
|
+
: t('kpi.trends.tasks.good'),
|
|
1998
|
+
indicator:
|
|
1999
|
+
totalTasks > 0 ? Math.round((pendingTasks / totalTasks) * 100) : 0,
|
|
2000
|
+
icon: ClipboardList,
|
|
2001
|
+
tone: pendingTasksTone,
|
|
2002
|
+
},
|
|
2003
|
+
{
|
|
2004
|
+
key: 'activeCollaborators',
|
|
2005
|
+
title: t('cards.activeCollaborators'),
|
|
2006
|
+
value: activeCollaborators,
|
|
2007
|
+
subtitle: t('cards.activeCollaboratorsDescription'),
|
|
2008
|
+
trend:
|
|
2009
|
+
activeCollaborators > 0
|
|
2010
|
+
? t('kpi.trends.collaborators.active')
|
|
2011
|
+
: t('kpi.trends.collaborators.empty'),
|
|
2012
|
+
indicator:
|
|
2013
|
+
project.assignments.length > 0
|
|
2014
|
+
? Math.round((activeCollaborators / project.assignments.length) * 100)
|
|
2015
|
+
: 0,
|
|
2016
|
+
icon: Users,
|
|
2017
|
+
tone: activeCollaborators > 0 ? 'positive' : 'warning',
|
|
813
2018
|
},
|
|
814
2019
|
];
|
|
815
2020
|
|
|
816
2021
|
return (
|
|
817
2022
|
<Page>
|
|
818
|
-
<
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
2023
|
+
<motion.div
|
|
2024
|
+
initial={{ opacity: 0, y: 8 }}
|
|
2025
|
+
animate={{ opacity: 1, y: 0 }}
|
|
2026
|
+
transition={{ duration: 0.2 }}
|
|
2027
|
+
className="flex flex-col gap-0 pt-4"
|
|
2028
|
+
>
|
|
2029
|
+
{/* Row 1: Identity + actions */}
|
|
2030
|
+
<div className="flex flex-wrap items-center gap-3 pb-2">
|
|
2031
|
+
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
2032
|
+
<div className="flex size-9 shrink-0 items-center justify-center rounded-xl border bg-background shadow-xs">
|
|
2033
|
+
<FolderKanban className="size-5 text-primary" />
|
|
2034
|
+
</div>
|
|
2035
|
+
<div className="min-w-0">
|
|
2036
|
+
<nav
|
|
2037
|
+
aria-label="Breadcrumb"
|
|
2038
|
+
className="mb-0.5 flex items-center gap-1 text-[11px] text-muted-foreground"
|
|
2039
|
+
>
|
|
2040
|
+
<Link
|
|
2041
|
+
href="/operations/projects"
|
|
2042
|
+
className="transition hover:text-foreground"
|
|
2043
|
+
>
|
|
2044
|
+
{t('breadcrumbTrail.projects')}
|
|
2045
|
+
</Link>
|
|
2046
|
+
<ChevronRight className="size-3" />
|
|
2047
|
+
<span className="font-medium text-foreground">
|
|
2048
|
+
{project.code || project.name}
|
|
2049
|
+
</span>
|
|
2050
|
+
</nav>
|
|
2051
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
2052
|
+
<h1 className="text-base font-semibold leading-tight">
|
|
2053
|
+
{project.name}
|
|
2054
|
+
</h1>
|
|
2055
|
+
<StatusBadge
|
|
2056
|
+
label={getProjectStatusLabel(project.status)}
|
|
2057
|
+
className={getStatusBadgeClass(project.status)}
|
|
2058
|
+
/>
|
|
2059
|
+
<span
|
|
2060
|
+
className={[
|
|
2061
|
+
'rounded-full px-2 py-0.5 text-xs font-medium',
|
|
2062
|
+
projectHealth.tone === 'good'
|
|
2063
|
+
? 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400'
|
|
2064
|
+
: projectHealth.tone === 'warning'
|
|
2065
|
+
? 'bg-amber-500/15 text-amber-600 dark:text-amber-400'
|
|
2066
|
+
: 'bg-rose-500/15 text-rose-600 dark:text-rose-400',
|
|
2067
|
+
].join(' ')}
|
|
2068
|
+
>
|
|
2069
|
+
{projectHealthLabel}
|
|
2070
|
+
</span>
|
|
2071
|
+
</div>
|
|
2072
|
+
</div>
|
|
2073
|
+
</div>
|
|
2074
|
+
<div className="flex shrink-0 flex-wrap gap-1.5">
|
|
2075
|
+
{access.isDirector ? (
|
|
835
2076
|
<Button
|
|
836
2077
|
size="sm"
|
|
837
2078
|
onClick={openEditSheet}
|
|
838
|
-
className="cursor-pointer"
|
|
2079
|
+
className="cursor-pointer gap-1.5"
|
|
839
2080
|
>
|
|
840
|
-
<Pencil className="size-
|
|
2081
|
+
<Pencil className="size-3.5" />
|
|
841
2082
|
{commonT('actions.edit')}
|
|
842
2083
|
</Button>
|
|
843
|
-
|
|
844
|
-
) : undefined
|
|
845
|
-
}
|
|
846
|
-
/>
|
|
847
|
-
|
|
848
|
-
{!isLimitedView ? (
|
|
849
|
-
<>
|
|
850
|
-
<div className="rounded-xl border bg-linear-to-b from-muted/40 to-background p-3 sm:p-4">
|
|
851
|
-
<KpiCardsGrid items={cards} />
|
|
2084
|
+
) : null}
|
|
852
2085
|
</div>
|
|
2086
|
+
</div>
|
|
853
2087
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
{
|
|
863
|
-
|
|
864
|
-
<
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
</
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
2088
|
+
{/* Row 2: Metadata strip */}
|
|
2089
|
+
<div className="flex flex-wrap border-y text-xs">
|
|
2090
|
+
<Tooltip>
|
|
2091
|
+
<TooltipTrigger asChild>
|
|
2092
|
+
<div className="flex cursor-default items-center gap-1.5 border-r px-3 py-2 transition hover:bg-muted/30">
|
|
2093
|
+
<Avatar className="size-5 shrink-0 border bg-muted">
|
|
2094
|
+
<AvatarImage
|
|
2095
|
+
src={getPersonAvatarUrl(project.clientAvatarId)}
|
|
2096
|
+
alt={project.clientName || ''}
|
|
2097
|
+
/>
|
|
2098
|
+
<AvatarFallback className="text-[9px]">
|
|
2099
|
+
{getInitials(project.clientName)}
|
|
2100
|
+
</AvatarFallback>
|
|
2101
|
+
</Avatar>
|
|
2102
|
+
<span className="max-w-28 truncate font-medium">
|
|
2103
|
+
{project.clientName || commonT('labels.notAvailable')}
|
|
2104
|
+
</span>
|
|
2105
|
+
</div>
|
|
2106
|
+
</TooltipTrigger>
|
|
2107
|
+
<TooltipContent side="bottom">
|
|
2108
|
+
<p className="text-[11px] text-muted-foreground">
|
|
2109
|
+
{commonT('labels.client')}
|
|
2110
|
+
</p>
|
|
2111
|
+
</TooltipContent>
|
|
2112
|
+
</Tooltip>
|
|
2113
|
+
|
|
2114
|
+
<Tooltip>
|
|
2115
|
+
<TooltipTrigger asChild>
|
|
2116
|
+
<div className="flex cursor-default items-center gap-1.5 border-r px-3 py-2 transition hover:bg-muted/30">
|
|
2117
|
+
<Rocket className="size-3.5 shrink-0 text-muted-foreground" />
|
|
2118
|
+
<span className="max-w-32 truncate">
|
|
2119
|
+
{project.deliveryModel
|
|
2120
|
+
? getDeliveryModelLabel(project.deliveryModel)
|
|
2121
|
+
: commonT('labels.notAvailable')}
|
|
2122
|
+
</span>
|
|
2123
|
+
</div>
|
|
2124
|
+
</TooltipTrigger>
|
|
2125
|
+
<TooltipContent side="bottom">
|
|
2126
|
+
<p className="text-[11px] text-muted-foreground">
|
|
2127
|
+
{commonT('labels.deliveryModel')}
|
|
2128
|
+
</p>
|
|
2129
|
+
</TooltipContent>
|
|
2130
|
+
</Tooltip>
|
|
2131
|
+
|
|
2132
|
+
<Tooltip>
|
|
2133
|
+
<TooltipTrigger asChild>
|
|
2134
|
+
<div className="flex cursor-default items-center gap-1.5 border-r px-3 py-2 transition hover:bg-muted/30">
|
|
2135
|
+
<Users className="size-3.5 shrink-0 text-muted-foreground" />
|
|
2136
|
+
<span className="max-w-28 truncate">
|
|
2137
|
+
{project.managerName || commonT('labels.notAssigned')}
|
|
2138
|
+
</span>
|
|
2139
|
+
</div>
|
|
2140
|
+
</TooltipTrigger>
|
|
2141
|
+
<TooltipContent side="bottom">
|
|
2142
|
+
<p className="text-[11px] text-muted-foreground">
|
|
2143
|
+
{commonT('labels.manager')}
|
|
2144
|
+
</p>
|
|
2145
|
+
</TooltipContent>
|
|
2146
|
+
</Tooltip>
|
|
2147
|
+
|
|
2148
|
+
<Tooltip>
|
|
2149
|
+
<TooltipTrigger asChild>
|
|
2150
|
+
<div className="flex cursor-default items-center gap-1.5 border-r px-3 py-2 transition hover:bg-muted/30">
|
|
2151
|
+
<CalendarDays className="size-3.5 shrink-0 text-muted-foreground" />
|
|
2152
|
+
<span>
|
|
2153
|
+
{formatDate(
|
|
2154
|
+
project.startDate,
|
|
2155
|
+
getSettingValue,
|
|
2156
|
+
currentLocaleCode
|
|
2157
|
+
)}
|
|
2158
|
+
</span>
|
|
2159
|
+
</div>
|
|
2160
|
+
</TooltipTrigger>
|
|
2161
|
+
<TooltipContent side="bottom">
|
|
2162
|
+
<p className="text-[11px] text-muted-foreground">
|
|
2163
|
+
{commonT('labels.startDate')}
|
|
2164
|
+
</p>
|
|
2165
|
+
</TooltipContent>
|
|
2166
|
+
</Tooltip>
|
|
2167
|
+
|
|
2168
|
+
<Tooltip>
|
|
2169
|
+
<TooltipTrigger asChild>
|
|
2170
|
+
<div
|
|
2171
|
+
className={[
|
|
2172
|
+
'flex cursor-default items-center gap-1.5 border-r px-3 py-2 transition hover:bg-muted/30',
|
|
2173
|
+
isPastDue(project.endDate) ? 'text-rose-500' : '',
|
|
2174
|
+
].join(' ')}
|
|
2175
|
+
>
|
|
2176
|
+
<CalendarClock className="size-3.5 shrink-0 text-muted-foreground" />
|
|
2177
|
+
<span>
|
|
2178
|
+
{formatDate(
|
|
2179
|
+
project.endDate,
|
|
2180
|
+
getSettingValue,
|
|
2181
|
+
currentLocaleCode
|
|
2182
|
+
)}
|
|
2183
|
+
</span>
|
|
2184
|
+
</div>
|
|
2185
|
+
</TooltipTrigger>
|
|
2186
|
+
<TooltipContent side="bottom">
|
|
2187
|
+
<p className="text-[11px] text-muted-foreground">
|
|
2188
|
+
{commonT('labels.endDate')}
|
|
2189
|
+
</p>
|
|
2190
|
+
</TooltipContent>
|
|
2191
|
+
</Tooltip>
|
|
2192
|
+
|
|
2193
|
+
<Tooltip>
|
|
2194
|
+
<TooltipTrigger asChild>
|
|
2195
|
+
<div className="flex cursor-default items-center gap-2 border-r px-3 py-2 transition hover:bg-muted/30">
|
|
2196
|
+
<Gauge className="size-3.5 shrink-0 text-muted-foreground" />
|
|
2197
|
+
<div className="flex items-center gap-1.5">
|
|
2198
|
+
<div className="h-1.5 w-20 overflow-hidden rounded-full bg-muted">
|
|
2199
|
+
<motion.div
|
|
2200
|
+
initial={{ width: 0 }}
|
|
2201
|
+
animate={{ width: `${displayedProgress}%` }}
|
|
2202
|
+
transition={{ duration: 0.7, ease: 'easeOut' }}
|
|
2203
|
+
className="h-full rounded-full bg-primary"
|
|
913
2204
|
/>
|
|
914
|
-
</
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
{commonT('labels.deliveryModel')}
|
|
919
|
-
</dt>
|
|
920
|
-
<dd className="font-medium">
|
|
921
|
-
{project.deliveryModel
|
|
922
|
-
? getDeliveryModelLabel(project.deliveryModel)
|
|
923
|
-
: commonT('labels.notAvailable')}
|
|
924
|
-
</dd>
|
|
925
|
-
</div>
|
|
926
|
-
<div>
|
|
927
|
-
<dt className="text-muted-foreground">
|
|
928
|
-
{commonT('labels.startDate')}
|
|
929
|
-
</dt>
|
|
930
|
-
<dd className="font-medium">
|
|
931
|
-
{formatDate(
|
|
932
|
-
project.startDate,
|
|
933
|
-
getSettingValue,
|
|
934
|
-
currentLocaleCode
|
|
935
|
-
)}
|
|
936
|
-
</dd>
|
|
937
|
-
</div>
|
|
938
|
-
<div>
|
|
939
|
-
<dt className="text-muted-foreground">
|
|
940
|
-
{commonT('labels.endDate')}
|
|
941
|
-
</dt>
|
|
942
|
-
<dd className="font-medium">
|
|
943
|
-
{formatDate(
|
|
944
|
-
project.endDate,
|
|
945
|
-
getSettingValue,
|
|
946
|
-
currentLocaleCode
|
|
947
|
-
)}
|
|
948
|
-
</dd>
|
|
949
|
-
</div>
|
|
950
|
-
<div>
|
|
951
|
-
<dt className="text-muted-foreground">
|
|
952
|
-
{commonT('labels.budget')}
|
|
953
|
-
</dt>
|
|
954
|
-
<dd className="font-medium">
|
|
955
|
-
{project.budgetAmount
|
|
956
|
-
? formatCurrency(
|
|
957
|
-
project.budgetAmount,
|
|
958
|
-
getSettingValue,
|
|
959
|
-
currentLocaleCode
|
|
960
|
-
)
|
|
961
|
-
: commonT('labels.notAvailable')}
|
|
962
|
-
</dd>
|
|
963
|
-
</div>
|
|
964
|
-
<div>
|
|
965
|
-
<dt className="text-muted-foreground">
|
|
966
|
-
{commonT('labels.progress')}
|
|
967
|
-
</dt>
|
|
968
|
-
<dd className="font-medium">
|
|
969
|
-
{formatPercent(project.progressPercent)}
|
|
970
|
-
</dd>
|
|
971
|
-
</div>
|
|
972
|
-
<div>
|
|
973
|
-
<dt className="text-muted-foreground">
|
|
974
|
-
{commonT('labels.timeline')}
|
|
975
|
-
</dt>
|
|
976
|
-
<dd className="font-medium">
|
|
977
|
-
{formatDateRange(
|
|
978
|
-
project.startDate,
|
|
979
|
-
project.endDate,
|
|
980
|
-
getSettingValue,
|
|
981
|
-
currentLocaleCode
|
|
982
|
-
)}
|
|
983
|
-
</dd>
|
|
2205
|
+
</div>
|
|
2206
|
+
<span className="w-8 text-right font-semibold tabular-nums">
|
|
2207
|
+
{displayedProgress}%
|
|
2208
|
+
</span>
|
|
984
2209
|
</div>
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
2210
|
+
</div>
|
|
2211
|
+
</TooltipTrigger>
|
|
2212
|
+
<TooltipContent side="bottom">
|
|
2213
|
+
<p className="text-[11px] text-muted-foreground">
|
|
2214
|
+
{commonT('labels.progress')}
|
|
2215
|
+
</p>
|
|
2216
|
+
<p className="font-medium">
|
|
2217
|
+
{completedTasks}/{totalTasks} {t('executive.completedTasks')}
|
|
2218
|
+
</p>
|
|
2219
|
+
</TooltipContent>
|
|
2220
|
+
</Tooltip>
|
|
2221
|
+
|
|
2222
|
+
<Tooltip>
|
|
2223
|
+
<TooltipTrigger asChild>
|
|
2224
|
+
<div className="flex cursor-default items-center gap-2 px-3 py-2 transition hover:bg-muted/30">
|
|
2225
|
+
<div className="flex -space-x-1.5">
|
|
2226
|
+
{teamPreview.map((assignment) => (
|
|
2227
|
+
<Avatar
|
|
2228
|
+
key={assignment.id}
|
|
2229
|
+
className="size-6 border-2 border-card bg-muted"
|
|
2230
|
+
>
|
|
2231
|
+
<AvatarImage
|
|
2232
|
+
src={
|
|
2233
|
+
getUserPhotoUrl(assignment.userPhotoId) ||
|
|
2234
|
+
getPersonAvatarUrl(assignment.personAvatarId)
|
|
2235
|
+
}
|
|
2236
|
+
alt={assignment.collaboratorName}
|
|
994
2237
|
/>
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
2238
|
+
<AvatarFallback className="text-[9px]">
|
|
2239
|
+
{getInitials(assignment.collaboratorName)}
|
|
2240
|
+
</AvatarFallback>
|
|
2241
|
+
</Avatar>
|
|
2242
|
+
))}
|
|
2243
|
+
{hiddenTeamCount > 0 ? (
|
|
2244
|
+
<div className="flex size-6 items-center justify-center rounded-full border-2 border-card bg-muted text-[10px] font-semibold text-muted-foreground">
|
|
2245
|
+
+{hiddenTeamCount}
|
|
2246
|
+
</div>
|
|
2247
|
+
) : null}
|
|
1004
2248
|
</div>
|
|
2249
|
+
<span className="text-xs text-muted-foreground">
|
|
2250
|
+
{project.assignments.length}
|
|
2251
|
+
</span>
|
|
2252
|
+
</div>
|
|
2253
|
+
</TooltipTrigger>
|
|
2254
|
+
<TooltipContent side="bottom" className="max-w-44">
|
|
2255
|
+
<p className="mb-1 font-medium">{t('executive.team')}</p>
|
|
2256
|
+
{teamPreview.map((a) => (
|
|
2257
|
+
<p key={a.id} className="text-xs text-muted-foreground">
|
|
2258
|
+
{a.collaboratorName}
|
|
2259
|
+
{a.roleLabel ? ` · ${a.roleLabel}` : ''}
|
|
2260
|
+
</p>
|
|
2261
|
+
))}
|
|
2262
|
+
{hiddenTeamCount > 0 ? (
|
|
2263
|
+
<p className="text-xs text-muted-foreground">
|
|
2264
|
+
+{hiddenTeamCount}
|
|
2265
|
+
</p>
|
|
1005
2266
|
) : null}
|
|
1006
|
-
</
|
|
2267
|
+
</TooltipContent>
|
|
2268
|
+
</Tooltip>
|
|
2269
|
+
</div>
|
|
1007
2270
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
2271
|
+
{/* Row 3: KPI strip */}
|
|
2272
|
+
{!isLimitedView ? (
|
|
2273
|
+
<div className="flex divide-x bg-muted/10">
|
|
2274
|
+
{kpiWidgets.map((kpi, index) => {
|
|
2275
|
+
const kpiToneClass =
|
|
2276
|
+
kpi.tone === 'critical'
|
|
2277
|
+
? 'text-rose-500'
|
|
2278
|
+
: kpi.tone === 'warning'
|
|
2279
|
+
? 'text-amber-500'
|
|
2280
|
+
: kpi.tone === 'positive'
|
|
2281
|
+
? 'text-emerald-500'
|
|
2282
|
+
: 'text-sky-500';
|
|
2283
|
+
const kpiBarClass =
|
|
2284
|
+
kpi.tone === 'critical'
|
|
2285
|
+
? 'bg-rose-500'
|
|
2286
|
+
: kpi.tone === 'warning'
|
|
2287
|
+
? 'bg-amber-500'
|
|
2288
|
+
: kpi.tone === 'positive'
|
|
2289
|
+
? 'bg-emerald-500'
|
|
2290
|
+
: 'bg-sky-500';
|
|
2291
|
+
const isVelocity = kpi.key === 'velocity';
|
|
2292
|
+
const velocityBars = velocityChartData.slice(-5);
|
|
2293
|
+
const velocityMax = Math.max(
|
|
2294
|
+
...velocityBars.map((d) => d.loggedHours),
|
|
2295
|
+
1
|
|
2296
|
+
);
|
|
2297
|
+
return (
|
|
2298
|
+
<Tooltip key={kpi.key}>
|
|
2299
|
+
<TooltipTrigger asChild>
|
|
2300
|
+
<motion.div
|
|
2301
|
+
initial={{ opacity: 0 }}
|
|
2302
|
+
animate={{ opacity: 1 }}
|
|
2303
|
+
transition={{ delay: index * 0.05 }}
|
|
2304
|
+
className="flex flex-1 cursor-default select-none items-center gap-2 px-3 py-2 transition hover:bg-muted/20"
|
|
2305
|
+
>
|
|
2306
|
+
<kpi.icon
|
|
2307
|
+
className={['size-4 shrink-0', kpiToneClass].join(' ')}
|
|
2308
|
+
/>
|
|
2309
|
+
<div className="min-w-0 flex-1">
|
|
2310
|
+
<div className="text-sm font-semibold leading-none">
|
|
2311
|
+
{kpi.value}
|
|
2312
|
+
</div>
|
|
2313
|
+
{isVelocity && velocityBars.length > 1 ? (
|
|
2314
|
+
<div className="mt-1.5 flex items-end gap-px">
|
|
2315
|
+
{velocityBars.map((d, i) => (
|
|
2316
|
+
<motion.div
|
|
2317
|
+
key={i}
|
|
2318
|
+
className={[
|
|
2319
|
+
'w-1.5 rounded-sm opacity-70',
|
|
2320
|
+
kpiBarClass,
|
|
2321
|
+
].join(' ')}
|
|
2322
|
+
initial={{ height: 0 }}
|
|
2323
|
+
animate={{
|
|
2324
|
+
height: `${Math.max(3, Math.round((d.loggedHours / velocityMax) * 16))}px`,
|
|
2325
|
+
}}
|
|
2326
|
+
transition={{
|
|
2327
|
+
delay: index * 0.05 + i * 0.06,
|
|
2328
|
+
type: 'spring',
|
|
2329
|
+
stiffness: 180,
|
|
2330
|
+
}}
|
|
2331
|
+
/>
|
|
2332
|
+
))}
|
|
2333
|
+
</div>
|
|
2334
|
+
) : (
|
|
2335
|
+
<div className="mt-1.5 h-1 overflow-hidden rounded-full bg-muted">
|
|
2336
|
+
<motion.div
|
|
2337
|
+
initial={{ width: 0 }}
|
|
2338
|
+
animate={{
|
|
2339
|
+
width: `${clampPercent(kpi.indicator)}%`,
|
|
2340
|
+
}}
|
|
2341
|
+
transition={{
|
|
2342
|
+
duration: 0.55,
|
|
2343
|
+
delay: index * 0.07,
|
|
2344
|
+
ease: 'easeOut',
|
|
2345
|
+
}}
|
|
2346
|
+
className={[
|
|
2347
|
+
'h-full rounded-full',
|
|
2348
|
+
kpiBarClass,
|
|
2349
|
+
].join(' ')}
|
|
2350
|
+
/>
|
|
2351
|
+
</div>
|
|
2352
|
+
)}
|
|
1018
2353
|
</div>
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
2354
|
+
</motion.div>
|
|
2355
|
+
</TooltipTrigger>
|
|
2356
|
+
<TooltipContent side="bottom" className="max-w-52">
|
|
2357
|
+
<p className="font-medium">{kpi.title}</p>
|
|
2358
|
+
<p className="text-xs text-muted-foreground">
|
|
2359
|
+
{kpi.subtitle}
|
|
2360
|
+
</p>
|
|
2361
|
+
<p className="mt-1 text-xs font-medium">{kpi.trend}</p>
|
|
2362
|
+
</TooltipContent>
|
|
2363
|
+
</Tooltip>
|
|
2364
|
+
);
|
|
2365
|
+
})}
|
|
2366
|
+
</div>
|
|
2367
|
+
) : null}
|
|
2368
|
+
</motion.div>
|
|
2369
|
+
|
|
2370
|
+
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
|
2371
|
+
<TabsList className="flex-wrap">
|
|
2372
|
+
<TabsTrigger value="tasks">{t('tabs.tasks')}</TabsTrigger>
|
|
2373
|
+
{!isLimitedView ? (
|
|
2374
|
+
<TabsTrigger value="overview">{t('tabs.overview')}</TabsTrigger>
|
|
2375
|
+
) : null}
|
|
2376
|
+
{!isLimitedView ? (
|
|
2377
|
+
<TabsTrigger value="analysis">{t('tabs.analysis')}</TabsTrigger>
|
|
2378
|
+
) : null}
|
|
2379
|
+
<TabsTrigger value="team">{t('tabs.team')}</TabsTrigger>
|
|
2380
|
+
<TabsTrigger value="timeline">{t('tabs.timeline')}</TabsTrigger>
|
|
2381
|
+
<TabsTrigger value="archive">{t('tabs.archive')}</TabsTrigger>
|
|
2382
|
+
<TabsTrigger value="costs">{t('tabs.costs')}</TabsTrigger>
|
|
2383
|
+
</TabsList>
|
|
2384
|
+
|
|
2385
|
+
{!isLimitedView ? (
|
|
2386
|
+
<TabsContent value="overview">
|
|
2387
|
+
<div className="grid gap-4 xl:grid-cols-12">
|
|
2388
|
+
<SectionCard
|
|
2389
|
+
title={t('sections.overview')}
|
|
2390
|
+
className="rounded-xl border bg-card p-4 shadow-sm xl:col-span-12"
|
|
2391
|
+
>
|
|
2392
|
+
<dl className="grid gap-3 text-sm sm:grid-cols-2 xl:grid-cols-3">
|
|
2393
|
+
<div>
|
|
2394
|
+
<dt className="text-muted-foreground">
|
|
2395
|
+
{commonT('labels.project')}
|
|
2396
|
+
</dt>
|
|
2397
|
+
<dd className="font-medium">{project.name}</dd>
|
|
2398
|
+
</div>
|
|
2399
|
+
<div>
|
|
2400
|
+
<dt className="text-muted-foreground">
|
|
2401
|
+
{commonT('labels.code')}
|
|
2402
|
+
</dt>
|
|
2403
|
+
<dd className="font-medium">
|
|
2404
|
+
{project.code || commonT('labels.notAvailable')}
|
|
2405
|
+
</dd>
|
|
2406
|
+
</div>
|
|
2407
|
+
<div>
|
|
2408
|
+
<dt className="text-muted-foreground">
|
|
2409
|
+
{commonT('labels.client')}
|
|
2410
|
+
</dt>
|
|
2411
|
+
<dd className="font-medium">
|
|
2412
|
+
<div className="flex items-center gap-2">
|
|
2413
|
+
<Avatar className="h-8 w-8 border border-border/60 bg-muted">
|
|
2414
|
+
<AvatarImage
|
|
2415
|
+
src={getPersonAvatarUrl(project.clientAvatarId)}
|
|
2416
|
+
alt={project.clientName || commonT('labels.client')}
|
|
2417
|
+
/>
|
|
2418
|
+
<AvatarFallback className="bg-muted text-xs font-semibold text-foreground">
|
|
2419
|
+
{getInitials(
|
|
2420
|
+
project.clientName || commonT('labels.client')
|
|
2421
|
+
)}
|
|
2422
|
+
</AvatarFallback>
|
|
2423
|
+
</Avatar>
|
|
2424
|
+
<span>
|
|
2425
|
+
{project.clientName || commonT('labels.notAvailable')}
|
|
2426
|
+
</span>
|
|
1026
2427
|
</div>
|
|
1027
|
-
</
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
2428
|
+
</dd>
|
|
2429
|
+
</div>
|
|
2430
|
+
<div>
|
|
2431
|
+
<dt className="text-muted-foreground">
|
|
2432
|
+
{commonT('labels.manager')}
|
|
2433
|
+
</dt>
|
|
2434
|
+
<dd className="font-medium">
|
|
2435
|
+
{project.managerName || commonT('labels.notAssigned')}
|
|
2436
|
+
</dd>
|
|
2437
|
+
</div>
|
|
2438
|
+
<div>
|
|
2439
|
+
<dt className="text-muted-foreground">
|
|
2440
|
+
{commonT('labels.status')}
|
|
2441
|
+
</dt>
|
|
2442
|
+
<dd className="font-medium">
|
|
2443
|
+
<StatusBadge
|
|
2444
|
+
label={getProjectStatusLabel(project.status)}
|
|
2445
|
+
className={getStatusBadgeClass(project.status)}
|
|
2446
|
+
/>
|
|
2447
|
+
</dd>
|
|
2448
|
+
</div>
|
|
2449
|
+
<div>
|
|
2450
|
+
<dt className="text-muted-foreground">
|
|
2451
|
+
{commonT('labels.deliveryModel')}
|
|
2452
|
+
</dt>
|
|
2453
|
+
<dd className="font-medium">
|
|
2454
|
+
{project.deliveryModel
|
|
2455
|
+
? getDeliveryModelLabel(project.deliveryModel)
|
|
2456
|
+
: commonT('labels.notAvailable')}
|
|
2457
|
+
</dd>
|
|
2458
|
+
</div>
|
|
2459
|
+
<div>
|
|
2460
|
+
<dt className="text-muted-foreground">
|
|
2461
|
+
{commonT('labels.startDate')}
|
|
2462
|
+
</dt>
|
|
2463
|
+
<dd className="font-medium">
|
|
2464
|
+
{formatDate(
|
|
2465
|
+
project.startDate,
|
|
2466
|
+
getSettingValue,
|
|
2467
|
+
currentLocaleCode
|
|
1031
2468
|
)}
|
|
1032
|
-
|
|
1033
|
-
|
|
2469
|
+
</dd>
|
|
2470
|
+
</div>
|
|
2471
|
+
<div>
|
|
2472
|
+
<dt className="text-muted-foreground">
|
|
2473
|
+
{commonT('labels.endDate')}
|
|
2474
|
+
</dt>
|
|
2475
|
+
<dd className="font-medium">
|
|
2476
|
+
{formatDate(
|
|
2477
|
+
project.endDate,
|
|
2478
|
+
getSettingValue,
|
|
2479
|
+
currentLocaleCode
|
|
1034
2480
|
)}
|
|
1035
|
-
|
|
2481
|
+
</dd>
|
|
1036
2482
|
</div>
|
|
1037
|
-
<
|
|
1038
|
-
<
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
2483
|
+
<div>
|
|
2484
|
+
<dt className="text-muted-foreground">
|
|
2485
|
+
{commonT('labels.budget')}
|
|
2486
|
+
</dt>
|
|
2487
|
+
<dd className="font-medium">
|
|
2488
|
+
{project.budgetAmount
|
|
2489
|
+
? formatCurrency(
|
|
2490
|
+
project.budgetAmount,
|
|
2491
|
+
getSettingValue,
|
|
2492
|
+
currentLocaleCode
|
|
2493
|
+
)
|
|
2494
|
+
: commonT('labels.notAvailable')}
|
|
2495
|
+
</dd>
|
|
2496
|
+
</div>
|
|
2497
|
+
<div>
|
|
2498
|
+
<dt className="text-muted-foreground">
|
|
2499
|
+
{commonT('labels.progress')}
|
|
2500
|
+
</dt>
|
|
2501
|
+
<dd className="font-medium">
|
|
2502
|
+
{formatPercent(project.progressPercent)}
|
|
2503
|
+
</dd>
|
|
2504
|
+
</div>
|
|
2505
|
+
<div>
|
|
2506
|
+
<dt className="text-muted-foreground">
|
|
2507
|
+
{commonT('labels.timeline')}
|
|
2508
|
+
</dt>
|
|
2509
|
+
<dd className="font-medium">
|
|
2510
|
+
{formatDateRange(
|
|
2511
|
+
project.startDate,
|
|
2512
|
+
project.endDate,
|
|
2513
|
+
getSettingValue,
|
|
2514
|
+
currentLocaleCode
|
|
2515
|
+
)}
|
|
2516
|
+
</dd>
|
|
2517
|
+
</div>
|
|
2518
|
+
<div>
|
|
2519
|
+
<dt className="text-muted-foreground">
|
|
2520
|
+
{commonT('labels.contractStatus')}
|
|
2521
|
+
</dt>
|
|
2522
|
+
<dd className="font-medium">
|
|
2523
|
+
{project.contractStatus ? (
|
|
2524
|
+
<StatusBadge
|
|
2525
|
+
label={getContractStatusLabel(project.contractStatus)}
|
|
2526
|
+
className={getStatusBadgeClass(
|
|
2527
|
+
project.contractStatus
|
|
2528
|
+
)}
|
|
2529
|
+
/>
|
|
2530
|
+
) : (
|
|
2531
|
+
commonT('labels.notAssigned')
|
|
2532
|
+
)}
|
|
2533
|
+
</dd>
|
|
2534
|
+
</div>
|
|
2535
|
+
</dl>
|
|
2536
|
+
{project.summary ? (
|
|
2537
|
+
<div className="mt-4 rounded-lg border border-border/70 bg-muted/30 p-3 text-sm text-muted-foreground">
|
|
2538
|
+
{project.summary}
|
|
2539
|
+
</div>
|
|
2540
|
+
) : null}
|
|
2541
|
+
|
|
2542
|
+
{/* Contrato vinculado */}
|
|
2543
|
+
<div className="mt-6 border-t pt-6">
|
|
2544
|
+
<div className="mb-4 flex items-center gap-2 text-sm font-semibold">
|
|
2545
|
+
<FileText className="size-4 text-muted-foreground" />
|
|
2546
|
+
{t('sections.contract')}
|
|
2547
|
+
</div>
|
|
2548
|
+
{project.relatedContract ? (
|
|
2549
|
+
<div className="space-y-4">
|
|
2550
|
+
<div className="flex flex-col gap-3 rounded-xl border bg-muted/20 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
|
|
2551
|
+
<div>
|
|
2552
|
+
<div className="font-medium">
|
|
2553
|
+
{project.relatedContract.name}
|
|
2554
|
+
</div>
|
|
2555
|
+
<div className="text-sm text-muted-foreground">
|
|
2556
|
+
{[
|
|
2557
|
+
project.relatedContract.code,
|
|
2558
|
+
project.relatedContract.clientName,
|
|
2559
|
+
]
|
|
2560
|
+
.filter(Boolean)
|
|
2561
|
+
.join(' • ') || commonT('labels.notAvailable')}
|
|
2562
|
+
</div>
|
|
2563
|
+
</div>
|
|
2564
|
+
<div className="flex items-center gap-3">
|
|
2565
|
+
<StatusBadge
|
|
2566
|
+
label={getContractStatusLabel(
|
|
2567
|
+
project.relatedContract.status
|
|
2568
|
+
)}
|
|
2569
|
+
className={getStatusBadgeClass(
|
|
2570
|
+
project.relatedContract.status
|
|
2571
|
+
)}
|
|
2572
|
+
/>
|
|
2573
|
+
<Button
|
|
2574
|
+
variant="outline"
|
|
2575
|
+
size="sm"
|
|
2576
|
+
asChild
|
|
2577
|
+
className="shrink-0"
|
|
2578
|
+
>
|
|
2579
|
+
<Link
|
|
2580
|
+
href={`/operations/contracts?edit=${project.relatedContract.id}`}
|
|
2581
|
+
>
|
|
2582
|
+
<FileText className="size-4" />
|
|
2583
|
+
{commonT('actions.openContract')}
|
|
2584
|
+
</Link>
|
|
2585
|
+
</Button>
|
|
2586
|
+
</div>
|
|
2587
|
+
</div>
|
|
2588
|
+
<dl className="grid gap-3 text-sm sm:grid-cols-2 xl:grid-cols-3">
|
|
2589
|
+
<div>
|
|
2590
|
+
<dt className="text-muted-foreground">
|
|
2591
|
+
{commonT('labels.contractCategory')}
|
|
2592
|
+
</dt>
|
|
2593
|
+
<dd className="font-medium">
|
|
2594
|
+
{project.relatedContract.contractCategory
|
|
2595
|
+
? getContractCategoryLabel(
|
|
2596
|
+
project.relatedContract.contractCategory
|
|
2597
|
+
)
|
|
2598
|
+
: commonT('labels.notAvailable')}
|
|
2599
|
+
</dd>
|
|
2600
|
+
</div>
|
|
2601
|
+
<div>
|
|
2602
|
+
<dt className="text-muted-foreground">
|
|
2603
|
+
{commonT('labels.contractType')}
|
|
2604
|
+
</dt>
|
|
2605
|
+
<dd className="font-medium">
|
|
2606
|
+
{project.relatedContract.contractType
|
|
2607
|
+
? getContractTypeLabel(
|
|
2608
|
+
project.relatedContract.contractType
|
|
2609
|
+
)
|
|
2610
|
+
: commonT('labels.notAvailable')}
|
|
2611
|
+
</dd>
|
|
2612
|
+
</div>
|
|
2613
|
+
<div>
|
|
2614
|
+
<dt className="text-muted-foreground">
|
|
2615
|
+
{commonT('labels.billingModel')}
|
|
2616
|
+
</dt>
|
|
2617
|
+
<dd className="font-medium">
|
|
2618
|
+
{getBillingModelLabel(
|
|
2619
|
+
project.relatedContract.billingModel
|
|
2620
|
+
)}
|
|
2621
|
+
</dd>
|
|
2622
|
+
</div>
|
|
2623
|
+
<div>
|
|
2624
|
+
<dt className="text-muted-foreground">
|
|
2625
|
+
{commonT('labels.timeline')}
|
|
2626
|
+
</dt>
|
|
2627
|
+
<dd className="font-medium">
|
|
2628
|
+
{formatDateRange(
|
|
2629
|
+
project.relatedContract.startDate,
|
|
2630
|
+
project.relatedContract.endDate,
|
|
2631
|
+
getSettingValue,
|
|
2632
|
+
currentLocaleCode
|
|
2633
|
+
)}
|
|
2634
|
+
</dd>
|
|
2635
|
+
</div>
|
|
2636
|
+
<div>
|
|
2637
|
+
<dt className="text-muted-foreground">
|
|
2638
|
+
{commonT('labels.signatureStatus')}
|
|
2639
|
+
</dt>
|
|
2640
|
+
<dd className="font-medium">
|
|
2641
|
+
{project.relatedContract.signatureStatus
|
|
2642
|
+
? getSignatureStatusLabel(
|
|
2643
|
+
project.relatedContract.signatureStatus
|
|
2644
|
+
)
|
|
2645
|
+
: commonT('labels.notAvailable')}
|
|
2646
|
+
</dd>
|
|
2647
|
+
</div>
|
|
2648
|
+
<div>
|
|
2649
|
+
<dt className="text-muted-foreground">
|
|
2650
|
+
{commonT('labels.budget')}
|
|
2651
|
+
</dt>
|
|
2652
|
+
<dd className="font-medium">
|
|
2653
|
+
{project.relatedContract.budgetAmount
|
|
2654
|
+
? formatCurrency(
|
|
2655
|
+
project.relatedContract.budgetAmount,
|
|
2656
|
+
getSettingValue,
|
|
2657
|
+
currentLocaleCode
|
|
2658
|
+
)
|
|
2659
|
+
: commonT('labels.notAvailable')}
|
|
2660
|
+
</dd>
|
|
2661
|
+
</div>
|
|
2662
|
+
</dl>
|
|
1049
2663
|
</div>
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
2664
|
+
) : (
|
|
2665
|
+
<p className="text-sm text-muted-foreground">
|
|
2666
|
+
{t('noContract')}
|
|
2667
|
+
</p>
|
|
2668
|
+
)}
|
|
2669
|
+
</div>
|
|
2670
|
+
</SectionCard>
|
|
2671
|
+
</div>
|
|
2672
|
+
</TabsContent>
|
|
2673
|
+
) : null}
|
|
2674
|
+
|
|
2675
|
+
{!isLimitedView ? (
|
|
2676
|
+
<TabsContent value="analysis">
|
|
2677
|
+
<SectionCard
|
|
2678
|
+
title={t('sections.deliveryHealth')}
|
|
2679
|
+
description={t('sections.deliveryHealthDescription')}
|
|
2680
|
+
className="rounded-3xl border bg-card p-4 shadow-sm"
|
|
2681
|
+
>
|
|
2682
|
+
<div className="grid gap-4 xl:grid-cols-12">
|
|
2683
|
+
<ProjectChartCard
|
|
2684
|
+
title={t('charts.burnup')}
|
|
2685
|
+
description={t('charts.burnupDescription')}
|
|
2686
|
+
icon={LineChartIcon}
|
|
2687
|
+
metric={formatPercent(displayedProgress)}
|
|
2688
|
+
className="xl:col-span-8"
|
|
2689
|
+
isLoading={chartDashboardLoading}
|
|
2690
|
+
>
|
|
2691
|
+
{burnupChartData.length > 1 ? (
|
|
2692
|
+
<ChartContainer
|
|
2693
|
+
className="h-80 w-full"
|
|
2694
|
+
config={boardChartConfig}
|
|
2695
|
+
>
|
|
2696
|
+
<AreaChart data={burnupChartData}>
|
|
2697
|
+
<defs>
|
|
2698
|
+
<linearGradient
|
|
2699
|
+
id="burnupLogged"
|
|
2700
|
+
x1="0"
|
|
2701
|
+
y1="0"
|
|
2702
|
+
x2="0"
|
|
2703
|
+
y2="1"
|
|
2704
|
+
>
|
|
2705
|
+
<stop
|
|
2706
|
+
offset="5%"
|
|
2707
|
+
stopColor="var(--color-loggedHours)"
|
|
2708
|
+
stopOpacity={0.26}
|
|
2709
|
+
/>
|
|
2710
|
+
<stop
|
|
2711
|
+
offset="95%"
|
|
2712
|
+
stopColor="var(--color-loggedHours)"
|
|
2713
|
+
stopOpacity={0.02}
|
|
2714
|
+
/>
|
|
2715
|
+
</linearGradient>
|
|
2716
|
+
</defs>
|
|
2717
|
+
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
|
2718
|
+
<XAxis
|
|
2719
|
+
dataKey="week"
|
|
2720
|
+
tickLine={false}
|
|
2721
|
+
axisLine={false}
|
|
2722
|
+
/>
|
|
2723
|
+
<YAxis tickLine={false} axisLine={false} width={36} />
|
|
2724
|
+
<ChartTooltip content={<ChartTooltipContent />} />
|
|
2725
|
+
<Area
|
|
2726
|
+
type="monotone"
|
|
2727
|
+
dataKey="planned"
|
|
2728
|
+
stroke="var(--color-planned)"
|
|
2729
|
+
strokeDasharray="4 4"
|
|
2730
|
+
strokeWidth={2}
|
|
2731
|
+
fill="transparent"
|
|
2732
|
+
/>
|
|
2733
|
+
<Area
|
|
2734
|
+
type="monotone"
|
|
2735
|
+
dataKey="loggedHours"
|
|
2736
|
+
stroke="var(--color-loggedHours)"
|
|
2737
|
+
strokeWidth={2.5}
|
|
2738
|
+
fill="url(#burnupLogged)"
|
|
2739
|
+
/>
|
|
2740
|
+
</AreaChart>
|
|
2741
|
+
</ChartContainer>
|
|
2742
|
+
) : (
|
|
2743
|
+
<ChartEmptyState
|
|
2744
|
+
icon={LineChartIcon}
|
|
2745
|
+
title={t('charts.emptyTitle')}
|
|
2746
|
+
description={t('charts.emptyBurnup')}
|
|
2747
|
+
/>
|
|
2748
|
+
)}
|
|
2749
|
+
</ProjectChartCard>
|
|
2750
|
+
|
|
2751
|
+
<ProjectChartCard
|
|
2752
|
+
title={t('charts.weeklyVelocity')}
|
|
2753
|
+
description={t('charts.weeklyVelocityDescription')}
|
|
2754
|
+
icon={Rocket}
|
|
2755
|
+
metric={formatHours(weeklyVelocity)}
|
|
2756
|
+
className="xl:col-span-4"
|
|
2757
|
+
isLoading={isProjectStatsLoading}
|
|
2758
|
+
>
|
|
2759
|
+
{velocityChartData.length > 0 ? (
|
|
2760
|
+
<ChartContainer
|
|
2761
|
+
className="h-80 w-full"
|
|
2762
|
+
config={boardChartConfig}
|
|
2763
|
+
>
|
|
2764
|
+
<LineChart data={velocityChartData}>
|
|
2765
|
+
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
|
2766
|
+
<XAxis
|
|
2767
|
+
dataKey="week"
|
|
2768
|
+
tickLine={false}
|
|
2769
|
+
axisLine={false}
|
|
2770
|
+
/>
|
|
2771
|
+
<YAxis tickLine={false} axisLine={false} width={30} />
|
|
2772
|
+
<ChartTooltip content={<ChartTooltipContent />} />
|
|
2773
|
+
<Line
|
|
2774
|
+
type="monotone"
|
|
2775
|
+
dataKey="loggedHours"
|
|
2776
|
+
stroke="var(--color-loggedHours)"
|
|
2777
|
+
strokeWidth={2.5}
|
|
2778
|
+
dot={{ r: 3 }}
|
|
2779
|
+
activeDot={{ r: 5 }}
|
|
2780
|
+
/>
|
|
2781
|
+
</LineChart>
|
|
2782
|
+
</ChartContainer>
|
|
2783
|
+
) : (
|
|
2784
|
+
<ChartEmptyState
|
|
2785
|
+
icon={Rocket}
|
|
2786
|
+
title={t('charts.emptyTitle')}
|
|
2787
|
+
description={t('charts.emptyVelocity')}
|
|
2788
|
+
/>
|
|
2789
|
+
)}
|
|
2790
|
+
</ProjectChartCard>
|
|
2791
|
+
|
|
2792
|
+
<ProjectChartCard
|
|
2793
|
+
title={t('charts.allocationByCollaborator')}
|
|
2794
|
+
description={t('charts.allocationDescription')}
|
|
2795
|
+
icon={BarChart3}
|
|
2796
|
+
metric={formatPercent(
|
|
2797
|
+
project.operationalIndicators.averageAllocation
|
|
2798
|
+
)}
|
|
2799
|
+
className="xl:col-span-5"
|
|
2800
|
+
isLoading={chartDashboardLoading}
|
|
2801
|
+
>
|
|
2802
|
+
{allocationChartData.length > 0 ? (
|
|
2803
|
+
<ChartContainer
|
|
2804
|
+
className="h-72 w-full"
|
|
2805
|
+
config={boardChartConfig}
|
|
2806
|
+
>
|
|
2807
|
+
<BarChart data={allocationChartData}>
|
|
2808
|
+
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
|
2809
|
+
<XAxis
|
|
2810
|
+
dataKey="name"
|
|
2811
|
+
tickLine={false}
|
|
2812
|
+
axisLine={false}
|
|
2813
|
+
/>
|
|
2814
|
+
<YAxis tickLine={false} axisLine={false} width={32} />
|
|
2815
|
+
<ChartTooltip
|
|
2816
|
+
content={<ChartTooltipContent hideLabel />}
|
|
2817
|
+
/>
|
|
2818
|
+
<Bar
|
|
2819
|
+
dataKey="allocation"
|
|
2820
|
+
radius={[8, 8, 3, 3]}
|
|
2821
|
+
fill="var(--color-allocation)"
|
|
2822
|
+
/>
|
|
2823
|
+
</BarChart>
|
|
2824
|
+
</ChartContainer>
|
|
2825
|
+
) : (
|
|
2826
|
+
<ChartEmptyState
|
|
2827
|
+
icon={BarChart3}
|
|
2828
|
+
title={t('charts.emptyTitle')}
|
|
2829
|
+
description={t('charts.emptyAllocation')}
|
|
2830
|
+
/>
|
|
2831
|
+
)}
|
|
2832
|
+
</ProjectChartCard>
|
|
2833
|
+
|
|
2834
|
+
<ProjectChartCard
|
|
2835
|
+
title={t('charts.taskDistribution')}
|
|
2836
|
+
description={t('charts.taskDistributionDescription')}
|
|
2837
|
+
icon={ClipboardList}
|
|
2838
|
+
metric={`${totalTasks} ${t('kanban.items')}`}
|
|
2839
|
+
className="xl:col-span-4"
|
|
2840
|
+
isLoading={isTasksLoading}
|
|
2841
|
+
>
|
|
2842
|
+
{taskDistributionData.length > 0 ? (
|
|
2843
|
+
<div className="grid gap-4 md:grid-cols-[1fr_11rem]">
|
|
2844
|
+
<ChartContainer
|
|
2845
|
+
className="h-72 w-full"
|
|
2846
|
+
config={boardChartConfig}
|
|
2847
|
+
>
|
|
2848
|
+
<PieChart>
|
|
2849
|
+
<ChartTooltip
|
|
2850
|
+
content={<ChartTooltipContent hideLabel />}
|
|
2851
|
+
/>
|
|
2852
|
+
<Pie
|
|
2853
|
+
data={taskDistributionData}
|
|
2854
|
+
dataKey="value"
|
|
2855
|
+
nameKey="name"
|
|
2856
|
+
innerRadius={58}
|
|
2857
|
+
outerRadius={92}
|
|
2858
|
+
paddingAngle={4}
|
|
2859
|
+
>
|
|
2860
|
+
{taskDistributionData.map((entry) => (
|
|
2861
|
+
<Cell key={entry.key} fill={entry.fill} />
|
|
2862
|
+
))}
|
|
2863
|
+
</Pie>
|
|
2864
|
+
</PieChart>
|
|
2865
|
+
</ChartContainer>
|
|
2866
|
+
<div className="flex flex-col justify-center gap-2">
|
|
2867
|
+
{taskDistributionData.map((item) => (
|
|
2868
|
+
<div
|
|
2869
|
+
key={item.key}
|
|
2870
|
+
className="flex items-center justify-between gap-3 rounded-lg border bg-muted/10 px-3 py-2 text-xs"
|
|
2871
|
+
>
|
|
2872
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
2873
|
+
<span
|
|
2874
|
+
className="size-2.5 shrink-0 rounded-full"
|
|
2875
|
+
style={{ backgroundColor: item.fill }}
|
|
2876
|
+
/>
|
|
2877
|
+
<span className="truncate">{item.name}</span>
|
|
2878
|
+
</span>
|
|
2879
|
+
<span className="font-semibold">{item.value}</span>
|
|
2880
|
+
</div>
|
|
2881
|
+
))}
|
|
2882
|
+
</div>
|
|
1061
2883
|
</div>
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
2884
|
+
) : (
|
|
2885
|
+
<ChartEmptyState
|
|
2886
|
+
icon={ClipboardList}
|
|
2887
|
+
title={t('charts.emptyTitle')}
|
|
2888
|
+
description={t('charts.emptyTasks')}
|
|
2889
|
+
/>
|
|
2890
|
+
)}
|
|
2891
|
+
</ProjectChartCard>
|
|
2892
|
+
|
|
2893
|
+
<ProjectChartCard
|
|
2894
|
+
title={t('charts.operationalHealth')}
|
|
2895
|
+
description={t('charts.operationalHealthDescription')}
|
|
2896
|
+
icon={HeartPulse}
|
|
2897
|
+
metric={projectHealthLabel}
|
|
2898
|
+
className="xl:col-span-3"
|
|
2899
|
+
isLoading={chartDashboardLoading}
|
|
2900
|
+
>
|
|
2901
|
+
<div className="grid h-72 place-items-center">
|
|
2902
|
+
<ChartContainer
|
|
2903
|
+
className="h-56 w-full"
|
|
2904
|
+
config={boardChartConfig}
|
|
2905
|
+
>
|
|
2906
|
+
<RadialBarChart
|
|
2907
|
+
data={healthChartData}
|
|
2908
|
+
innerRadius="72%"
|
|
2909
|
+
outerRadius="100%"
|
|
2910
|
+
startAngle={180}
|
|
2911
|
+
endAngle={0}
|
|
2912
|
+
>
|
|
2913
|
+
<PolarAngleAxis
|
|
2914
|
+
type="number"
|
|
2915
|
+
domain={[0, 100]}
|
|
2916
|
+
tick={false}
|
|
2917
|
+
/>
|
|
2918
|
+
<RadialBar
|
|
2919
|
+
dataKey="value"
|
|
2920
|
+
cornerRadius={12}
|
|
2921
|
+
background={{ fill: 'hsl(var(--muted))' }}
|
|
2922
|
+
/>
|
|
2923
|
+
<ChartTooltip
|
|
2924
|
+
content={<ChartTooltipContent hideLabel />}
|
|
2925
|
+
/>
|
|
2926
|
+
</RadialBarChart>
|
|
2927
|
+
</ChartContainer>
|
|
2928
|
+
<div className="-mt-20 text-center">
|
|
2929
|
+
<div className="text-3xl font-semibold tabular-nums">
|
|
2930
|
+
{projectHealth.value}%
|
|
2931
|
+
</div>
|
|
2932
|
+
<div className="mt-1 text-xs text-muted-foreground">
|
|
2933
|
+
{t('charts.healthScore')}
|
|
2934
|
+
</div>
|
|
1071
2935
|
</div>
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
2936
|
+
</div>
|
|
2937
|
+
</ProjectChartCard>
|
|
2938
|
+
</div>
|
|
2939
|
+
</SectionCard>
|
|
2940
|
+
</TabsContent>
|
|
2941
|
+
) : null}
|
|
2942
|
+
|
|
2943
|
+
<TabsContent value="tasks" className="space-y-4">
|
|
2944
|
+
<SectionCard
|
|
2945
|
+
title={t('sections.taskBoard')}
|
|
2946
|
+
description={t('sections.taskBoardDescription')}
|
|
2947
|
+
className="rounded-3xl border bg-card p-4 shadow-sm"
|
|
2948
|
+
actions={
|
|
2949
|
+
!isLimitedView ? (
|
|
2950
|
+
<Button
|
|
2951
|
+
size="sm"
|
|
2952
|
+
variant="outline"
|
|
2953
|
+
className="gap-2"
|
|
2954
|
+
onClick={() => openCreateTaskForm()}
|
|
2955
|
+
>
|
|
2956
|
+
<Plus className="size-4" />
|
|
2957
|
+
{t('taskForm.titleNew')}
|
|
2958
|
+
</Button>
|
|
2959
|
+
) : undefined
|
|
2960
|
+
}
|
|
2961
|
+
>
|
|
2962
|
+
<div className="mb-4 flex flex-col gap-3 rounded-2xl border bg-muted/20 p-3 lg:flex-row lg:items-center lg:justify-between">
|
|
2963
|
+
<div className="relative min-w-0 flex-1">
|
|
2964
|
+
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
2965
|
+
<Input
|
|
2966
|
+
value={boardSearch}
|
|
2967
|
+
onChange={(event) => setBoardSearch(event.target.value)}
|
|
2968
|
+
placeholder={t('kanban.searchPlaceholder')}
|
|
2969
|
+
className="h-10 bg-background pl-9"
|
|
2970
|
+
/>
|
|
2971
|
+
</div>
|
|
2972
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
|
2973
|
+
<div className="flex items-center gap-2 rounded-xl border bg-background px-3 py-2 text-xs text-muted-foreground">
|
|
2974
|
+
<SlidersHorizontal className="size-4" />
|
|
2975
|
+
{t('kanban.filters')}
|
|
2976
|
+
</div>
|
|
2977
|
+
<Select
|
|
2978
|
+
value={boardPriorityFilter}
|
|
2979
|
+
onValueChange={(value) =>
|
|
2980
|
+
setBoardPriorityFilter(value as typeof boardPriorityFilter)
|
|
2981
|
+
}
|
|
2982
|
+
>
|
|
2983
|
+
<SelectTrigger className="h-10 w-full bg-background sm:w-44">
|
|
2984
|
+
<SelectValue />
|
|
2985
|
+
</SelectTrigger>
|
|
2986
|
+
<SelectContent>
|
|
2987
|
+
<SelectItem value="all">
|
|
2988
|
+
{t('kanban.allPriorities')}
|
|
2989
|
+
</SelectItem>
|
|
2990
|
+
<SelectItem value="high">
|
|
2991
|
+
{getTaskPriorityLabel('high')}
|
|
2992
|
+
</SelectItem>
|
|
2993
|
+
<SelectItem value="medium">
|
|
2994
|
+
{getTaskPriorityLabel('medium')}
|
|
2995
|
+
</SelectItem>
|
|
2996
|
+
<SelectItem value="low">
|
|
2997
|
+
{getTaskPriorityLabel('low')}
|
|
2998
|
+
</SelectItem>
|
|
2999
|
+
</SelectContent>
|
|
3000
|
+
</Select>
|
|
3001
|
+
<Select
|
|
3002
|
+
value={boardGroupMode}
|
|
3003
|
+
onValueChange={setBoardGroupMode}
|
|
3004
|
+
>
|
|
3005
|
+
<SelectTrigger className="h-10 w-full bg-background sm:w-44">
|
|
3006
|
+
<SelectValue />
|
|
3007
|
+
</SelectTrigger>
|
|
3008
|
+
<SelectContent>
|
|
3009
|
+
<SelectItem value="status">
|
|
3010
|
+
{t('kanban.groupStatus')}
|
|
3011
|
+
</SelectItem>
|
|
3012
|
+
</SelectContent>
|
|
3013
|
+
</Select>
|
|
3014
|
+
</div>
|
|
3015
|
+
</div>
|
|
3016
|
+
|
|
3017
|
+
<DndContext
|
|
3018
|
+
sensors={sensors}
|
|
3019
|
+
collisionDetection={kanbanCollision}
|
|
3020
|
+
onDragStart={onBoardDragStart}
|
|
3021
|
+
onDragCancel={() => setActiveDragTask(null)}
|
|
3022
|
+
onDragEnd={onBoardDragEnd}
|
|
3023
|
+
>
|
|
3024
|
+
<div className="relative">
|
|
3025
|
+
<div className="grid auto-cols-[minmax(19rem,1fr)] grid-flow-col gap-4 overflow-x-auto pb-2 xl:grid-flow-row xl:grid-cols-4 xl:overflow-visible xl:pb-0">
|
|
3026
|
+
{KANBAN_COLUMNS.map((column) => (
|
|
3027
|
+
<DroppableColumn key={column.id} columnId={column.id}>
|
|
3028
|
+
{(isOver) => (
|
|
3029
|
+
<div
|
|
3030
|
+
className={[
|
|
3031
|
+
'flex min-h-[32rem] flex-col overflow-hidden rounded-3xl border bg-linear-to-b p-3 transition-all',
|
|
3032
|
+
getColumnClassName(column.id),
|
|
3033
|
+
isOver
|
|
3034
|
+
? 'border-primary shadow-lg ring-2 ring-primary/15'
|
|
3035
|
+
: 'border-border',
|
|
3036
|
+
].join(' ')}
|
|
3037
|
+
>
|
|
3038
|
+
<div className="mb-3 flex items-center justify-between gap-3 rounded-2xl border bg-background/85 p-3 shadow-xs">
|
|
3039
|
+
<div className="min-w-0">
|
|
3040
|
+
<div className="flex items-center gap-2 text-sm font-semibold">
|
|
3041
|
+
<span
|
|
3042
|
+
className={[
|
|
3043
|
+
'size-2.5 rounded-full',
|
|
3044
|
+
getColumnDotClassName(column.id),
|
|
3045
|
+
].join(' ')}
|
|
3046
|
+
/>
|
|
3047
|
+
{column.label}
|
|
3048
|
+
</div>
|
|
3049
|
+
<div className="mt-1 text-xs text-muted-foreground">
|
|
3050
|
+
{filteredTaskColumns[column.id].length}/
|
|
3051
|
+
{taskColumns[column.id].length}{' '}
|
|
3052
|
+
{t('kanban.items')}
|
|
3053
|
+
</div>
|
|
3054
|
+
</div>
|
|
3055
|
+
<div className="flex items-center gap-1">
|
|
3056
|
+
<span className="rounded-full border bg-background px-2 py-0.5 text-xs font-medium text-muted-foreground">
|
|
3057
|
+
{filteredTaskColumns[column.id].length}
|
|
3058
|
+
</span>
|
|
3059
|
+
{!isLimitedView ? (
|
|
3060
|
+
<button
|
|
3061
|
+
type="button"
|
|
3062
|
+
className="flex size-5 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
|
3063
|
+
onClick={() => {
|
|
3064
|
+
setInlineCreateColumn(column.id);
|
|
3065
|
+
setInlineCreateName('');
|
|
3066
|
+
}}
|
|
3067
|
+
>
|
|
3068
|
+
<Plus className="size-3.5" />
|
|
3069
|
+
</button>
|
|
3070
|
+
) : null}
|
|
3071
|
+
</div>
|
|
3072
|
+
</div>
|
|
3073
|
+
|
|
3074
|
+
<div className="flex flex-1 flex-col gap-2">
|
|
3075
|
+
<AnimatePresence initial={false}>
|
|
3076
|
+
{filteredTaskColumns[column.id].map((task) => {
|
|
3077
|
+
const tags = getTaskTags(task);
|
|
3078
|
+
const comments = getTaskCommentCount(task);
|
|
3079
|
+
const attachments =
|
|
3080
|
+
getTaskAttachmentCount(task);
|
|
3081
|
+
return (
|
|
3082
|
+
<DraggableTaskCard
|
|
3083
|
+
key={task.id}
|
|
3084
|
+
task={task}
|
|
3085
|
+
disabled={false}
|
|
3086
|
+
>
|
|
3087
|
+
{(isDragging) => (
|
|
3088
|
+
<motion.div
|
|
3089
|
+
initial={{ opacity: 0, scale: 0.96 }}
|
|
3090
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
3091
|
+
exit={{
|
|
3092
|
+
opacity: 0,
|
|
3093
|
+
scale: 0.95,
|
|
3094
|
+
y: -4,
|
|
3095
|
+
}}
|
|
3096
|
+
transition={{ duration: 0.18 }}
|
|
3097
|
+
role="button"
|
|
3098
|
+
tabIndex={0}
|
|
3099
|
+
className={[
|
|
3100
|
+
'group w-full cursor-pointer rounded-2xl border bg-card p-3 text-left shadow-xs transition',
|
|
3101
|
+
isDragging
|
|
3102
|
+
? 'opacity-0'
|
|
3103
|
+
: 'hover:border-primary/40 hover:shadow-lg',
|
|
3104
|
+
].join(' ')}
|
|
3105
|
+
onClick={() =>
|
|
3106
|
+
setSelectedTask({
|
|
3107
|
+
...task,
|
|
3108
|
+
projectName: project?.name,
|
|
3109
|
+
projectCode: project?.code,
|
|
3110
|
+
})
|
|
3111
|
+
}
|
|
3112
|
+
onKeyDown={(event) => {
|
|
3113
|
+
if (
|
|
3114
|
+
event.key === 'Enter' ||
|
|
3115
|
+
event.key === ' '
|
|
3116
|
+
) {
|
|
3117
|
+
event.preventDefault();
|
|
3118
|
+
setSelectedTask({
|
|
3119
|
+
...task,
|
|
3120
|
+
projectName: project?.name,
|
|
3121
|
+
projectCode: project?.code,
|
|
3122
|
+
});
|
|
3123
|
+
}
|
|
3124
|
+
}}
|
|
3125
|
+
>
|
|
3126
|
+
<div className="mb-3 flex items-start justify-between gap-2">
|
|
3127
|
+
<div className="min-w-0 space-y-1">
|
|
3128
|
+
<p className="line-clamp-2 text-sm font-semibold leading-snug">
|
|
3129
|
+
{task.name}
|
|
3130
|
+
</p>
|
|
3131
|
+
{task.description ? (
|
|
3132
|
+
<p className="line-clamp-2 text-xs leading-5 text-muted-foreground">
|
|
3133
|
+
{task.description.replace(
|
|
3134
|
+
/<[^>]*>/g,
|
|
3135
|
+
''
|
|
3136
|
+
)}
|
|
3137
|
+
</p>
|
|
3138
|
+
) : null}
|
|
3139
|
+
</div>
|
|
3140
|
+
<div className="flex items-start gap-2">
|
|
3141
|
+
<span
|
|
3142
|
+
className={[
|
|
3143
|
+
'shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
|
|
3144
|
+
getPriorityClassName(
|
|
3145
|
+
task.priority
|
|
3146
|
+
),
|
|
3147
|
+
].join(' ')}
|
|
3148
|
+
>
|
|
3149
|
+
{getTaskPriorityLabel(
|
|
3150
|
+
task.priority
|
|
3151
|
+
)}
|
|
3152
|
+
</span>
|
|
3153
|
+
<Button
|
|
3154
|
+
type="button"
|
|
3155
|
+
variant="ghost"
|
|
3156
|
+
size="icon"
|
|
3157
|
+
className="size-7 shrink-0 rounded-full opacity-0 transition group-hover:opacity-100"
|
|
3158
|
+
onPointerDown={(event) =>
|
|
3159
|
+
event.stopPropagation()
|
|
3160
|
+
}
|
|
3161
|
+
onClick={(event) => {
|
|
3162
|
+
event.stopPropagation();
|
|
3163
|
+
openEditTaskForm(task);
|
|
3164
|
+
}}
|
|
3165
|
+
>
|
|
3166
|
+
<Pencil className="size-3.5" />
|
|
3167
|
+
</Button>
|
|
3168
|
+
</div>
|
|
3169
|
+
</div>
|
|
3170
|
+
|
|
3171
|
+
{tags.length > 0 ? (
|
|
3172
|
+
<div className="mb-3 flex flex-wrap gap-1">
|
|
3173
|
+
{tags.slice(0, 4).map((tag) => (
|
|
3174
|
+
<span
|
|
3175
|
+
key={`${task.id}-${tag}`}
|
|
3176
|
+
className="rounded-full border bg-muted/60 px-2 py-0.5 text-[10px] font-medium text-muted-foreground"
|
|
3177
|
+
>
|
|
3178
|
+
{tag}
|
|
3179
|
+
</span>
|
|
3180
|
+
))}
|
|
3181
|
+
{tags.length > 4 ? (
|
|
3182
|
+
<span className="rounded-full border bg-muted/60 px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
|
|
3183
|
+
+{tags.length - 4}
|
|
3184
|
+
</span>
|
|
3185
|
+
) : null}
|
|
3186
|
+
</div>
|
|
3187
|
+
) : null}
|
|
3188
|
+
|
|
3189
|
+
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
3190
|
+
<div
|
|
3191
|
+
className={[
|
|
3192
|
+
'rounded-xl border bg-muted/20 px-2 py-1.5',
|
|
3193
|
+
isPastDue(task.dueDate) &&
|
|
3194
|
+
task.status !== 'done'
|
|
3195
|
+
? 'border-rose-500/30 bg-rose-500/10 text-rose-700 dark:text-rose-300'
|
|
3196
|
+
: 'text-muted-foreground',
|
|
3197
|
+
].join(' ')}
|
|
3198
|
+
>
|
|
3199
|
+
<span className="flex items-center gap-1">
|
|
3200
|
+
<AlarmClock className="size-3.5" />
|
|
3201
|
+
{formatDate(
|
|
3202
|
+
task.dueDate,
|
|
3203
|
+
getSettingValue,
|
|
3204
|
+
currentLocaleCode
|
|
3205
|
+
)}
|
|
3206
|
+
</span>
|
|
3207
|
+
</div>
|
|
3208
|
+
<div className="rounded-xl border bg-muted/20 px-2 py-1.5 text-muted-foreground">
|
|
3209
|
+
<span className="flex items-center gap-1">
|
|
3210
|
+
<Timer className="size-3.5" />
|
|
3211
|
+
{task.estimateHours != null
|
|
3212
|
+
? `${task.estimateHours}h`
|
|
3213
|
+
: t('kanban.noEstimate')}
|
|
3214
|
+
</span>
|
|
3215
|
+
</div>
|
|
3216
|
+
</div>
|
|
3217
|
+
|
|
3218
|
+
<div className="mt-3 space-y-1.5">
|
|
3219
|
+
<div className="flex items-center justify-between text-[11px] text-muted-foreground">
|
|
3220
|
+
<span>{t('kanban.progress')}</span>
|
|
3221
|
+
<span>
|
|
3222
|
+
{getTaskProgress(task.status)}%
|
|
3223
|
+
</span>
|
|
3224
|
+
</div>
|
|
3225
|
+
<Progress
|
|
3226
|
+
value={getTaskProgress(task.status)}
|
|
3227
|
+
className="h-1.5"
|
|
3228
|
+
/>
|
|
3229
|
+
</div>
|
|
3230
|
+
|
|
3231
|
+
<div className="mt-3 flex items-center justify-between gap-3 border-t pt-3">
|
|
3232
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
3233
|
+
<div className="flex size-7 shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted text-[10px] font-semibold uppercase text-muted-foreground ring-1 ring-border">
|
|
3234
|
+
{(() => {
|
|
3235
|
+
const photoUrl =
|
|
3236
|
+
getUserPhotoUrl(
|
|
3237
|
+
task.assigneeUserPhotoId
|
|
3238
|
+
);
|
|
3239
|
+
const avatarUrl =
|
|
3240
|
+
task.assigneePersonAvatarId
|
|
3241
|
+
? getPersonAvatarUrl(
|
|
3242
|
+
task.assigneePersonAvatarId
|
|
3243
|
+
)
|
|
3244
|
+
: null;
|
|
3245
|
+
const imgSrc =
|
|
3246
|
+
photoUrl ?? avatarUrl;
|
|
3247
|
+
return imgSrc ? (
|
|
3248
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
3249
|
+
<img
|
|
3250
|
+
src={imgSrc}
|
|
3251
|
+
alt={
|
|
3252
|
+
task.assigneeName ||
|
|
3253
|
+
commonT(
|
|
3254
|
+
'labels.notAssigned'
|
|
3255
|
+
)
|
|
3256
|
+
}
|
|
3257
|
+
className="size-full object-cover"
|
|
3258
|
+
/>
|
|
3259
|
+
) : (
|
|
3260
|
+
getInitials(task.assigneeName)
|
|
3261
|
+
);
|
|
3262
|
+
})()}
|
|
3263
|
+
</div>
|
|
3264
|
+
<span className="truncate text-[11px] text-muted-foreground">
|
|
3265
|
+
{task.assigneeName ||
|
|
3266
|
+
commonT('labels.notAssigned')}
|
|
3267
|
+
</span>
|
|
3268
|
+
</div>
|
|
3269
|
+
<div className="flex shrink-0 items-center gap-2 text-[11px] text-muted-foreground">
|
|
3270
|
+
<span className="inline-flex items-center gap-1">
|
|
3271
|
+
<MessageSquare className="size-3.5" />
|
|
3272
|
+
{comments}
|
|
3273
|
+
</span>
|
|
3274
|
+
<span className="inline-flex items-center gap-1">
|
|
3275
|
+
<Paperclip className="size-3.5" />
|
|
3276
|
+
{attachments}
|
|
3277
|
+
</span>
|
|
3278
|
+
</div>
|
|
3279
|
+
</div>
|
|
3280
|
+
</motion.div>
|
|
3281
|
+
)}
|
|
3282
|
+
</DraggableTaskCard>
|
|
3283
|
+
);
|
|
3284
|
+
})}
|
|
3285
|
+
</AnimatePresence>
|
|
3286
|
+
{filteredTaskColumns[column.id].length === 0 ? (
|
|
3287
|
+
<div className="rounded-2xl border border-dashed bg-background/70 p-4 text-center text-xs text-muted-foreground">
|
|
3288
|
+
{boardSearch || boardPriorityFilter !== 'all'
|
|
3289
|
+
? t('kanban.noFilteredTasks')
|
|
3290
|
+
: t('kanban.emptyColumn')}
|
|
3291
|
+
</div>
|
|
3292
|
+
) : null}
|
|
3293
|
+
{!isLimitedView &&
|
|
3294
|
+
inlineCreateColumn === column.id ? (
|
|
3295
|
+
<div className="space-y-1.5 rounded-2xl border bg-card p-2 shadow-sm">
|
|
3296
|
+
<Input
|
|
3297
|
+
autoFocus
|
|
3298
|
+
placeholder={t('taskForm.namePlaceholder')}
|
|
3299
|
+
value={inlineCreateName}
|
|
3300
|
+
onChange={(e) =>
|
|
3301
|
+
setInlineCreateName(e.target.value)
|
|
3302
|
+
}
|
|
3303
|
+
onKeyDown={(e) => {
|
|
3304
|
+
if (e.key === 'Enter') {
|
|
3305
|
+
e.preventDefault();
|
|
3306
|
+
void handleInlineCreateTask(
|
|
3307
|
+
column.id,
|
|
3308
|
+
inlineCreateName
|
|
3309
|
+
);
|
|
3310
|
+
} else if (e.key === 'Escape') {
|
|
3311
|
+
setInlineCreateColumn(null);
|
|
3312
|
+
setInlineCreateName('');
|
|
3313
|
+
}
|
|
3314
|
+
}}
|
|
3315
|
+
onBlur={() => {
|
|
3316
|
+
if (!inlineCreateName.trim()) {
|
|
3317
|
+
setInlineCreateColumn(null);
|
|
3318
|
+
setInlineCreateName('');
|
|
3319
|
+
}
|
|
3320
|
+
}}
|
|
3321
|
+
disabled={inlineCreateLoading}
|
|
3322
|
+
className="h-8 text-sm"
|
|
3323
|
+
/>
|
|
3324
|
+
<div className="flex gap-1">
|
|
3325
|
+
<Button
|
|
3326
|
+
type="button"
|
|
3327
|
+
size="sm"
|
|
3328
|
+
className="h-7 px-2 text-xs"
|
|
3329
|
+
disabled={
|
|
3330
|
+
!inlineCreateName.trim() ||
|
|
3331
|
+
inlineCreateLoading
|
|
3332
|
+
}
|
|
3333
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
3334
|
+
onClick={() =>
|
|
3335
|
+
void handleInlineCreateTask(
|
|
3336
|
+
column.id,
|
|
3337
|
+
inlineCreateName
|
|
3338
|
+
)
|
|
3339
|
+
}
|
|
3340
|
+
>
|
|
3341
|
+
{t('taskForm.titleNew')}
|
|
3342
|
+
</Button>
|
|
3343
|
+
<Button
|
|
3344
|
+
type="button"
|
|
3345
|
+
variant="ghost"
|
|
3346
|
+
size="sm"
|
|
3347
|
+
className="h-7 px-2 text-xs"
|
|
3348
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
3349
|
+
onClick={() => {
|
|
3350
|
+
setInlineCreateColumn(null);
|
|
3351
|
+
setInlineCreateName('');
|
|
3352
|
+
}}
|
|
3353
|
+
>
|
|
3354
|
+
{commonT('actions.cancel')}
|
|
3355
|
+
</Button>
|
|
3356
|
+
</div>
|
|
3357
|
+
</div>
|
|
3358
|
+
) : !isLimitedView ? (
|
|
3359
|
+
<button
|
|
3360
|
+
type="button"
|
|
3361
|
+
className="mt-auto flex w-full cursor-pointer items-center justify-center gap-1 rounded-2xl border border-dashed bg-background/70 px-3 py-2 text-xs text-muted-foreground transition hover:border-primary/40 hover:bg-primary/5 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
|
3362
|
+
onClick={() => {
|
|
3363
|
+
setInlineCreateColumn(column.id);
|
|
3364
|
+
setInlineCreateName('');
|
|
3365
|
+
}}
|
|
3366
|
+
>
|
|
3367
|
+
<Plus className="size-3" />
|
|
3368
|
+
{t('taskForm.titleNew')}
|
|
3369
|
+
</button>
|
|
3370
|
+
) : null}
|
|
3371
|
+
</div>
|
|
3372
|
+
</div>
|
|
3373
|
+
)}
|
|
3374
|
+
</DroppableColumn>
|
|
3375
|
+
))}
|
|
3376
|
+
</div>
|
|
3377
|
+
{/* Scroll fade overlay — visible only on mobile/tablet */}
|
|
3378
|
+
<div
|
|
3379
|
+
aria-hidden
|
|
3380
|
+
className="pointer-events-none absolute inset-y-0 right-0 w-16 bg-linear-to-l from-background/80 to-transparent xl:hidden"
|
|
3381
|
+
/>
|
|
3382
|
+
</div>
|
|
3383
|
+
{/* DragOverlay renders the floating card following the pointer */}
|
|
3384
|
+
<DragOverlay dropAnimation={{ duration: 160, easing: 'ease' }}>
|
|
3385
|
+
{activeDragTask
|
|
3386
|
+
? (() => {
|
|
3387
|
+
const overlayTask = activeDragTask;
|
|
3388
|
+
const overlayTags = getTaskTags(overlayTask);
|
|
3389
|
+
const overlayComments = getTaskCommentCount(overlayTask);
|
|
3390
|
+
const overlayAttachments =
|
|
3391
|
+
getTaskAttachmentCount(overlayTask);
|
|
3392
|
+
return (
|
|
3393
|
+
<div className="w-76 cursor-grabbing rounded-2xl border border-primary/60 bg-card p-3 shadow-2xl ring-2 ring-primary/20 opacity-95">
|
|
3394
|
+
<div className="mb-3 flex items-start justify-between gap-2">
|
|
3395
|
+
<div className="min-w-0 space-y-1">
|
|
3396
|
+
<p className="line-clamp-2 text-sm font-semibold leading-snug">
|
|
3397
|
+
{overlayTask.name}
|
|
3398
|
+
</p>
|
|
3399
|
+
{overlayTask.description ? (
|
|
3400
|
+
<p className="line-clamp-2 text-xs leading-5 text-muted-foreground">
|
|
3401
|
+
{overlayTask.description.replace(
|
|
3402
|
+
/<[^>]*>/g,
|
|
3403
|
+
''
|
|
3404
|
+
)}
|
|
3405
|
+
</p>
|
|
3406
|
+
) : null}
|
|
3407
|
+
</div>
|
|
3408
|
+
<span
|
|
3409
|
+
className={[
|
|
3410
|
+
'shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
|
|
3411
|
+
getPriorityClassName(overlayTask.priority),
|
|
3412
|
+
].join(' ')}
|
|
3413
|
+
>
|
|
3414
|
+
{getTaskPriorityLabel(overlayTask.priority)}
|
|
3415
|
+
</span>
|
|
3416
|
+
</div>
|
|
3417
|
+
|
|
3418
|
+
{overlayTags.length > 0 ? (
|
|
3419
|
+
<div className="mb-3 flex flex-wrap gap-1">
|
|
3420
|
+
{overlayTags.slice(0, 4).map((tag) => (
|
|
3421
|
+
<span
|
|
3422
|
+
key={tag}
|
|
3423
|
+
className="rounded-full border bg-muted/60 px-2 py-0.5 text-[10px] font-medium text-muted-foreground"
|
|
3424
|
+
>
|
|
3425
|
+
{tag}
|
|
3426
|
+
</span>
|
|
3427
|
+
))}
|
|
3428
|
+
{overlayTags.length > 4 ? (
|
|
3429
|
+
<span className="rounded-full border bg-muted/60 px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
|
|
3430
|
+
+{overlayTags.length - 4}
|
|
3431
|
+
</span>
|
|
3432
|
+
) : null}
|
|
3433
|
+
</div>
|
|
3434
|
+
) : null}
|
|
3435
|
+
|
|
3436
|
+
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
3437
|
+
<div
|
|
3438
|
+
className={[
|
|
3439
|
+
'rounded-xl border bg-muted/20 px-2 py-1.5',
|
|
3440
|
+
isPastDue(overlayTask.dueDate) &&
|
|
3441
|
+
overlayTask.status !== 'done'
|
|
3442
|
+
? 'border-rose-500/30 bg-rose-500/10 text-rose-700 dark:text-rose-300'
|
|
3443
|
+
: 'text-muted-foreground',
|
|
3444
|
+
].join(' ')}
|
|
3445
|
+
>
|
|
3446
|
+
<span className="flex items-center gap-1">
|
|
3447
|
+
<AlarmClock className="size-3.5" />
|
|
3448
|
+
{formatDate(
|
|
3449
|
+
overlayTask.dueDate,
|
|
3450
|
+
getSettingValue,
|
|
3451
|
+
currentLocaleCode
|
|
3452
|
+
)}
|
|
3453
|
+
</span>
|
|
3454
|
+
</div>
|
|
3455
|
+
<div className="rounded-xl border bg-muted/20 px-2 py-1.5 text-muted-foreground">
|
|
3456
|
+
<span className="flex items-center gap-1">
|
|
3457
|
+
<Timer className="size-3.5" />
|
|
3458
|
+
{overlayTask.estimateHours != null
|
|
3459
|
+
? `${overlayTask.estimateHours}h`
|
|
3460
|
+
: t('kanban.noEstimate')}
|
|
3461
|
+
</span>
|
|
3462
|
+
</div>
|
|
3463
|
+
</div>
|
|
3464
|
+
|
|
3465
|
+
<div className="mt-3 space-y-1.5">
|
|
3466
|
+
<div className="flex items-center justify-between text-[11px] text-muted-foreground">
|
|
3467
|
+
<span>{t('kanban.progress')}</span>
|
|
3468
|
+
<span>
|
|
3469
|
+
{getTaskProgress(overlayTask.status)}%
|
|
3470
|
+
</span>
|
|
3471
|
+
</div>
|
|
3472
|
+
<Progress
|
|
3473
|
+
value={getTaskProgress(overlayTask.status)}
|
|
3474
|
+
className="h-1.5"
|
|
3475
|
+
/>
|
|
3476
|
+
</div>
|
|
3477
|
+
|
|
3478
|
+
<div className="mt-3 flex items-center justify-between gap-3 border-t pt-3">
|
|
3479
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
3480
|
+
<div className="flex size-7 shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted text-[10px] font-semibold uppercase text-muted-foreground ring-1 ring-border">
|
|
3481
|
+
{(() => {
|
|
3482
|
+
const photoUrl = getUserPhotoUrl(
|
|
3483
|
+
overlayTask.assigneeUserPhotoId
|
|
3484
|
+
);
|
|
3485
|
+
const avatarUrl =
|
|
3486
|
+
overlayTask.assigneePersonAvatarId
|
|
3487
|
+
? getPersonAvatarUrl(
|
|
3488
|
+
overlayTask.assigneePersonAvatarId
|
|
3489
|
+
)
|
|
3490
|
+
: null;
|
|
3491
|
+
const imgSrc = photoUrl ?? avatarUrl;
|
|
3492
|
+
return imgSrc ? (
|
|
3493
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
3494
|
+
<img
|
|
3495
|
+
src={imgSrc}
|
|
3496
|
+
alt={
|
|
3497
|
+
overlayTask.assigneeName ||
|
|
3498
|
+
commonT('labels.notAssigned')
|
|
3499
|
+
}
|
|
3500
|
+
className="size-full object-cover"
|
|
3501
|
+
/>
|
|
3502
|
+
) : (
|
|
3503
|
+
getInitials(overlayTask.assigneeName)
|
|
3504
|
+
);
|
|
3505
|
+
})()}
|
|
3506
|
+
</div>
|
|
3507
|
+
<span className="truncate text-[11px] text-muted-foreground">
|
|
3508
|
+
{overlayTask.assigneeName ||
|
|
3509
|
+
commonT('labels.notAssigned')}
|
|
3510
|
+
</span>
|
|
3511
|
+
</div>
|
|
3512
|
+
<div className="flex shrink-0 items-center gap-2 text-[11px] text-muted-foreground">
|
|
3513
|
+
<span className="inline-flex items-center gap-1">
|
|
3514
|
+
<MessageSquare className="size-3.5" />
|
|
3515
|
+
{overlayComments}
|
|
3516
|
+
</span>
|
|
3517
|
+
<span className="inline-flex items-center gap-1">
|
|
3518
|
+
<Paperclip className="size-3.5" />
|
|
3519
|
+
{overlayAttachments}
|
|
3520
|
+
</span>
|
|
3521
|
+
</div>
|
|
3522
|
+
</div>
|
|
3523
|
+
</div>
|
|
3524
|
+
);
|
|
3525
|
+
})()
|
|
3526
|
+
: null}
|
|
3527
|
+
</DragOverlay>
|
|
3528
|
+
</DndContext>
|
|
3529
|
+
</SectionCard>
|
|
3530
|
+
</TabsContent>
|
|
3531
|
+
|
|
3532
|
+
<TabsContent value="timeline">
|
|
3533
|
+
<SectionCard
|
|
3534
|
+
title={t('sections.timeline')}
|
|
3535
|
+
description={t('sections.timelineDescription')}
|
|
3536
|
+
className="rounded-3xl border bg-card p-4 shadow-sm"
|
|
3537
|
+
actions={
|
|
3538
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
3539
|
+
<Select
|
|
3540
|
+
value={timelineTypeFilter}
|
|
3541
|
+
onValueChange={(value) => {
|
|
3542
|
+
setTimelineTypeFilter(value as typeof timelineTypeFilter);
|
|
3543
|
+
setTimelineVisibleCount(8);
|
|
3544
|
+
}}
|
|
3545
|
+
>
|
|
3546
|
+
<SelectTrigger className="h-9 w-44 bg-background">
|
|
3547
|
+
<SelectValue />
|
|
3548
|
+
</SelectTrigger>
|
|
3549
|
+
<SelectContent>
|
|
3550
|
+
<SelectItem value="all">
|
|
3551
|
+
{t('timeline.filters.all')}
|
|
3552
|
+
</SelectItem>
|
|
3553
|
+
<SelectItem value="task">
|
|
3554
|
+
{t('timeline.filters.task')}
|
|
3555
|
+
</SelectItem>
|
|
3556
|
+
<SelectItem value="status">
|
|
3557
|
+
{t('timeline.filters.status')}
|
|
3558
|
+
</SelectItem>
|
|
3559
|
+
<SelectItem value="timesheet">
|
|
3560
|
+
{t('timeline.filters.timesheet')}
|
|
3561
|
+
</SelectItem>
|
|
3562
|
+
<SelectItem value="approval">
|
|
3563
|
+
{t('timeline.filters.approval')}
|
|
3564
|
+
</SelectItem>
|
|
3565
|
+
<SelectItem value="comment">
|
|
3566
|
+
{t('timeline.filters.comment')}
|
|
3567
|
+
</SelectItem>
|
|
3568
|
+
</SelectContent>
|
|
3569
|
+
</Select>
|
|
3570
|
+
</div>
|
|
3571
|
+
}
|
|
3572
|
+
>
|
|
3573
|
+
<div className="rounded-3xl border bg-linear-to-b from-muted/30 to-background p-4">
|
|
3574
|
+
{groupedTimelineEvents.length > 0 ? (
|
|
3575
|
+
<div className="space-y-6">
|
|
3576
|
+
{groupedTimelineEvents.map((group) => (
|
|
3577
|
+
<div key={group.dayKey} className="space-y-3">
|
|
3578
|
+
<div className="sticky top-0 z-10 w-fit rounded-full border bg-background px-3 py-1 text-xs font-medium text-muted-foreground shadow-xs">
|
|
3579
|
+
{formatDate(
|
|
3580
|
+
group.dayKey,
|
|
1080
3581
|
getSettingValue,
|
|
1081
3582
|
currentLocaleCode
|
|
1082
3583
|
)}
|
|
1083
|
-
</
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
3584
|
+
</div>
|
|
3585
|
+
<div className="space-y-0">
|
|
3586
|
+
{group.events.map((event, index) => {
|
|
3587
|
+
const Icon = event.icon;
|
|
3588
|
+
return (
|
|
3589
|
+
<motion.div
|
|
3590
|
+
key={event.id}
|
|
3591
|
+
initial={{ opacity: 0, y: 8 }}
|
|
3592
|
+
animate={{ opacity: 1, y: 0 }}
|
|
3593
|
+
transition={{ duration: 0.18 }}
|
|
3594
|
+
className="grid grid-cols-[2rem_1fr] gap-3"
|
|
3595
|
+
>
|
|
3596
|
+
<div className="flex flex-col items-center">
|
|
3597
|
+
<div
|
|
3598
|
+
className={[
|
|
3599
|
+
'flex size-8 items-center justify-center rounded-full shadow-sm',
|
|
3600
|
+
event.toneClassName,
|
|
3601
|
+
].join(' ')}
|
|
3602
|
+
>
|
|
3603
|
+
<Icon className="size-4" />
|
|
3604
|
+
</div>
|
|
3605
|
+
{index < group.events.length - 1 ? (
|
|
3606
|
+
<div className="w-px flex-1 bg-border" />
|
|
3607
|
+
) : null}
|
|
3608
|
+
</div>
|
|
3609
|
+
<div className="pb-5">
|
|
3610
|
+
<div className="rounded-2xl border bg-card p-4 shadow-xs transition hover:shadow-sm">
|
|
3611
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
3612
|
+
<div className="min-w-0">
|
|
3613
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
3614
|
+
<span className="text-sm font-semibold">
|
|
3615
|
+
{event.title}
|
|
3616
|
+
</span>
|
|
3617
|
+
<span className="rounded-full border bg-muted/40 px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
|
|
3618
|
+
{t(`timeline.types.${event.type}`)}
|
|
3619
|
+
</span>
|
|
3620
|
+
</div>
|
|
3621
|
+
<p className="mt-1 line-clamp-2 text-sm text-muted-foreground">
|
|
3622
|
+
{event.description}
|
|
3623
|
+
</p>
|
|
3624
|
+
</div>
|
|
3625
|
+
<div className="shrink-0 text-xs text-muted-foreground">
|
|
3626
|
+
{formatRelativeTime(
|
|
3627
|
+
event.timestamp,
|
|
3628
|
+
currentLocaleCode
|
|
3629
|
+
)}
|
|
3630
|
+
</div>
|
|
3631
|
+
</div>
|
|
3632
|
+
<div className="mt-3 flex items-center gap-2 border-t pt-3">
|
|
3633
|
+
<Avatar className="size-7 border bg-muted">
|
|
3634
|
+
<AvatarImage
|
|
3635
|
+
src={
|
|
3636
|
+
getUserPhotoUrl(
|
|
3637
|
+
event.actorUserPhotoId
|
|
3638
|
+
) ||
|
|
3639
|
+
getPersonAvatarUrl(
|
|
3640
|
+
event.actorAvatarId
|
|
3641
|
+
)
|
|
3642
|
+
}
|
|
3643
|
+
alt={
|
|
3644
|
+
event.actorName ||
|
|
3645
|
+
commonT('labels.notAssigned')
|
|
3646
|
+
}
|
|
3647
|
+
/>
|
|
3648
|
+
<AvatarFallback className="text-[10px]">
|
|
3649
|
+
{getInitials(event.actorName)}
|
|
3650
|
+
</AvatarFallback>
|
|
3651
|
+
</Avatar>
|
|
3652
|
+
<span className="truncate text-xs text-muted-foreground">
|
|
3653
|
+
{event.actorName ||
|
|
3654
|
+
commonT('labels.notAssigned')}
|
|
3655
|
+
</span>
|
|
3656
|
+
</div>
|
|
3657
|
+
</div>
|
|
3658
|
+
</div>
|
|
3659
|
+
</motion.div>
|
|
3660
|
+
);
|
|
3661
|
+
})}
|
|
3662
|
+
</div>
|
|
1096
3663
|
</div>
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
<
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
3664
|
+
))}
|
|
3665
|
+
{visibleTimelineEvents.length <
|
|
3666
|
+
filteredTimelineEvents.length ? (
|
|
3667
|
+
<div className="flex justify-center">
|
|
3668
|
+
<Button
|
|
3669
|
+
type="button"
|
|
3670
|
+
variant="outline"
|
|
3671
|
+
size="sm"
|
|
3672
|
+
onClick={() =>
|
|
3673
|
+
setTimelineVisibleCount((current) => current + 8)
|
|
3674
|
+
}
|
|
3675
|
+
>
|
|
3676
|
+
{t('timeline.loadMore')}
|
|
3677
|
+
</Button>
|
|
1110
3678
|
</div>
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
<Button variant="outline" size="sm" asChild className="w-fit">
|
|
1114
|
-
<Link
|
|
1115
|
-
href={`/operations/contracts?edit=${project.relatedContract.id}`}
|
|
1116
|
-
>
|
|
1117
|
-
<FileText className="size-4" />
|
|
1118
|
-
{commonT('actions.openContract')}
|
|
1119
|
-
</Link>
|
|
1120
|
-
</Button>
|
|
3679
|
+
) : null}
|
|
1121
3680
|
</div>
|
|
1122
3681
|
) : (
|
|
1123
|
-
<
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
)}
|
|
1127
|
-
</SectionCard>
|
|
1128
|
-
</div>
|
|
1129
|
-
|
|
1130
|
-
<div className="grid gap-4 xl:grid-cols-12">
|
|
1131
|
-
<SectionCard
|
|
1132
|
-
title={t('sections.deliveryHealth')}
|
|
1133
|
-
description={t('sections.deliveryHealthDescription')}
|
|
1134
|
-
className="rounded-xl border bg-card p-4 shadow-sm xl:col-span-7"
|
|
1135
|
-
>
|
|
1136
|
-
<div className="grid gap-4 lg:grid-cols-2">
|
|
1137
|
-
<div className="rounded-lg border bg-muted/10 p-3">
|
|
1138
|
-
<div className="mb-2 flex items-center gap-2 text-sm font-medium">
|
|
1139
|
-
<BarChart3 className="size-4 text-sky-700" />
|
|
1140
|
-
{t('charts.allocationByCollaborator')}
|
|
3682
|
+
<div className="flex min-h-56 flex-col items-center justify-center rounded-2xl border border-dashed bg-background p-6 text-center">
|
|
3683
|
+
<div className="flex size-12 items-center justify-center rounded-2xl bg-muted text-muted-foreground">
|
|
3684
|
+
<GitCommitHorizontal className="size-6" />
|
|
1141
3685
|
</div>
|
|
1142
|
-
<
|
|
1143
|
-
|
|
1144
|
-
config={boardChartConfig}
|
|
1145
|
-
>
|
|
1146
|
-
<BarChart data={allocationChartData}>
|
|
1147
|
-
<CartesianGrid vertical={false} />
|
|
1148
|
-
<XAxis dataKey="name" tickLine={false} axisLine={false} />
|
|
1149
|
-
<YAxis tickLine={false} axisLine={false} width={28} />
|
|
1150
|
-
<ChartTooltip
|
|
1151
|
-
content={<ChartTooltipContent hideLabel />}
|
|
1152
|
-
/>
|
|
1153
|
-
<Bar
|
|
1154
|
-
dataKey="allocation"
|
|
1155
|
-
radius={6}
|
|
1156
|
-
fill="var(--color-allocation)"
|
|
1157
|
-
/>
|
|
1158
|
-
</BarChart>
|
|
1159
|
-
</ChartContainer>
|
|
1160
|
-
</div>
|
|
1161
|
-
|
|
1162
|
-
<div className="rounded-lg border bg-muted/10 p-3">
|
|
1163
|
-
<div className="mb-2 flex items-center gap-2 text-sm font-medium">
|
|
1164
|
-
<Rocket className="size-4 text-emerald-700" />
|
|
1165
|
-
{t('charts.weeklyVelocity')}
|
|
3686
|
+
<div className="mt-3 text-sm font-medium">
|
|
3687
|
+
{t('timeline.emptyTitle')}
|
|
1166
3688
|
</div>
|
|
1167
|
-
<
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
>
|
|
1171
|
-
<LineChart data={velocityChartData}>
|
|
1172
|
-
<CartesianGrid vertical={false} />
|
|
1173
|
-
<XAxis dataKey="week" tickLine={false} axisLine={false} />
|
|
1174
|
-
<YAxis tickLine={false} axisLine={false} width={28} />
|
|
1175
|
-
<ChartTooltip content={<ChartTooltipContent />} />
|
|
1176
|
-
<Line
|
|
1177
|
-
type="monotone"
|
|
1178
|
-
dataKey="loggedHours"
|
|
1179
|
-
stroke="var(--color-loggedHours)"
|
|
1180
|
-
strokeWidth={2.5}
|
|
1181
|
-
dot={{ r: 3 }}
|
|
1182
|
-
/>
|
|
1183
|
-
<Line
|
|
1184
|
-
type="monotone"
|
|
1185
|
-
dataKey="completedTasks"
|
|
1186
|
-
stroke="var(--color-allocation)"
|
|
1187
|
-
strokeWidth={2}
|
|
1188
|
-
dot={{ r: 3 }}
|
|
1189
|
-
/>
|
|
1190
|
-
</LineChart>
|
|
1191
|
-
</ChartContainer>
|
|
3689
|
+
<p className="mt-1 max-w-sm text-xs leading-5 text-muted-foreground">
|
|
3690
|
+
{t('timeline.empty')}
|
|
3691
|
+
</p>
|
|
1192
3692
|
</div>
|
|
1193
|
-
|
|
1194
|
-
</
|
|
3693
|
+
)}
|
|
3694
|
+
</div>
|
|
3695
|
+
</SectionCard>
|
|
3696
|
+
</TabsContent>
|
|
1195
3697
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
{
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
3698
|
+
<TabsContent value="archive">
|
|
3699
|
+
<SectionCard
|
|
3700
|
+
title={t('sections.archivedTasks')}
|
|
3701
|
+
description={t('sections.archivedTasksDescription')}
|
|
3702
|
+
className="rounded-xl border bg-card p-4 shadow-sm"
|
|
3703
|
+
>
|
|
3704
|
+
{archivedTasks.length > 0 ? (
|
|
3705
|
+
<div className="overflow-x-auto rounded-lg border bg-muted/10">
|
|
3706
|
+
<Table>
|
|
3707
|
+
<TableHeader>
|
|
3708
|
+
<TableRow>
|
|
3709
|
+
<TableHead>{commonT('labels.task')}</TableHead>
|
|
3710
|
+
<TableHead>{commonT('labels.status')}</TableHead>
|
|
3711
|
+
<TableHead>{t('taskForm.deadlineLabel')}</TableHead>
|
|
3712
|
+
<TableHead className="text-right">
|
|
3713
|
+
{commonT('labels.actions')}
|
|
3714
|
+
</TableHead>
|
|
3715
|
+
</TableRow>
|
|
3716
|
+
</TableHeader>
|
|
3717
|
+
<TableBody>
|
|
3718
|
+
{archivedTasks.map((task) => (
|
|
3719
|
+
<TableRow
|
|
3720
|
+
key={task.id}
|
|
3721
|
+
className="cursor-pointer hover:bg-muted/30"
|
|
3722
|
+
onClick={() => setSelectedTask(task)}
|
|
3723
|
+
>
|
|
3724
|
+
<TableCell>
|
|
3725
|
+
<div className="min-w-0">
|
|
3726
|
+
<div className="truncate font-medium">
|
|
3727
|
+
{task.name}
|
|
3728
|
+
</div>
|
|
3729
|
+
{task.description ? (
|
|
3730
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
3731
|
+
{task.description}
|
|
3732
|
+
</div>
|
|
3733
|
+
) : null}
|
|
3734
|
+
</div>
|
|
3735
|
+
</TableCell>
|
|
3736
|
+
<TableCell>
|
|
3737
|
+
<StatusBadge
|
|
3738
|
+
label={
|
|
3739
|
+
KANBAN_COLUMNS.find(
|
|
3740
|
+
(column) => column.id === task.status
|
|
3741
|
+
)?.label ?? formatEnumLabel(task.status)
|
|
3742
|
+
}
|
|
3743
|
+
className={getStatusBadgeClass(task.status)}
|
|
3744
|
+
/>
|
|
3745
|
+
</TableCell>
|
|
3746
|
+
<TableCell>
|
|
3747
|
+
{formatDate(
|
|
3748
|
+
task.dueDate,
|
|
3749
|
+
getSettingValue,
|
|
3750
|
+
currentLocaleCode
|
|
3751
|
+
)}
|
|
3752
|
+
</TableCell>
|
|
3753
|
+
<TableCell>
|
|
3754
|
+
<div className="flex justify-end gap-2">
|
|
3755
|
+
<Button
|
|
3756
|
+
variant="outline"
|
|
3757
|
+
size="sm"
|
|
3758
|
+
className="gap-2"
|
|
3759
|
+
disabled={restoringTaskId === task.id}
|
|
3760
|
+
onClick={(event) => {
|
|
3761
|
+
event.stopPropagation();
|
|
3762
|
+
void handleRestoreTask(task.id);
|
|
3763
|
+
}}
|
|
3764
|
+
>
|
|
3765
|
+
{restoringTaskId === task.id ? (
|
|
3766
|
+
<Loader2 className="size-4 animate-spin" />
|
|
3767
|
+
) : (
|
|
3768
|
+
<ArchiveRestore className="size-4" />
|
|
3769
|
+
)}
|
|
3770
|
+
{commonT('actions.unarchive')}
|
|
3771
|
+
</Button>
|
|
3772
|
+
<Button
|
|
3773
|
+
variant="destructive"
|
|
3774
|
+
size="sm"
|
|
3775
|
+
className="gap-2"
|
|
3776
|
+
onClick={(event) => {
|
|
3777
|
+
event.stopPropagation();
|
|
3778
|
+
setDeletePromptTask(task);
|
|
3779
|
+
}}
|
|
3780
|
+
>
|
|
3781
|
+
<Trash2 className="size-4" />
|
|
3782
|
+
{commonT('actions.delete')}
|
|
3783
|
+
</Button>
|
|
3784
|
+
</div>
|
|
3785
|
+
</TableCell>
|
|
3786
|
+
</TableRow>
|
|
3787
|
+
))}
|
|
3788
|
+
</TableBody>
|
|
3789
|
+
</Table>
|
|
1237
3790
|
</div>
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
3791
|
+
) : (
|
|
3792
|
+
<ChartEmptyState
|
|
3793
|
+
icon={Archive}
|
|
3794
|
+
title={commonT('states.emptyTitle')}
|
|
3795
|
+
description={t('emptyArchivedDescription')}
|
|
3796
|
+
/>
|
|
3797
|
+
)}
|
|
3798
|
+
</SectionCard>
|
|
3799
|
+
</TabsContent>
|
|
1242
3800
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
onClick={() => openCreateTaskForm()}
|
|
3801
|
+
<TabsContent value="team">
|
|
3802
|
+
<div className="grid gap-4 xl:grid-cols-12">
|
|
3803
|
+
<SectionCard
|
|
3804
|
+
title={t('sections.team')}
|
|
3805
|
+
description={t('sections.teamDescription')}
|
|
3806
|
+
className={[
|
|
3807
|
+
'rounded-2xl border bg-card p-4 shadow-sm',
|
|
3808
|
+
isLimitedView ? 'xl:col-span-12' : 'xl:col-span-8',
|
|
3809
|
+
].join(' ')}
|
|
1253
3810
|
>
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
<DndContext
|
|
1261
|
-
sensors={sensors}
|
|
1262
|
-
collisionDetection={closestCenter}
|
|
1263
|
-
onDragEnd={onBoardDragEnd}
|
|
1264
|
-
>
|
|
1265
|
-
<div className="grid gap-4 xl:grid-cols-4">
|
|
1266
|
-
{KANBAN_COLUMNS.map((column) => (
|
|
1267
|
-
<DroppableColumn key={column.id} columnId={column.id}>
|
|
1268
|
-
{(isOver) => (
|
|
1269
|
-
<div
|
|
1270
|
-
className={[
|
|
1271
|
-
'rounded-xl border bg-muted/20 p-3 transition-colors',
|
|
1272
|
-
isOver ? 'border-primary bg-primary/5' : 'border-border',
|
|
1273
|
-
].join(' ')}
|
|
1274
|
-
>
|
|
1275
|
-
<div className="mb-3 flex items-center justify-between">
|
|
1276
|
-
<div className="flex items-center gap-2 text-sm font-semibold">
|
|
1277
|
-
<Rows3 className="size-4 text-muted-foreground" />
|
|
1278
|
-
{column.label}
|
|
3811
|
+
{project.assignments.length > 0 ? (
|
|
3812
|
+
<div className="space-y-4">
|
|
3813
|
+
<div className="grid gap-3 sm:grid-cols-3">
|
|
3814
|
+
<div className="rounded-2xl border bg-emerald-500/10 p-3">
|
|
3815
|
+
<div className="text-xs text-muted-foreground">
|
|
3816
|
+
{t('teamPanel.available')}
|
|
1279
3817
|
</div>
|
|
1280
|
-
<div className="
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
3818
|
+
<div className="mt-1 text-2xl font-semibold text-emerald-700 dark:text-emerald-300">
|
|
3819
|
+
{availableAssignments}
|
|
3820
|
+
</div>
|
|
3821
|
+
</div>
|
|
3822
|
+
<div className="rounded-2xl border bg-amber-500/10 p-3">
|
|
3823
|
+
<div className="text-xs text-muted-foreground">
|
|
3824
|
+
{t('teamPanel.highAllocation')}
|
|
3825
|
+
</div>
|
|
3826
|
+
<div className="mt-1 text-2xl font-semibold text-amber-700 dark:text-amber-300">
|
|
3827
|
+
{highAllocationAssignments}
|
|
1284
3828
|
</div>
|
|
1285
3829
|
</div>
|
|
3830
|
+
<div className="rounded-2xl border bg-rose-500/10 p-3">
|
|
3831
|
+
<div className="text-xs text-muted-foreground">
|
|
3832
|
+
{t('teamPanel.overload')}
|
|
3833
|
+
</div>
|
|
3834
|
+
<div className="mt-1 text-2xl font-semibold text-rose-700 dark:text-rose-300">
|
|
3835
|
+
{overloadedAssignments}
|
|
3836
|
+
</div>
|
|
3837
|
+
</div>
|
|
3838
|
+
</div>
|
|
1286
3839
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
3840
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
3841
|
+
{project.assignments.map((assignment) => {
|
|
3842
|
+
const allocationValue =
|
|
3843
|
+
typeof assignment.allocationPercent === 'number'
|
|
3844
|
+
? Math.round(assignment.allocationPercent)
|
|
3845
|
+
: 0;
|
|
3846
|
+
const allocation = clampPercent(allocationValue);
|
|
3847
|
+
const weeklyHours = assignment.weeklyHours ?? 0;
|
|
3848
|
+
const usedHours =
|
|
3849
|
+
weeklyHours > 0
|
|
3850
|
+
? (weeklyHours * Math.max(allocationValue, 0)) / 100
|
|
3851
|
+
: 0;
|
|
3852
|
+
const availablePercent = Math.max(
|
|
3853
|
+
0,
|
|
3854
|
+
100 - allocationValue
|
|
3855
|
+
);
|
|
3856
|
+
const availabilityHours =
|
|
3857
|
+
weeklyHours > 0
|
|
3858
|
+
? Math.max(0, weeklyHours - usedHours)
|
|
3859
|
+
: 0;
|
|
3860
|
+
const tone = getAllocationTone(allocationValue);
|
|
3861
|
+
const ToneIcon = tone.icon;
|
|
3862
|
+
|
|
3863
|
+
return (
|
|
3864
|
+
<motion.div
|
|
3865
|
+
key={assignment.id}
|
|
3866
|
+
whileHover={{ y: -2 }}
|
|
3867
|
+
className={[
|
|
3868
|
+
'overflow-hidden rounded-2xl border bg-background shadow-xs transition hover:shadow-md',
|
|
3869
|
+
tone.border,
|
|
3870
|
+
].join(' ')}
|
|
1293
3871
|
>
|
|
1294
|
-
|
|
1295
|
-
<div
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
onClick={() => setSelectedTask(task)}
|
|
1305
|
-
onKeyDown={(event) => {
|
|
1306
|
-
if (
|
|
1307
|
-
event.key === 'Enter' ||
|
|
1308
|
-
event.key === ' '
|
|
1309
|
-
) {
|
|
1310
|
-
event.preventDefault();
|
|
1311
|
-
setSelectedTask(task);
|
|
1312
|
-
}
|
|
1313
|
-
}}
|
|
1314
|
-
>
|
|
1315
|
-
<div className="mb-2 flex items-start justify-between gap-2">
|
|
1316
|
-
<p className="text-sm font-medium leading-snug">
|
|
1317
|
-
{task.name}
|
|
1318
|
-
</p>
|
|
1319
|
-
<div className="flex items-start gap-2">
|
|
1320
|
-
<span
|
|
1321
|
-
className={[
|
|
1322
|
-
'shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
|
|
1323
|
-
task.priority === 'high'
|
|
1324
|
-
? 'bg-rose-100 text-rose-700'
|
|
1325
|
-
: task.priority === 'medium'
|
|
1326
|
-
? 'bg-amber-100 text-amber-700'
|
|
1327
|
-
: 'bg-emerald-100 text-emerald-700',
|
|
1328
|
-
].join(' ')}
|
|
1329
|
-
>
|
|
1330
|
-
{getTaskPriorityLabel(task.priority)}
|
|
1331
|
-
</span>
|
|
1332
|
-
<Button
|
|
1333
|
-
type="button"
|
|
1334
|
-
variant="ghost"
|
|
1335
|
-
size="icon"
|
|
1336
|
-
className="size-7 shrink-0 rounded-full"
|
|
1337
|
-
onPointerDown={(event) =>
|
|
1338
|
-
event.stopPropagation()
|
|
3872
|
+
<div className="border-b bg-linear-to-br from-muted/50 to-background p-4">
|
|
3873
|
+
<div className="flex items-start justify-between gap-3">
|
|
3874
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
3875
|
+
<Avatar className="size-12 border bg-muted">
|
|
3876
|
+
<AvatarImage
|
|
3877
|
+
src={
|
|
3878
|
+
getUserPhotoUrl(assignment.userPhotoId) ||
|
|
3879
|
+
getPersonAvatarUrl(
|
|
3880
|
+
assignment.personAvatarId
|
|
3881
|
+
)
|
|
1339
3882
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
}
|
|
1344
|
-
>
|
|
1345
|
-
|
|
1346
|
-
|
|
3883
|
+
alt={assignment.collaboratorName}
|
|
3884
|
+
/>
|
|
3885
|
+
<AvatarFallback className="text-xs font-semibold">
|
|
3886
|
+
{getInitials(assignment.collaboratorName)}
|
|
3887
|
+
</AvatarFallback>
|
|
3888
|
+
</Avatar>
|
|
3889
|
+
<div className="min-w-0">
|
|
3890
|
+
<div className="truncate text-sm font-semibold">
|
|
3891
|
+
{assignment.collaboratorName}
|
|
3892
|
+
</div>
|
|
3893
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
3894
|
+
{assignment.roleLabel ||
|
|
3895
|
+
commonT('labels.notAssigned')}
|
|
3896
|
+
</div>
|
|
1347
3897
|
</div>
|
|
1348
3898
|
</div>
|
|
3899
|
+
<div className="flex shrink-0 flex-col items-end gap-2">
|
|
3900
|
+
<StatusBadge
|
|
3901
|
+
label={formatEnumLabel(assignment.status)}
|
|
3902
|
+
className={getStatusBadgeClass(
|
|
3903
|
+
assignment.status
|
|
3904
|
+
)}
|
|
3905
|
+
/>
|
|
3906
|
+
<span
|
|
3907
|
+
className={[
|
|
3908
|
+
'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] font-semibold',
|
|
3909
|
+
tone.border,
|
|
3910
|
+
tone.bg,
|
|
3911
|
+
tone.text,
|
|
3912
|
+
].join(' ')}
|
|
3913
|
+
>
|
|
3914
|
+
<ToneIcon className="size-3" />
|
|
3915
|
+
{t(`teamPanel.status.${tone.labelKey}`)}
|
|
3916
|
+
</span>
|
|
3917
|
+
</div>
|
|
3918
|
+
</div>
|
|
3919
|
+
</div>
|
|
1349
3920
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
)
|
|
3921
|
+
<div className="space-y-4 p-4">
|
|
3922
|
+
<div className="space-y-2">
|
|
3923
|
+
<div className="flex items-center justify-between text-xs">
|
|
3924
|
+
<span className="text-muted-foreground">
|
|
3925
|
+
{commonT('labels.allocationPercent')}
|
|
3926
|
+
</span>
|
|
3927
|
+
<span
|
|
3928
|
+
className={['font-semibold', tone.text].join(
|
|
3929
|
+
' '
|
|
3930
|
+
)}
|
|
3931
|
+
>
|
|
3932
|
+
{formatPercent(assignment.allocationPercent)}
|
|
3933
|
+
</span>
|
|
3934
|
+
</div>
|
|
3935
|
+
<Progress
|
|
3936
|
+
value={allocation}
|
|
3937
|
+
className={['h-2.5', tone.progress].join(' ')}
|
|
3938
|
+
/>
|
|
3939
|
+
{allocationValue > 100 ? (
|
|
3940
|
+
<div className="flex items-center gap-1 text-xs text-rose-700 dark:text-rose-300">
|
|
3941
|
+
<AlertTriangle className="size-3.5" />
|
|
3942
|
+
{t('teamPanel.overloadWarning', {
|
|
3943
|
+
value: allocationValue - 100,
|
|
3944
|
+
})}
|
|
1360
3945
|
</div>
|
|
1361
3946
|
) : null}
|
|
3947
|
+
</div>
|
|
1362
3948
|
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
{
|
|
1367
|
-
|
|
3949
|
+
<div className="grid grid-cols-2 gap-3 text-xs xl:grid-cols-4">
|
|
3950
|
+
<div className="rounded-xl border bg-muted/20 p-2">
|
|
3951
|
+
<div className="text-muted-foreground">
|
|
3952
|
+
{commonT('labels.weeklyCapacity')}
|
|
3953
|
+
</div>
|
|
3954
|
+
<div className="mt-1 font-semibold">
|
|
3955
|
+
{weeklyHours
|
|
3956
|
+
? formatHours(weeklyHours)
|
|
3957
|
+
: commonT('labels.notAvailable')}
|
|
3958
|
+
</div>
|
|
3959
|
+
</div>
|
|
3960
|
+
<div className="rounded-xl border bg-muted/20 p-2">
|
|
3961
|
+
<div className="text-muted-foreground">
|
|
3962
|
+
{t('teamPanel.usedHours')}
|
|
3963
|
+
</div>
|
|
3964
|
+
<div className="mt-1 font-semibold">
|
|
3965
|
+
{weeklyHours
|
|
3966
|
+
? formatHours(usedHours)
|
|
3967
|
+
: commonT('labels.notAvailable')}
|
|
3968
|
+
</div>
|
|
3969
|
+
</div>
|
|
3970
|
+
<div className="rounded-xl border bg-muted/20 p-2">
|
|
3971
|
+
<div className="text-muted-foreground">
|
|
3972
|
+
{t('teamPanel.availability')}
|
|
3973
|
+
</div>
|
|
3974
|
+
<div className="mt-1 font-semibold">
|
|
3975
|
+
{weeklyHours
|
|
3976
|
+
? formatHours(availabilityHours)
|
|
3977
|
+
: `${clampPercent(availablePercent)}%`}
|
|
3978
|
+
</div>
|
|
3979
|
+
</div>
|
|
3980
|
+
<div className="rounded-xl border bg-muted/20 p-2">
|
|
3981
|
+
<div className="text-muted-foreground">
|
|
3982
|
+
{commonT('labels.timeline')}
|
|
3983
|
+
</div>
|
|
3984
|
+
<div className="mt-1 truncate font-semibold">
|
|
3985
|
+
{formatDateRange(
|
|
3986
|
+
assignment.startDate,
|
|
3987
|
+
assignment.endDate,
|
|
1368
3988
|
getSettingValue,
|
|
1369
3989
|
currentLocaleCode
|
|
1370
3990
|
)}
|
|
1371
|
-
</span>
|
|
1372
|
-
<span>
|
|
1373
|
-
{task.estimateHours != null
|
|
1374
|
-
? `${task.estimateHours}h`
|
|
1375
|
-
: ''}
|
|
1376
|
-
</span>
|
|
1377
|
-
</div>
|
|
1378
|
-
|
|
1379
|
-
{task.assigneeName ? (
|
|
1380
|
-
<div className="mt-2 flex items-center gap-1.5">
|
|
1381
|
-
<div className="flex size-5 shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted text-[9px] font-semibold uppercase text-muted-foreground ring-1 ring-border">
|
|
1382
|
-
{(() => {
|
|
1383
|
-
const photoUrl = getUserPhotoUrl(
|
|
1384
|
-
task.assigneeUserPhotoId
|
|
1385
|
-
);
|
|
1386
|
-
const avatarUrl =
|
|
1387
|
-
task.assigneePersonAvatarId
|
|
1388
|
-
? getPersonAvatarUrl(
|
|
1389
|
-
task.assigneePersonAvatarId
|
|
1390
|
-
)
|
|
1391
|
-
: null;
|
|
1392
|
-
const imgSrc = photoUrl ?? avatarUrl;
|
|
1393
|
-
return imgSrc ? (
|
|
1394
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
1395
|
-
<img
|
|
1396
|
-
src={imgSrc}
|
|
1397
|
-
alt={task.assigneeName}
|
|
1398
|
-
className="size-full object-cover"
|
|
1399
|
-
/>
|
|
1400
|
-
) : (
|
|
1401
|
-
getInitials(task.assigneeName)
|
|
1402
|
-
);
|
|
1403
|
-
})()}
|
|
1404
|
-
</div>
|
|
1405
|
-
<span className="truncate text-[11px] text-muted-foreground">
|
|
1406
|
-
{task.assigneeName}
|
|
1407
|
-
</span>
|
|
1408
3991
|
</div>
|
|
1409
|
-
|
|
3992
|
+
</div>
|
|
1410
3993
|
</div>
|
|
1411
|
-
|
|
1412
|
-
</
|
|
1413
|
-
)
|
|
1414
|
-
|
|
3994
|
+
</div>
|
|
3995
|
+
</motion.div>
|
|
3996
|
+
);
|
|
3997
|
+
})}
|
|
1415
3998
|
</div>
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
3999
|
+
</div>
|
|
4000
|
+
) : (
|
|
4001
|
+
<ChartEmptyState
|
|
4002
|
+
icon={Users}
|
|
4003
|
+
title={commonT('states.emptyTitle')}
|
|
4004
|
+
description={t('noAssignments')}
|
|
4005
|
+
/>
|
|
4006
|
+
)}
|
|
4007
|
+
</SectionCard>
|
|
1422
4008
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
4009
|
+
{!isLimitedView ? (
|
|
4010
|
+
<SectionCard
|
|
4011
|
+
title={t('sections.indicators')}
|
|
4012
|
+
description={t('sections.indicatorsDescription')}
|
|
4013
|
+
className="rounded-xl border bg-card p-4 shadow-sm xl:col-span-4"
|
|
4014
|
+
>
|
|
4015
|
+
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-1">
|
|
4016
|
+
{[
|
|
4017
|
+
{
|
|
4018
|
+
icon: Users,
|
|
4019
|
+
label: t('indicators.activeAssignments'),
|
|
4020
|
+
value: project.operationalIndicators.activeAssignments,
|
|
4021
|
+
tone: 'text-sky-700 dark:text-sky-300',
|
|
4022
|
+
bg: 'bg-sky-500/10',
|
|
4023
|
+
},
|
|
4024
|
+
{
|
|
4025
|
+
icon: CheckCircle2,
|
|
4026
|
+
label: t('indicators.completedAssignments'),
|
|
4027
|
+
value: project.operationalIndicators.completedAssignments,
|
|
4028
|
+
tone: 'text-emerald-700 dark:text-emerald-300',
|
|
4029
|
+
bg: 'bg-emerald-500/10',
|
|
4030
|
+
},
|
|
4031
|
+
{
|
|
4032
|
+
icon: Gauge,
|
|
4033
|
+
label: t('indicators.averageAllocation'),
|
|
4034
|
+
value: formatPercent(
|
|
4035
|
+
project.operationalIndicators.averageAllocation
|
|
4036
|
+
),
|
|
4037
|
+
tone:
|
|
4038
|
+
averageAllocation > 100
|
|
4039
|
+
? 'text-rose-700 dark:text-rose-300'
|
|
4040
|
+
: averageAllocation > 85
|
|
4041
|
+
? 'text-amber-700 dark:text-amber-300'
|
|
4042
|
+
: 'text-emerald-700 dark:text-emerald-300',
|
|
4043
|
+
bg:
|
|
4044
|
+
averageAllocation > 100
|
|
4045
|
+
? 'bg-rose-500/10'
|
|
4046
|
+
: averageAllocation > 85
|
|
4047
|
+
? 'bg-amber-500/10'
|
|
4048
|
+
: 'bg-emerald-500/10',
|
|
4049
|
+
},
|
|
4050
|
+
{
|
|
4051
|
+
icon: Timer,
|
|
4052
|
+
label: t('indicators.totalWeeklyHours'),
|
|
4053
|
+
value: formatHours(
|
|
4054
|
+
project.operationalIndicators.totalWeeklyHours
|
|
4055
|
+
),
|
|
4056
|
+
tone: 'text-violet-700 dark:text-violet-300',
|
|
4057
|
+
bg: 'bg-violet-500/10',
|
|
4058
|
+
},
|
|
4059
|
+
{
|
|
4060
|
+
icon: ClipboardList,
|
|
4061
|
+
label: t('cards.timesheets'),
|
|
4062
|
+
value: project.timesheetSummary.totalTimesheets,
|
|
4063
|
+
tone: 'text-foreground',
|
|
4064
|
+
bg: 'bg-muted/40',
|
|
4065
|
+
},
|
|
4066
|
+
{
|
|
4067
|
+
icon: AlarmClock,
|
|
4068
|
+
label: commonT('labels.pending'),
|
|
4069
|
+
value: project.timesheetSummary.pendingTimesheets,
|
|
4070
|
+
tone:
|
|
4071
|
+
project.timesheetSummary.pendingTimesheets > 0
|
|
4072
|
+
? 'text-amber-700 dark:text-amber-300'
|
|
4073
|
+
: 'text-foreground',
|
|
4074
|
+
bg:
|
|
4075
|
+
project.timesheetSummary.pendingTimesheets > 0
|
|
4076
|
+
? 'bg-amber-500/10'
|
|
4077
|
+
: 'bg-muted/40',
|
|
4078
|
+
},
|
|
4079
|
+
{
|
|
4080
|
+
icon: BarChart2,
|
|
4081
|
+
label: t('cards.loggedHours'),
|
|
4082
|
+
value: formatHours(project.timesheetSummary.totalHours),
|
|
4083
|
+
tone: 'text-sky-700 dark:text-sky-300',
|
|
4084
|
+
bg: 'bg-sky-500/10',
|
|
4085
|
+
},
|
|
4086
|
+
].map(({ icon: Icon, label, value, tone, bg }) => (
|
|
4087
|
+
<div
|
|
4088
|
+
key={label}
|
|
4089
|
+
className="flex items-center gap-3 rounded-xl border bg-card p-3 transition-shadow hover:shadow-sm"
|
|
4090
|
+
>
|
|
4091
|
+
<div
|
|
4092
|
+
className={[
|
|
4093
|
+
'flex size-9 shrink-0 items-center justify-center rounded-xl',
|
|
4094
|
+
bg,
|
|
4095
|
+
].join(' ')}
|
|
4096
|
+
>
|
|
4097
|
+
<Icon className={['size-4', tone].join(' ')} />
|
|
1456
4098
|
</div>
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
/>
|
|
1467
|
-
</TableCell>
|
|
1468
|
-
<TableCell>
|
|
1469
|
-
{formatDate(
|
|
1470
|
-
task.dueDate,
|
|
1471
|
-
getSettingValue,
|
|
1472
|
-
currentLocaleCode
|
|
1473
|
-
)}
|
|
1474
|
-
</TableCell>
|
|
1475
|
-
<TableCell>
|
|
1476
|
-
<div className="flex justify-end gap-2">
|
|
1477
|
-
<Button
|
|
1478
|
-
variant="outline"
|
|
1479
|
-
size="sm"
|
|
1480
|
-
className="gap-2"
|
|
1481
|
-
onClick={(event) => {
|
|
1482
|
-
event.stopPropagation();
|
|
1483
|
-
void handleRestoreTask(task.id);
|
|
1484
|
-
}}
|
|
1485
|
-
>
|
|
1486
|
-
<ArchiveRestore className="size-4" />
|
|
1487
|
-
{commonT('actions.unarchive')}
|
|
1488
|
-
</Button>
|
|
1489
|
-
<Button
|
|
1490
|
-
variant="destructive"
|
|
1491
|
-
size="sm"
|
|
1492
|
-
className="gap-2"
|
|
1493
|
-
onClick={(event) => {
|
|
1494
|
-
event.stopPropagation();
|
|
1495
|
-
setDeletePromptTask(task);
|
|
1496
|
-
}}
|
|
4099
|
+
<div className="min-w-0 flex-1">
|
|
4100
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
4101
|
+
{label}
|
|
4102
|
+
</div>
|
|
4103
|
+
<div
|
|
4104
|
+
className={[
|
|
4105
|
+
'mt-0.5 text-sm font-semibold tabular-nums',
|
|
4106
|
+
tone,
|
|
4107
|
+
].join(' ')}
|
|
1497
4108
|
>
|
|
1498
|
-
|
|
1499
|
-
{commonT('actions.delete')}
|
|
1500
|
-
</Button>
|
|
1501
|
-
</div>
|
|
1502
|
-
</TableCell>
|
|
1503
|
-
</TableRow>
|
|
1504
|
-
))}
|
|
1505
|
-
</TableBody>
|
|
1506
|
-
</Table>
|
|
1507
|
-
</div>
|
|
1508
|
-
) : (
|
|
1509
|
-
<p className="text-sm text-muted-foreground">
|
|
1510
|
-
{t('emptyArchivedDescription')}
|
|
1511
|
-
</p>
|
|
1512
|
-
)}
|
|
1513
|
-
</SectionCard>
|
|
1514
|
-
|
|
1515
|
-
<div className="grid gap-4 xl:grid-cols-12">
|
|
1516
|
-
<SectionCard
|
|
1517
|
-
title={t('sections.team')}
|
|
1518
|
-
description={t('sections.teamDescription')}
|
|
1519
|
-
className={[
|
|
1520
|
-
'rounded-xl border bg-card p-4 shadow-sm',
|
|
1521
|
-
isLimitedView ? 'xl:col-span-12' : 'xl:col-span-8',
|
|
1522
|
-
].join(' ')}
|
|
1523
|
-
>
|
|
1524
|
-
{project.assignments.length > 0 ? (
|
|
1525
|
-
<div className="overflow-x-auto rounded-lg border bg-muted/10">
|
|
1526
|
-
<Table>
|
|
1527
|
-
<TableHeader>
|
|
1528
|
-
<TableRow>
|
|
1529
|
-
<TableHead>{commonT('labels.collaborator')}</TableHead>
|
|
1530
|
-
<TableHead>{commonT('labels.role')}</TableHead>
|
|
1531
|
-
<TableHead className="hidden lg:table-cell">
|
|
1532
|
-
{commonT('labels.allocationPercent')}
|
|
1533
|
-
</TableHead>
|
|
1534
|
-
<TableHead>{commonT('labels.weeklyCapacity')}</TableHead>
|
|
1535
|
-
<TableHead className="hidden xl:table-cell">
|
|
1536
|
-
{commonT('labels.timeline')}
|
|
1537
|
-
</TableHead>
|
|
1538
|
-
<TableHead>{commonT('labels.status')}</TableHead>
|
|
1539
|
-
</TableRow>
|
|
1540
|
-
</TableHeader>
|
|
1541
|
-
<TableBody>
|
|
1542
|
-
{project.assignments.map((assignment) => (
|
|
1543
|
-
<TableRow key={assignment.id}>
|
|
1544
|
-
<TableCell>
|
|
1545
|
-
<div className="flex items-center gap-2">
|
|
1546
|
-
<Avatar className="h-8 w-8 border border-border/60 bg-muted">
|
|
1547
|
-
<AvatarImage
|
|
1548
|
-
src={
|
|
1549
|
-
getUserPhotoUrl(assignment.userPhotoId) ||
|
|
1550
|
-
getPersonAvatarUrl(assignment.personAvatarId)
|
|
1551
|
-
}
|
|
1552
|
-
alt={assignment.collaboratorName}
|
|
1553
|
-
/>
|
|
1554
|
-
<AvatarFallback className="bg-muted text-xs font-semibold text-foreground">
|
|
1555
|
-
{getInitials(assignment.collaboratorName)}
|
|
1556
|
-
</AvatarFallback>
|
|
1557
|
-
</Avatar>
|
|
1558
|
-
<span>{assignment.collaboratorName}</span>
|
|
4109
|
+
{value}
|
|
1559
4110
|
</div>
|
|
1560
|
-
</
|
|
1561
|
-
|
|
1562
|
-
{assignment.roleLabel || commonT('labels.notAssigned')}
|
|
1563
|
-
</TableCell>
|
|
1564
|
-
<TableCell className="hidden lg:table-cell">
|
|
1565
|
-
{formatPercent(assignment.allocationPercent)}
|
|
1566
|
-
</TableCell>
|
|
1567
|
-
<TableCell>
|
|
1568
|
-
{assignment.weeklyHours
|
|
1569
|
-
? formatHours(assignment.weeklyHours)
|
|
1570
|
-
: commonT('labels.notAvailable')}
|
|
1571
|
-
</TableCell>
|
|
1572
|
-
<TableCell className="hidden xl:table-cell">
|
|
1573
|
-
{formatDateRange(
|
|
1574
|
-
assignment.startDate,
|
|
1575
|
-
assignment.endDate,
|
|
1576
|
-
getSettingValue,
|
|
1577
|
-
currentLocaleCode
|
|
1578
|
-
)}
|
|
1579
|
-
</TableCell>
|
|
1580
|
-
<TableCell>
|
|
1581
|
-
<StatusBadge
|
|
1582
|
-
label={formatEnumLabel(assignment.status)}
|
|
1583
|
-
className={getStatusBadgeClass(assignment.status)}
|
|
1584
|
-
/>
|
|
1585
|
-
</TableCell>
|
|
1586
|
-
</TableRow>
|
|
4111
|
+
</div>
|
|
4112
|
+
</div>
|
|
1587
4113
|
))}
|
|
1588
|
-
</
|
|
1589
|
-
</
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
{t('noAssignments')}
|
|
1594
|
-
</p>
|
|
1595
|
-
)}
|
|
1596
|
-
</SectionCard>
|
|
4114
|
+
</div>
|
|
4115
|
+
</SectionCard>
|
|
4116
|
+
) : null}
|
|
4117
|
+
</div>
|
|
4118
|
+
</TabsContent>
|
|
1597
4119
|
|
|
1598
|
-
|
|
4120
|
+
<TabsContent value="costs">
|
|
1599
4121
|
<SectionCard
|
|
1600
|
-
title={t('sections.
|
|
1601
|
-
description={t('sections.
|
|
1602
|
-
className="rounded-
|
|
4122
|
+
title={t('sections.costs')}
|
|
4123
|
+
description={t('sections.costsDescription')}
|
|
4124
|
+
className="rounded-2xl border bg-card p-4 shadow-sm"
|
|
1603
4125
|
>
|
|
1604
|
-
<
|
|
1605
|
-
<div>
|
|
1606
|
-
<dt className="text-muted-foreground">
|
|
1607
|
-
{t('indicators.activeAssignments')}
|
|
1608
|
-
</dt>
|
|
1609
|
-
<dd className="font-medium">
|
|
1610
|
-
{project.operationalIndicators.activeAssignments}
|
|
1611
|
-
</dd>
|
|
1612
|
-
</div>
|
|
1613
|
-
<div>
|
|
1614
|
-
<dt className="text-muted-foreground">
|
|
1615
|
-
{t('indicators.completedAssignments')}
|
|
1616
|
-
</dt>
|
|
1617
|
-
<dd className="font-medium">
|
|
1618
|
-
{project.operationalIndicators.completedAssignments}
|
|
1619
|
-
</dd>
|
|
1620
|
-
</div>
|
|
1621
|
-
<div>
|
|
1622
|
-
<dt className="text-muted-foreground">
|
|
1623
|
-
{t('indicators.averageAllocation')}
|
|
1624
|
-
</dt>
|
|
1625
|
-
<dd className="font-medium">
|
|
1626
|
-
{formatPercent(
|
|
1627
|
-
project.operationalIndicators.averageAllocation
|
|
1628
|
-
)}
|
|
1629
|
-
</dd>
|
|
1630
|
-
</div>
|
|
1631
|
-
<div>
|
|
1632
|
-
<dt className="text-muted-foreground">
|
|
1633
|
-
{t('indicators.totalWeeklyHours')}
|
|
1634
|
-
</dt>
|
|
1635
|
-
<dd className="font-medium">
|
|
1636
|
-
{formatHours(project.operationalIndicators.totalWeeklyHours)}
|
|
1637
|
-
</dd>
|
|
1638
|
-
</div>
|
|
1639
|
-
<div>
|
|
1640
|
-
<dt className="text-muted-foreground">
|
|
1641
|
-
{t('cards.timesheets')}
|
|
1642
|
-
</dt>
|
|
1643
|
-
<dd className="font-medium">
|
|
1644
|
-
{project.timesheetSummary.totalTimesheets}
|
|
1645
|
-
</dd>
|
|
1646
|
-
</div>
|
|
1647
|
-
<div>
|
|
1648
|
-
<dt className="text-muted-foreground">
|
|
1649
|
-
{commonT('labels.pending')}
|
|
1650
|
-
</dt>
|
|
1651
|
-
<dd className="font-medium">
|
|
1652
|
-
{project.timesheetSummary.pendingTimesheets}
|
|
1653
|
-
</dd>
|
|
1654
|
-
</div>
|
|
1655
|
-
<div>
|
|
1656
|
-
<dt className="text-muted-foreground">
|
|
1657
|
-
{t('cards.loggedHours')}
|
|
1658
|
-
</dt>
|
|
1659
|
-
<dd className="font-medium">
|
|
1660
|
-
{formatHours(project.timesheetSummary.totalHours)}
|
|
1661
|
-
</dd>
|
|
1662
|
-
</div>
|
|
1663
|
-
</dl>
|
|
4126
|
+
<ProjectCostsSection projectId={projectId} />
|
|
1664
4127
|
</SectionCard>
|
|
1665
|
-
|
|
1666
|
-
</
|
|
4128
|
+
</TabsContent>
|
|
4129
|
+
</Tabs>
|
|
1667
4130
|
|
|
1668
4131
|
<TaskDetailSheet
|
|
1669
4132
|
task={selectedTask}
|
|
@@ -1685,9 +4148,14 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1685
4148
|
variant="outline"
|
|
1686
4149
|
size="sm"
|
|
1687
4150
|
className="h-10 gap-2"
|
|
4151
|
+
disabled={restoringTaskId === selectedTask.id}
|
|
1688
4152
|
onClick={() => void handleRestoreTask(selectedTask.id)}
|
|
1689
4153
|
>
|
|
1690
|
-
|
|
4154
|
+
{restoringTaskId === selectedTask.id ? (
|
|
4155
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
4156
|
+
) : (
|
|
4157
|
+
<ArchiveRestore className="size-3.5" />
|
|
4158
|
+
)}
|
|
1691
4159
|
{commonT('actions.unarchive')}
|
|
1692
4160
|
</Button>
|
|
1693
4161
|
<Button
|
|
@@ -1701,26 +4169,20 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1701
4169
|
</Button>
|
|
1702
4170
|
</>
|
|
1703
4171
|
) : (
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
<Button
|
|
1715
|
-
variant="outline"
|
|
1716
|
-
size="sm"
|
|
1717
|
-
className="h-10 gap-2"
|
|
1718
|
-
onClick={() => void handleArchiveTask(selectedTask.id)}
|
|
1719
|
-
>
|
|
4172
|
+
<Button
|
|
4173
|
+
variant="outline"
|
|
4174
|
+
size="sm"
|
|
4175
|
+
className="col-span-2 h-10 gap-2"
|
|
4176
|
+
disabled={archivingTaskId === selectedTask.id}
|
|
4177
|
+
onClick={() => void handleArchiveTask(selectedTask.id)}
|
|
4178
|
+
>
|
|
4179
|
+
{archivingTaskId === selectedTask.id ? (
|
|
4180
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
4181
|
+
) : (
|
|
1720
4182
|
<Archive className="size-3.5" />
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
4183
|
+
)}
|
|
4184
|
+
{commonT('actions.archive')}
|
|
4185
|
+
</Button>
|
|
1724
4186
|
)}
|
|
1725
4187
|
</div>
|
|
1726
4188
|
) : null
|
|
@@ -1755,7 +4217,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1755
4217
|
) : null}
|
|
1756
4218
|
|
|
1757
4219
|
{!isLimitedView ? (
|
|
1758
|
-
<
|
|
4220
|
+
<Sheet
|
|
1759
4221
|
open={taskFormOpen}
|
|
1760
4222
|
onOpenChange={(open) => {
|
|
1761
4223
|
if (!open) {
|
|
@@ -1765,16 +4227,16 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1765
4227
|
}
|
|
1766
4228
|
}}
|
|
1767
4229
|
>
|
|
1768
|
-
<
|
|
1769
|
-
<
|
|
1770
|
-
<
|
|
4230
|
+
<SheetContent className="flex w-full flex-col overflow-hidden sm:max-w-xl">
|
|
4231
|
+
<SheetHeader>
|
|
4232
|
+
<SheetTitle>
|
|
1771
4233
|
{editingTaskId
|
|
1772
4234
|
? t('taskForm.titleEdit')
|
|
1773
4235
|
: t('taskForm.titleNew')}
|
|
1774
|
-
</
|
|
1775
|
-
</
|
|
4236
|
+
</SheetTitle>
|
|
4237
|
+
</SheetHeader>
|
|
1776
4238
|
|
|
1777
|
-
<div className="space-y-4">
|
|
4239
|
+
<div className="flex-1 space-y-4 overflow-y-auto px-4 py-2">
|
|
1778
4240
|
<div className="space-y-1.5">
|
|
1779
4241
|
<Label htmlFor="task-name">{t('taskForm.nameLabel')} *</Label>
|
|
1780
4242
|
<Input
|
|
@@ -1794,15 +4256,12 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1794
4256
|
<Label htmlFor="task-description">
|
|
1795
4257
|
{t('taskForm.descriptionLabel')}
|
|
1796
4258
|
</Label>
|
|
1797
|
-
<
|
|
1798
|
-
id="task-description"
|
|
1799
|
-
placeholder={t('taskForm.descriptionPlaceholder')}
|
|
1800
|
-
rows={3}
|
|
4259
|
+
<RichTextEditor
|
|
1801
4260
|
value={taskFormData.description}
|
|
1802
|
-
onChange={(
|
|
4261
|
+
onChange={(val) =>
|
|
1803
4262
|
setTaskFormData((prev) => ({
|
|
1804
4263
|
...prev,
|
|
1805
|
-
description:
|
|
4264
|
+
description: val,
|
|
1806
4265
|
}))
|
|
1807
4266
|
}
|
|
1808
4267
|
/>
|
|
@@ -1942,33 +4401,71 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1942
4401
|
}
|
|
1943
4402
|
/>
|
|
1944
4403
|
</div>
|
|
4404
|
+
|
|
4405
|
+
{editingTaskId ? (
|
|
4406
|
+
<div className="space-y-1.5">
|
|
4407
|
+
<Label className="flex items-center gap-1.5">
|
|
4408
|
+
<Paperclip className="size-3.5" />
|
|
4409
|
+
{t('taskForm.attachmentsLabel')}
|
|
4410
|
+
</Label>
|
|
4411
|
+
<TaskFileAttachments taskId={editingTaskId} />
|
|
4412
|
+
</div>
|
|
4413
|
+
) : null}
|
|
1945
4414
|
</div>
|
|
1946
4415
|
|
|
1947
|
-
<
|
|
1948
|
-
<
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
:
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
4416
|
+
<div className="mt-4 flex flex-wrap items-center justify-between gap-2 border-t px-4 pb-4 pt-4">
|
|
4417
|
+
<div className="flex gap-2">
|
|
4418
|
+
{editingTaskId ? (
|
|
4419
|
+
<Button
|
|
4420
|
+
type="button"
|
|
4421
|
+
variant="outline"
|
|
4422
|
+
disabled={
|
|
4423
|
+
taskFormLoading || archivingTaskId === editingTaskId
|
|
4424
|
+
}
|
|
4425
|
+
onClick={() => {
|
|
4426
|
+
if (!editingTaskId) return;
|
|
4427
|
+
const id = editingTaskId;
|
|
4428
|
+
setTaskFormOpen(false);
|
|
4429
|
+
setEditingTaskId(null);
|
|
4430
|
+
setTaskFormData(EMPTY_TASK_FORM);
|
|
4431
|
+
void handleArchiveTask(id);
|
|
4432
|
+
}}
|
|
4433
|
+
>
|
|
4434
|
+
{archivingTaskId === editingTaskId ? (
|
|
4435
|
+
<Loader2 className="mr-2 size-4 animate-spin" />
|
|
4436
|
+
) : (
|
|
4437
|
+
<Archive className="mr-2 size-4" />
|
|
4438
|
+
)}
|
|
4439
|
+
{commonT('actions.archive')}
|
|
4440
|
+
</Button>
|
|
4441
|
+
) : null}
|
|
4442
|
+
</div>
|
|
4443
|
+
<div className="flex gap-2">
|
|
4444
|
+
<Button
|
|
4445
|
+
variant="outline"
|
|
4446
|
+
onClick={() => {
|
|
4447
|
+
setTaskFormOpen(false);
|
|
4448
|
+
setEditingTaskId(null);
|
|
4449
|
+
setTaskFormData(EMPTY_TASK_FORM);
|
|
4450
|
+
}}
|
|
4451
|
+
disabled={taskFormLoading}
|
|
4452
|
+
>
|
|
4453
|
+
{commonT('actions.cancel')}
|
|
4454
|
+
</Button>
|
|
4455
|
+
<Button
|
|
4456
|
+
onClick={() => void handleTaskFormSubmit()}
|
|
4457
|
+
disabled={taskFormLoading || !taskFormData.name.trim()}
|
|
4458
|
+
>
|
|
4459
|
+
{taskFormLoading
|
|
4460
|
+
? t('taskForm.saving')
|
|
4461
|
+
: editingTaskId
|
|
4462
|
+
? commonT('actions.save')
|
|
4463
|
+
: commonT('actions.create')}
|
|
4464
|
+
</Button>
|
|
4465
|
+
</div>
|
|
4466
|
+
</div>
|
|
4467
|
+
</SheetContent>
|
|
4468
|
+
</Sheet>
|
|
1972
4469
|
) : null}
|
|
1973
4470
|
|
|
1974
4471
|
{!isLimitedView ? (
|
|
@@ -1996,8 +4493,12 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1996
4493
|
{deletePromptTask ? (
|
|
1997
4494
|
<Button
|
|
1998
4495
|
variant="destructive"
|
|
4496
|
+
disabled={deletingTaskId === deletePromptTask.id}
|
|
1999
4497
|
onClick={() => void handleDeleteTask(deletePromptTask.id)}
|
|
2000
4498
|
>
|
|
4499
|
+
{deletingTaskId === deletePromptTask.id ? (
|
|
4500
|
+
<Loader2 className="mr-2 size-3.5 animate-spin" />
|
|
4501
|
+
) : null}
|
|
2001
4502
|
{commonT('actions.delete')}
|
|
2002
4503
|
</Button>
|
|
2003
4504
|
) : null}
|