@hed-hog/operations 0.0.330 → 0.0.332
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/README.md +5 -5
- package/dist/controllers/operations-collaborators.controller.d.ts +58 -213
- package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
- package/dist/controllers/operations-collaborators.controller.js +100 -0
- package/dist/controllers/operations-collaborators.controller.js.map +1 -1
- package/dist/controllers/operations-contracts.controller.d.ts +6 -6
- package/dist/controllers/operations-projects.controller.d.ts +25 -0
- package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
- package/dist/controllers/operations-projects.controller.js +48 -0
- package/dist/controllers/operations-projects.controller.js.map +1 -1
- package/dist/controllers/operations-reports.controller.d.ts +1 -1
- package/dist/controllers/operations-tasks.controller.d.ts +34 -9
- package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
- package/dist/controllers/operations-tasks.controller.js +43 -32
- package/dist/controllers/operations-tasks.controller.js.map +1 -1
- package/dist/controllers/operations-timesheets.controller.d.ts +9 -9
- package/dist/dashboard/components/DashboardLayout.d.ts +30 -0
- package/dist/dashboard/components/DashboardLayout.d.ts.map +1 -0
- package/dist/dashboard/components/DashboardLayout.js +87 -0
- package/dist/dashboard/components/DashboardLayout.js.map +1 -0
- package/dist/dashboard/components/widget-registry.d.ts +23 -0
- package/dist/dashboard/components/widget-registry.d.ts.map +1 -0
- package/dist/dashboard/components/widget-registry.js +245 -0
- package/dist/dashboard/components/widget-registry.js.map +1 -0
- package/dist/dashboard/hooks/useDashboardData.d.ts +20 -0
- package/dist/dashboard/hooks/useDashboardData.d.ts.map +1 -0
- package/dist/dashboard/hooks/useDashboardData.js +24 -0
- package/dist/dashboard/hooks/useDashboardData.js.map +1 -0
- package/dist/dashboard/types/widgets.types.d.ts +233 -0
- package/dist/dashboard/types/widgets.types.d.ts.map +1 -0
- package/dist/dashboard/types/widgets.types.js +6 -0
- package/dist/dashboard/types/widgets.types.js.map +1 -0
- package/dist/dashboard/widgets/CapacityDistribution.d.ts +23 -0
- package/dist/dashboard/widgets/CapacityDistribution.d.ts.map +1 -0
- package/dist/dashboard/widgets/CapacityDistribution.js +11 -0
- package/dist/dashboard/widgets/CapacityDistribution.js.map +1 -0
- package/dist/dashboard/widgets/EffortByProject.d.ts +22 -0
- package/dist/dashboard/widgets/EffortByProject.d.ts.map +1 -0
- package/dist/dashboard/widgets/EffortByProject.js +11 -0
- package/dist/dashboard/widgets/EffortByProject.js.map +1 -0
- package/dist/dashboard/widgets/HeadcountByArea.d.ts +24 -0
- package/dist/dashboard/widgets/HeadcountByArea.d.ts.map +1 -0
- package/dist/dashboard/widgets/HeadcountByArea.js +11 -0
- package/dist/dashboard/widgets/HeadcountByArea.js.map +1 -0
- package/dist/dashboard/widgets/ManagedProjectsStatus.d.ts +18 -0
- package/dist/dashboard/widgets/ManagedProjectsStatus.d.ts.map +1 -0
- package/dist/dashboard/widgets/ManagedProjectsStatus.js +12 -0
- package/dist/dashboard/widgets/ManagedProjectsStatus.js.map +1 -0
- package/dist/dashboard/widgets/MyHoursPeriodKpi.d.ts +22 -0
- package/dist/dashboard/widgets/MyHoursPeriodKpi.d.ts.map +1 -0
- package/dist/dashboard/widgets/MyHoursPeriodKpi.js +12 -0
- package/dist/dashboard/widgets/MyHoursPeriodKpi.js.map +1 -0
- package/dist/dashboard/widgets/MyOpenRequestsKpi.d.ts +19 -0
- package/dist/dashboard/widgets/MyOpenRequestsKpi.d.ts.map +1 -0
- package/dist/dashboard/widgets/MyOpenRequestsKpi.js +17 -0
- package/dist/dashboard/widgets/MyOpenRequestsKpi.js.map +1 -0
- package/dist/dashboard/widgets/MyPendingRequestsList.d.ts +23 -0
- package/dist/dashboard/widgets/MyPendingRequestsList.d.ts.map +1 -0
- package/dist/dashboard/widgets/MyPendingRequestsList.js +14 -0
- package/dist/dashboard/widgets/MyPendingRequestsList.js.map +1 -0
- package/dist/dashboard/widgets/MyProjectAllocationsKpi.d.ts +22 -0
- package/dist/dashboard/widgets/MyProjectAllocationsKpi.d.ts.map +1 -0
- package/dist/dashboard/widgets/MyProjectAllocationsKpi.js +11 -0
- package/dist/dashboard/widgets/MyProjectAllocationsKpi.js.map +1 -0
- package/dist/dashboard/widgets/MyQuickActions.d.ts +23 -0
- package/dist/dashboard/widgets/MyQuickActions.d.ts.map +1 -0
- package/dist/dashboard/widgets/MyQuickActions.js +18 -0
- package/dist/dashboard/widgets/MyQuickActions.js.map +1 -0
- package/dist/dashboard/widgets/MyRelevantDeadlines.d.ts +23 -0
- package/dist/dashboard/widgets/MyRelevantDeadlines.d.ts.map +1 -0
- package/dist/dashboard/widgets/MyRelevantDeadlines.js +22 -0
- package/dist/dashboard/widgets/MyRelevantDeadlines.js.map +1 -0
- package/dist/dashboard/widgets/MyTimesheetStatusKpi.d.ts +17 -0
- package/dist/dashboard/widgets/MyTimesheetStatusKpi.d.ts.map +1 -0
- package/dist/dashboard/widgets/MyTimesheetStatusKpi.js +11 -0
- package/dist/dashboard/widgets/MyTimesheetStatusKpi.js.map +1 -0
- package/dist/dashboard/widgets/MyWeeklyJourney.d.ts +21 -0
- package/dist/dashboard/widgets/MyWeeklyJourney.d.ts.map +1 -0
- package/dist/dashboard/widgets/MyWeeklyJourney.js +19 -0
- package/dist/dashboard/widgets/MyWeeklyJourney.js.map +1 -0
- package/dist/dashboard/widgets/PortfolioCostsKpi.d.ts +19 -0
- package/dist/dashboard/widgets/PortfolioCostsKpi.d.ts.map +1 -0
- package/dist/dashboard/widgets/PortfolioCostsKpi.js +12 -0
- package/dist/dashboard/widgets/PortfolioCostsKpi.js.map +1 -0
- package/dist/dashboard/widgets/PortfolioEffortKpi.d.ts +18 -0
- package/dist/dashboard/widgets/PortfolioEffortKpi.d.ts.map +1 -0
- package/dist/dashboard/widgets/PortfolioEffortKpi.js +8 -0
- package/dist/dashboard/widgets/PortfolioEffortKpi.js.map +1 -0
- package/dist/dashboard/widgets/PortfolioProjectsKpi.d.ts +22 -0
- package/dist/dashboard/widgets/PortfolioProjectsKpi.d.ts.map +1 -0
- package/dist/dashboard/widgets/PortfolioProjectsKpi.js +56 -0
- package/dist/dashboard/widgets/PortfolioProjectsKpi.js.map +1 -0
- package/dist/dashboard/widgets/PortfolioRiskKpi.d.ts +19 -0
- package/dist/dashboard/widgets/PortfolioRiskKpi.d.ts.map +1 -0
- package/dist/dashboard/widgets/PortfolioRiskKpi.js +11 -0
- package/dist/dashboard/widgets/PortfolioRiskKpi.js.map +1 -0
- package/dist/dashboard/widgets/ProjectStatusOverview.d.ts +19 -0
- package/dist/dashboard/widgets/ProjectStatusOverview.d.ts.map +1 -0
- package/dist/dashboard/widgets/ProjectStatusOverview.js +18 -0
- package/dist/dashboard/widgets/ProjectStatusOverview.js.map +1 -0
- package/dist/dashboard/widgets/StrategicDeadlines.d.ts +24 -0
- package/dist/dashboard/widgets/StrategicDeadlines.d.ts.map +1 -0
- package/dist/dashboard/widgets/StrategicDeadlines.js +22 -0
- package/dist/dashboard/widgets/StrategicDeadlines.js.map +1 -0
- package/dist/dashboard/widgets/TeamApprovalQueue.d.ts +24 -0
- package/dist/dashboard/widgets/TeamApprovalQueue.d.ts.map +1 -0
- package/dist/dashboard/widgets/TeamApprovalQueue.js +12 -0
- package/dist/dashboard/widgets/TeamApprovalQueue.js.map +1 -0
- package/dist/dashboard/widgets/TeamCapacityKpi.d.ts +18 -0
- package/dist/dashboard/widgets/TeamCapacityKpi.d.ts.map +1 -0
- package/dist/dashboard/widgets/TeamCapacityKpi.js +19 -0
- package/dist/dashboard/widgets/TeamCapacityKpi.js.map +1 -0
- package/dist/dashboard/widgets/TeamHeadcountKpi.d.ts +22 -0
- package/dist/dashboard/widgets/TeamHeadcountKpi.d.ts.map +1 -0
- package/dist/dashboard/widgets/TeamHeadcountKpi.js +56 -0
- package/dist/dashboard/widgets/TeamHeadcountKpi.js.map +1 -0
- package/dist/dashboard/widgets/TeamHoursKpi.d.ts +19 -0
- package/dist/dashboard/widgets/TeamHoursKpi.d.ts.map +1 -0
- package/dist/dashboard/widgets/TeamHoursKpi.js +13 -0
- package/dist/dashboard/widgets/TeamHoursKpi.js.map +1 -0
- package/dist/dashboard/widgets/TeamPendingApprovalsKpi.d.ts +20 -0
- package/dist/dashboard/widgets/TeamPendingApprovalsKpi.d.ts.map +1 -0
- package/dist/dashboard/widgets/TeamPendingApprovalsKpi.js +11 -0
- package/dist/dashboard/widgets/TeamPendingApprovalsKpi.js.map +1 -0
- package/dist/dashboard/widgets/TeamUtilizationOverview.d.ts +18 -0
- package/dist/dashboard/widgets/TeamUtilizationOverview.d.ts.map +1 -0
- package/dist/dashboard/widgets/TeamUtilizationOverview.js +17 -0
- package/dist/dashboard/widgets/TeamUtilizationOverview.js.map +1 -0
- package/dist/dashboard/widgets/TeamWorkloadAlerts.d.ts +24 -0
- package/dist/dashboard/widgets/TeamWorkloadAlerts.d.ts.map +1 -0
- package/dist/dashboard/widgets/TeamWorkloadAlerts.js +19 -0
- package/dist/dashboard/widgets/TeamWorkloadAlerts.js.map +1 -0
- package/dist/dashboard/widgets/index.d.ts +24 -0
- package/dist/dashboard/widgets/index.d.ts.map +1 -0
- package/dist/dashboard/widgets/index.js +54 -0
- package/dist/dashboard/widgets/index.js.map +1 -0
- package/dist/dto/create-collaborator-invoice.dto.d.ts +11 -0
- package/dist/dto/create-collaborator-invoice.dto.d.ts.map +1 -0
- package/dist/dto/create-collaborator-invoice.dto.js +55 -0
- package/dist/dto/create-collaborator-invoice.dto.js.map +1 -0
- package/dist/dto/create-collaborator-payment.dto.d.ts +10 -0
- package/dist/dto/create-collaborator-payment.dto.d.ts.map +1 -0
- package/dist/dto/create-collaborator-payment.dto.js +50 -0
- package/dist/dto/create-collaborator-payment.dto.js.map +1 -0
- package/dist/dto/create-collaborator.dto.d.ts +0 -1
- package/dist/dto/create-collaborator.dto.d.ts.map +1 -1
- package/dist/dto/create-collaborator.dto.js +0 -6
- package/dist/dto/create-collaborator.dto.js.map +1 -1
- package/dist/dto/list-collaborator-invoice.dto.d.ts +4 -0
- package/dist/dto/list-collaborator-invoice.dto.d.ts.map +1 -0
- package/dist/dto/list-collaborator-invoice.dto.js +8 -0
- package/dist/dto/list-collaborator-invoice.dto.js.map +1 -0
- package/dist/dto/list-collaborator-payment.dto.d.ts +4 -0
- package/dist/dto/list-collaborator-payment.dto.d.ts.map +1 -0
- package/dist/dto/list-collaborator-payment.dto.js +8 -0
- package/dist/dto/list-collaborator-payment.dto.js.map +1 -0
- package/dist/dto/update-collaborator-invoice.dto.d.ts +6 -0
- package/dist/dto/update-collaborator-invoice.dto.d.ts.map +1 -0
- package/dist/dto/update-collaborator-invoice.dto.js +9 -0
- package/dist/dto/update-collaborator-invoice.dto.js.map +1 -0
- package/dist/dto/update-collaborator-payment.dto.d.ts +6 -0
- package/dist/dto/update-collaborator-payment.dto.d.ts.map +1 -0
- package/dist/dto/update-collaborator-payment.dto.js +9 -0
- package/dist/dto/update-collaborator-payment.dto.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/operations.controller.d.ts +42 -0
- package/dist/operations.controller.d.ts.map +1 -1
- package/dist/operations.service.d.ts +258 -268
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +2381 -1341
- package/dist/operations.service.js.map +1 -1
- package/dist/operations.service.spec.js +345 -174
- package/dist/operations.service.spec.js.map +1 -1
- package/hedhog/data/dashboard_component.yaml +66 -0
- package/hedhog/data/dashboard_item.yaml +25 -25
- package/hedhog/data/menu.yaml +27 -8
- package/hedhog/data/route.yaml +133 -0
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +78 -102
- package/hedhog/frontend/app/_components/collaborator-invoices-tab.tsx.ejs +443 -0
- package/hedhog/frontend/app/_components/collaborator-payment-history-tab.tsx.ejs +429 -0
- package/hedhog/frontend/app/_components/collaborator-picker.tsx.ejs +158 -0
- package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +247 -50
- package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +643 -450
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +992 -431
- package/hedhog/frontend/app/_components/project-file-attachments.tsx.ejs +371 -0
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +558 -386
- package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +383 -157
- package/hedhog/frontend/app/_components/task-file-attachments.tsx.ejs +4 -1
- package/hedhog/frontend/app/_components/task-form-fields.tsx.ejs +406 -0
- package/hedhog/frontend/app/_components/task-form-sheet.tsx.ejs +629 -784
- package/hedhog/frontend/app/_components/task-info-display.tsx.ejs +137 -0
- package/hedhog/frontend/app/_components/timesheet-entry-create-sheet.tsx.ejs +306 -0
- package/hedhog/frontend/app/_lib/api.ts.ejs +155 -0
- package/hedhog/frontend/app/_lib/types.ts.ejs +62 -0
- package/hedhog/frontend/app/_lib/utils/format.ts.ejs +0 -2
- package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +61 -0
- package/hedhog/frontend/app/approvals/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/collaborators/page.tsx.ejs +59 -8
- package/hedhog/frontend/app/contracts/page.tsx.ejs +29 -8
- package/hedhog/frontend/app/dashboard/widgets/CapacityDistribution.tsx.ejs +84 -0
- package/hedhog/frontend/app/dashboard/widgets/EffortByProject.tsx.ejs +85 -0
- package/hedhog/frontend/app/dashboard/widgets/HeadcountByArea.tsx.ejs +101 -0
- package/hedhog/frontend/app/dashboard/widgets/ManagedProjectsStatus.tsx.ejs +113 -0
- package/hedhog/frontend/app/dashboard/widgets/MyHoursPeriodKpi.tsx.ejs +87 -0
- package/hedhog/frontend/app/dashboard/widgets/MyOpenRequestsKpi.tsx.ejs +97 -0
- package/hedhog/frontend/app/dashboard/widgets/MyPendingRequestsList.tsx.ejs +99 -0
- package/hedhog/frontend/app/dashboard/widgets/MyProjectAllocationsKpi.tsx.ejs +78 -0
- package/hedhog/frontend/app/dashboard/widgets/MyQuickActions.tsx.ejs +130 -0
- package/hedhog/frontend/app/dashboard/widgets/MyRelevantDeadlines.tsx.ejs +144 -0
- package/hedhog/frontend/app/dashboard/widgets/MyTimesheetStatusKpi.tsx.ejs +78 -0
- package/hedhog/frontend/app/dashboard/widgets/MyWeeklyJourney.tsx.ejs +99 -0
- package/hedhog/frontend/app/dashboard/widgets/PortfolioCostsKpi.tsx.ejs +112 -0
- package/hedhog/frontend/app/dashboard/widgets/PortfolioEffortKpi.tsx.ejs +93 -0
- package/hedhog/frontend/app/dashboard/widgets/PortfolioProjectsKpi.tsx.ejs +96 -0
- package/hedhog/frontend/app/dashboard/widgets/PortfolioRiskKpi.tsx.ejs +115 -0
- package/hedhog/frontend/app/dashboard/widgets/ProjectStatusOverview.tsx.ejs +120 -0
- package/hedhog/frontend/app/dashboard/widgets/StrategicDeadlines.tsx.ejs +146 -0
- package/hedhog/frontend/app/dashboard/widgets/TeamApprovalQueue.tsx.ejs +108 -0
- package/hedhog/frontend/app/dashboard/widgets/TeamCapacityKpi.tsx.ejs +97 -0
- package/hedhog/frontend/app/dashboard/widgets/TeamHeadcountKpi.tsx.ejs +100 -0
- package/hedhog/frontend/app/dashboard/widgets/TeamHoursKpi.tsx.ejs +104 -0
- package/hedhog/frontend/app/dashboard/widgets/TeamPendingApprovalsKpi.tsx.ejs +110 -0
- package/hedhog/frontend/app/dashboard/widgets/TeamUtilizationOverview.tsx.ejs +115 -0
- package/hedhog/frontend/app/dashboard/widgets/TeamWorkloadAlerts.tsx.ejs +117 -0
- package/hedhog/frontend/app/dashboard/widgets/index.ts.ejs +26 -0
- package/hedhog/frontend/app/departments/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/my-projects/page.tsx.ejs +30 -12
- package/hedhog/frontend/app/my-tasks/page.tsx.ejs +286 -125
- package/hedhog/frontend/app/project-cost-categories/page.tsx.ejs +58 -52
- package/hedhog/frontend/app/project-cost-types/page.tsx.ejs +58 -51
- package/hedhog/frontend/app/projects/page.tsx.ejs +415 -33
- package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/tasks-gantt/page.tsx.ejs +953 -0
- package/hedhog/frontend/app/time-off/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/timesheets/page.tsx.ejs +10 -4
- package/hedhog/frontend/messages/en.json +332 -46
- package/hedhog/frontend/messages/operations/en.json +61 -52
- package/hedhog/frontend/messages/operations/pt.json +59 -43
- package/hedhog/frontend/messages/pt.json +332 -46
- package/hedhog/frontend/widgets/capacity-distribution.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/effort-by-project.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/headcount-by-area.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/index.ts.ejs +25 -0
- package/hedhog/frontend/widgets/managed-projects-status.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/my-hours-period-kpi.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/my-open-requests-kpi.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/my-pending-requests-list.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/my-project-allocations-kpi.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/my-quick-actions.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/my-relevant-deadlines.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/my-timesheet-status-kpi.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/my-weekly-journey.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/portfolio-costs-kpi.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/portfolio-effort-kpi.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/portfolio-projects-kpi.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/portfolio-risk-kpi.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/project-status-overview.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/shared-operations-widget.tsx.ejs +170 -0
- package/hedhog/frontend/widgets/strategic-deadlines.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/team-approval-queue.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/team-capacity-kpi.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/team-headcount-kpi.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/team-hours-kpi.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/team-pending-approvals-kpi.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/team-utilization-overview.tsx.ejs +17 -0
- package/hedhog/frontend/widgets/team-workload-alerts.tsx.ejs +17 -0
- package/hedhog/table/operations_collaborator.yaml +8 -13
- package/hedhog/table/operations_collaborator_invoice.yaml +35 -0
- package/hedhog/table/operations_collaborator_payment.yaml +32 -0
- package/hedhog/table/operations_project.yaml +1 -1
- package/hedhog/table/operations_project_file.yaml +23 -0
- package/hedhog/table/operations_task.yaml +76 -69
- package/hedhog/table/operations_task_activity.yaml +51 -0
- package/package.json +6 -5
- package/src/controllers/operations-collaborators.controller.ts +117 -8
- package/src/controllers/operations-projects.controller.ts +41 -8
- package/src/controllers/operations-tasks.controller.ts +156 -166
- package/src/dashboard/README.md +214 -0
- package/src/dashboard/components/DashboardLayout.tsx +131 -0
- package/src/dashboard/components/widget-registry.ts +255 -0
- package/src/dashboard/hooks/useDashboardData.ts +29 -0
- package/src/dashboard/types/widgets.types.ts +237 -0
- package/src/dashboard/widgets/CapacityDistribution.tsx +56 -0
- package/src/dashboard/widgets/EffortByProject.tsx +51 -0
- package/src/dashboard/widgets/HeadcountByArea.tsx +57 -0
- package/src/dashboard/widgets/ManagedProjectsStatus.tsx +53 -0
- package/src/dashboard/widgets/MyHoursPeriodKpi.tsx +87 -0
- package/src/dashboard/widgets/MyOpenRequestsKpi.tsx +51 -0
- package/src/dashboard/widgets/MyPendingRequestsList.tsx +63 -0
- package/src/dashboard/widgets/MyProjectAllocationsKpi.tsx +57 -0
- package/src/dashboard/widgets/MyQuickActions.tsx +62 -0
- package/src/dashboard/widgets/MyRelevantDeadlines.tsx +84 -0
- package/src/dashboard/widgets/MyTimesheetStatusKpi.tsx +65 -0
- package/src/dashboard/widgets/MyWeeklyJourney.tsx +57 -0
- package/src/dashboard/widgets/PortfolioCostsKpi.tsx +48 -0
- package/src/dashboard/widgets/PortfolioEffortKpi.tsx +41 -0
- package/src/dashboard/widgets/PortfolioRiskKpi.tsx +50 -0
- package/src/dashboard/widgets/ProjectStatusOverview.tsx +52 -0
- package/src/dashboard/widgets/StrategicDeadlines.tsx +93 -0
- package/src/dashboard/widgets/TeamApprovalQueue.tsx +70 -0
- package/src/dashboard/widgets/TeamCapacityKpi.tsx +50 -0
- package/src/dashboard/widgets/TeamHoursKpi.tsx +51 -0
- package/src/dashboard/widgets/TeamPendingApprovalsKpi.tsx +53 -0
- package/src/dashboard/widgets/TeamUtilizationOverview.tsx +62 -0
- package/src/dashboard/widgets/TeamWorkloadAlerts.tsx +81 -0
- package/src/dashboard/widgets/index.ts +26 -0
- package/src/dto/create-collaborator-invoice.dto.ts +39 -0
- package/src/dto/create-collaborator-payment.dto.ts +35 -0
- package/src/dto/create-collaborator.dto.ts +4 -11
- package/src/dto/list-collaborator-invoice.dto.ts +3 -0
- package/src/dto/list-collaborator-payment.dto.ts +3 -0
- package/src/dto/update-collaborator-invoice.dto.ts +6 -0
- package/src/dto/update-collaborator-payment.dto.ts +6 -0
- package/src/index.ts +3 -0
- package/src/operations.service.spec.ts +988 -764
- package/src/operations.service.ts +4689 -2624
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { EmptyState, Page } from '@/components/entity-list';
|
|
4
|
-
import { RichTextEditor } from '@/components/rich-text-editor';
|
|
5
4
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
6
5
|
import { Button } from '@/components/ui/button';
|
|
7
6
|
import { Card, CardContent } from '@/components/ui/card';
|
|
@@ -14,10 +13,12 @@ import {
|
|
|
14
13
|
import {
|
|
15
14
|
Dialog,
|
|
16
15
|
DialogContent,
|
|
16
|
+
DialogDescription,
|
|
17
17
|
DialogFooter,
|
|
18
18
|
DialogHeader,
|
|
19
19
|
DialogTitle,
|
|
20
20
|
} from '@/components/ui/dialog';
|
|
21
|
+
import { EntityPicker } from '@/components/ui/entity-picker';
|
|
21
22
|
import { Input } from '@/components/ui/input';
|
|
22
23
|
import { Label } from '@/components/ui/label';
|
|
23
24
|
import { Progress } from '@/components/ui/progress';
|
|
@@ -95,10 +96,12 @@ import {
|
|
|
95
96
|
Plus,
|
|
96
97
|
Rocket,
|
|
97
98
|
Search,
|
|
99
|
+
Send,
|
|
98
100
|
SlidersHorizontal,
|
|
99
101
|
Timer,
|
|
100
102
|
Trash2,
|
|
101
103
|
TrendingUp,
|
|
104
|
+
UserPlus,
|
|
102
105
|
Users,
|
|
103
106
|
type LucideIcon,
|
|
104
107
|
} from 'lucide-react';
|
|
@@ -125,15 +128,17 @@ import {
|
|
|
125
128
|
YAxis,
|
|
126
129
|
} from 'recharts';
|
|
127
130
|
import { fetchOperations, mutateOperations } from '../_lib/api';
|
|
128
|
-
import { useMentionItems } from '../_lib/hooks/use-mention-items';
|
|
129
131
|
import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
|
|
130
132
|
import {
|
|
131
133
|
MASKED_VALUE,
|
|
132
134
|
useValuesVisibility,
|
|
133
135
|
} from '../_lib/hooks/use-values-visibility';
|
|
134
136
|
import type {
|
|
137
|
+
OperationsCollaborator,
|
|
138
|
+
OperationsCollaboratorDetails,
|
|
135
139
|
OperationsProjectDetails,
|
|
136
140
|
OperationsTaskOption,
|
|
141
|
+
PaginatedResponse,
|
|
137
142
|
} from '../_lib/types';
|
|
138
143
|
import {
|
|
139
144
|
formatCurrency,
|
|
@@ -144,17 +149,16 @@ import {
|
|
|
144
149
|
formatPercent,
|
|
145
150
|
getStatusBadgeClass,
|
|
146
151
|
} from '../_lib/utils/format';
|
|
152
|
+
import { parseNumberInput } from '../_lib/utils/forms';
|
|
147
153
|
import { OperationsHeader } from './operations-header';
|
|
148
154
|
import { ProjectCostsSection } from './project-costs-section';
|
|
155
|
+
import { ProjectFileAttachments } from './project-file-attachments';
|
|
149
156
|
import { ProjectFormScreen } from './project-form-screen';
|
|
150
157
|
import { SectionCard } from './section-card';
|
|
151
158
|
import { StatusBadge } from './status-badge';
|
|
152
|
-
import {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
type TaskDetailSheetData,
|
|
156
|
-
} from './task-detail-sheet';
|
|
157
|
-
import { TaskFileAttachments } from './task-file-attachments';
|
|
159
|
+
import { TaskDetailSheet, type TaskDetailSheetData } from './task-detail-sheet';
|
|
160
|
+
import { TaskFormSheet } from './task-form-sheet';
|
|
161
|
+
import { TimesheetEntryCreateSheet } from './timesheet-entry-create-sheet';
|
|
158
162
|
|
|
159
163
|
type BoardColumnId = 'todo' | 'doing' | 'review' | 'done';
|
|
160
164
|
|
|
@@ -175,6 +179,8 @@ type BoardTask = {
|
|
|
175
179
|
createdAt: string | null;
|
|
176
180
|
commentCount: number;
|
|
177
181
|
fileCount: number;
|
|
182
|
+
doingStartedAt: string | null;
|
|
183
|
+
totalDoingMinutes: number;
|
|
178
184
|
};
|
|
179
185
|
|
|
180
186
|
type ApiBoardTask = Partial<BoardTask> & {
|
|
@@ -184,28 +190,6 @@ type ApiBoardTask = Partial<BoardTask> & {
|
|
|
184
190
|
priority?: BoardTask['priority'] | null;
|
|
185
191
|
};
|
|
186
192
|
|
|
187
|
-
type TaskFormState = {
|
|
188
|
-
name: string;
|
|
189
|
-
description: string;
|
|
190
|
-
priority: 'low' | 'medium' | 'high';
|
|
191
|
-
status: BoardColumnId;
|
|
192
|
-
assigneeCollaboratorId: string;
|
|
193
|
-
dueDate: string;
|
|
194
|
-
estimateHours: string;
|
|
195
|
-
tags: string;
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const EMPTY_TASK_FORM: TaskFormState = {
|
|
199
|
-
name: '',
|
|
200
|
-
description: '',
|
|
201
|
-
priority: 'medium',
|
|
202
|
-
status: 'todo',
|
|
203
|
-
assigneeCollaboratorId: 'none',
|
|
204
|
-
dueDate: '',
|
|
205
|
-
estimateHours: '',
|
|
206
|
-
tags: '',
|
|
207
|
-
};
|
|
208
|
-
|
|
209
193
|
type BoardColumns = Record<BoardColumnId, BoardTask[]>;
|
|
210
194
|
|
|
211
195
|
type BoardState = {
|
|
@@ -213,6 +197,23 @@ type BoardState = {
|
|
|
213
197
|
columns: BoardColumns;
|
|
214
198
|
};
|
|
215
199
|
|
|
200
|
+
type TimesheetEntryPrefill = {
|
|
201
|
+
projectId: number;
|
|
202
|
+
projectAssignmentId?: number | null;
|
|
203
|
+
projectLabel: string;
|
|
204
|
+
taskId: number;
|
|
205
|
+
taskLabel: string;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
function formatAssignmentNumericValue(value: number) {
|
|
209
|
+
if (!Number.isFinite(value)) {
|
|
210
|
+
return '';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const roundedValue = Math.round(value * 100) / 100;
|
|
214
|
+
return String(roundedValue);
|
|
215
|
+
}
|
|
216
|
+
|
|
216
217
|
const KANBAN_COLUMNS: Array<{ id: BoardColumnId; label: string }> = [
|
|
217
218
|
{ id: 'todo', label: 'Backlog' },
|
|
218
219
|
{ id: 'doing', label: 'Em execução' },
|
|
@@ -252,6 +253,8 @@ function apiTaskToBoardTask(row: ApiBoardTask): BoardTask {
|
|
|
252
253
|
0,
|
|
253
254
|
fileCount:
|
|
254
255
|
((row as BoardTask & Record<string, unknown>).fileCount as number) ?? 0,
|
|
256
|
+
doingStartedAt: row.doingStartedAt ?? null,
|
|
257
|
+
totalDoingMinutes: row.totalDoingMinutes ?? 0,
|
|
255
258
|
};
|
|
256
259
|
}
|
|
257
260
|
|
|
@@ -264,6 +267,23 @@ function splitTasksByColumn(tasks: BoardTask[]): BoardColumns {
|
|
|
264
267
|
};
|
|
265
268
|
}
|
|
266
269
|
|
|
270
|
+
function replaceTaskInColumns(
|
|
271
|
+
columns: BoardColumns,
|
|
272
|
+
task: BoardTask
|
|
273
|
+
): BoardColumns {
|
|
274
|
+
const nextColumns = {
|
|
275
|
+
todo: columns.todo.filter((item) => item.id !== task.id),
|
|
276
|
+
doing: columns.doing.filter((item) => item.id !== task.id),
|
|
277
|
+
review: columns.review.filter((item) => item.id !== task.id),
|
|
278
|
+
done: columns.done.filter((item) => item.id !== task.id),
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
...nextColumns,
|
|
283
|
+
[task.status]: [task, ...nextColumns[task.status]],
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
267
287
|
const boardChartConfig = {
|
|
268
288
|
allocation: { label: 'Alocacao', color: 'hsl(201 96% 32%)' },
|
|
269
289
|
loggedHours: { label: 'Horas', color: 'hsl(166 72% 28%)' },
|
|
@@ -376,7 +396,7 @@ function getInitials(value?: string | null) {
|
|
|
376
396
|
function getPersonAvatarUrl(avatarId?: number | null) {
|
|
377
397
|
return typeof avatarId === 'number' && avatarId > 0
|
|
378
398
|
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
|
|
379
|
-
:
|
|
399
|
+
: undefined;
|
|
380
400
|
}
|
|
381
401
|
|
|
382
402
|
function getUserPhotoUrl(photoId?: number | null) {
|
|
@@ -385,26 +405,6 @@ function getUserPhotoUrl(photoId?: number | null) {
|
|
|
385
405
|
: null;
|
|
386
406
|
}
|
|
387
407
|
|
|
388
|
-
function normalizeDateInputValue(value?: string | null) {
|
|
389
|
-
if (!value) {
|
|
390
|
-
return '';
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const normalizedValue = String(value).trim();
|
|
394
|
-
const directMatch = normalizedValue.match(/^\d{4}-\d{2}-\d{2}/);
|
|
395
|
-
|
|
396
|
-
if (directMatch?.[0]) {
|
|
397
|
-
return directMatch[0];
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const parsedDate = new Date(normalizedValue);
|
|
401
|
-
if (Number.isNaN(parsedDate.getTime())) {
|
|
402
|
-
return '';
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
return parsedDate.toISOString().slice(0, 10);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
408
|
function clampPercent(value?: number | null) {
|
|
409
409
|
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
410
410
|
return 0;
|
|
@@ -1102,9 +1102,9 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1102
1102
|
const commonT = useTranslations('operations.Common');
|
|
1103
1103
|
const formT = useTranslations('operations.ProjectFormPage');
|
|
1104
1104
|
const contractT = useTranslations('operations.ContractFormPage');
|
|
1105
|
+
const collaboratorFormT = useTranslations('operations.CollaboratorFormPage');
|
|
1105
1106
|
const { request, currentLocaleCode, getSettingValue } = useApp();
|
|
1106
1107
|
const access = useOperationsAccess();
|
|
1107
|
-
const mentionItems = useMentionItems(request);
|
|
1108
1108
|
const isLimitedView = !access.isDirector && !access.isSupervisor;
|
|
1109
1109
|
const { valuesVisible, toggleValuesVisible } = useValuesVisibility();
|
|
1110
1110
|
const router = useRouter();
|
|
@@ -1281,11 +1281,13 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1281
1281
|
);
|
|
1282
1282
|
const [taskFormOpen, setTaskFormOpen] = useState(false);
|
|
1283
1283
|
const [editingTaskId, setEditingTaskId] = useState<number | null>(null);
|
|
1284
|
-
const [
|
|
1285
|
-
useState<
|
|
1286
|
-
const [taskFormLoading, setTaskFormLoading] = useState(false);
|
|
1284
|
+
const [createDefaultStatus, setCreateDefaultStatus] =
|
|
1285
|
+
useState<BoardColumnId>('todo');
|
|
1287
1286
|
const [deletePromptTask, setDeletePromptTask] =
|
|
1288
1287
|
useState<TaskDetailSheetData | null>(null);
|
|
1288
|
+
const [deleteProjectDialogOpen, setDeleteProjectDialogOpen] = useState(false);
|
|
1289
|
+
const [deleteProjectConfirmText, setDeleteProjectConfirmText] = useState('');
|
|
1290
|
+
const [deletingProject, setDeletingProject] = useState(false);
|
|
1289
1291
|
const [inlineCreateColumn, setInlineCreateColumn] =
|
|
1290
1292
|
useState<BoardColumnId | null>(null);
|
|
1291
1293
|
const [inlineCreateName, setInlineCreateName] = useState('');
|
|
@@ -1303,6 +1305,30 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1303
1305
|
const [restoringTaskId, setRestoringTaskId] = useState<number | null>(null);
|
|
1304
1306
|
const [deletingTaskId, setDeletingTaskId] = useState<number | null>(null);
|
|
1305
1307
|
const [activeDragTask, setActiveDragTask] = useState<BoardTask | null>(null);
|
|
1308
|
+
const [isTimesheetEntrySheetOpen, setIsTimesheetEntrySheetOpen] =
|
|
1309
|
+
useState(false);
|
|
1310
|
+
const [timesheetPrefill, setTimesheetPrefill] =
|
|
1311
|
+
useState<TimesheetEntryPrefill | null>(null);
|
|
1312
|
+
|
|
1313
|
+
// Assignment management state
|
|
1314
|
+
const [assignmentSheetOpen, setAssignmentSheetOpen] = useState(false);
|
|
1315
|
+
const [editingAssignment, setEditingAssignment] = useState<
|
|
1316
|
+
OperationsProjectDetails['assignments'][0] | null
|
|
1317
|
+
>(null);
|
|
1318
|
+
const [assignmentFormData, setAssignmentFormData] = useState({
|
|
1319
|
+
collaboratorId: '',
|
|
1320
|
+
weeklyHours: '',
|
|
1321
|
+
allocationPercent: '',
|
|
1322
|
+
status: 'active',
|
|
1323
|
+
startDate: '',
|
|
1324
|
+
endDate: '',
|
|
1325
|
+
});
|
|
1326
|
+
const [selectedAssignmentCollaborator, setSelectedAssignmentCollaborator] =
|
|
1327
|
+
useState<OperationsCollaborator | null>(null);
|
|
1328
|
+
const [savingAssignment, setSavingAssignment] = useState(false);
|
|
1329
|
+
const [removingAssignmentId, setRemovingAssignmentId] = useState<
|
|
1330
|
+
number | null
|
|
1331
|
+
>(null);
|
|
1306
1332
|
|
|
1307
1333
|
const apiTasks = useMemo(() => rawTasks.map(apiTaskToBoardTask), [rawTasks]);
|
|
1308
1334
|
const archivedTasks = useMemo(
|
|
@@ -1320,6 +1346,29 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1320
1346
|
return splitTasksByColumn(apiTasks);
|
|
1321
1347
|
}, [project, boardState, apiTasks]);
|
|
1322
1348
|
|
|
1349
|
+
const editingTask = useMemo(() => {
|
|
1350
|
+
if (!editingTaskId) return null;
|
|
1351
|
+
return (
|
|
1352
|
+
Object.values(taskColumns)
|
|
1353
|
+
.flat()
|
|
1354
|
+
.find((task) => task.id === editingTaskId) ??
|
|
1355
|
+
apiTasks.find((task) => task.id === editingTaskId) ??
|
|
1356
|
+
archivedTasks.find((task) => task.id === editingTaskId) ??
|
|
1357
|
+
null
|
|
1358
|
+
);
|
|
1359
|
+
}, [apiTasks, archivedTasks, editingTaskId, taskColumns]);
|
|
1360
|
+
|
|
1361
|
+
const editingTaskAsOption = useMemo(() => {
|
|
1362
|
+
if (!editingTask) return null;
|
|
1363
|
+
return {
|
|
1364
|
+
...editingTask,
|
|
1365
|
+
label: editingTask.name,
|
|
1366
|
+
projectId,
|
|
1367
|
+
projectName: project?.name ?? '',
|
|
1368
|
+
projectCode: project?.code,
|
|
1369
|
+
};
|
|
1370
|
+
}, [editingTask, projectId, project]);
|
|
1371
|
+
|
|
1323
1372
|
const filteredTaskColumns: BoardColumns = useMemo(() => {
|
|
1324
1373
|
const normalizedSearch = boardSearch.trim().toLocaleLowerCase();
|
|
1325
1374
|
const filterTask = (task: BoardTask) => {
|
|
@@ -1373,8 +1422,8 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1373
1422
|
|
|
1374
1423
|
const openCreateTaskForm = useCallback(
|
|
1375
1424
|
(defaultStatus: BoardColumnId = 'todo') => {
|
|
1425
|
+
setCreateDefaultStatus(defaultStatus);
|
|
1376
1426
|
setEditingTaskId(null);
|
|
1377
|
-
setTaskFormData({ ...EMPTY_TASK_FORM, status: defaultStatus });
|
|
1378
1427
|
setTaskFormOpen(true);
|
|
1379
1428
|
},
|
|
1380
1429
|
[]
|
|
@@ -1382,73 +1431,10 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1382
1431
|
|
|
1383
1432
|
const openEditTaskForm = useCallback((task: BoardTask) => {
|
|
1384
1433
|
setEditingTaskId(task.id);
|
|
1385
|
-
setTaskFormData({
|
|
1386
|
-
name: task.name,
|
|
1387
|
-
description: task.description ?? '',
|
|
1388
|
-
priority: task.priority,
|
|
1389
|
-
status: task.status,
|
|
1390
|
-
assigneeCollaboratorId: task.assigneeCollaboratorId
|
|
1391
|
-
? String(task.assigneeCollaboratorId)
|
|
1392
|
-
: 'none',
|
|
1393
|
-
dueDate: normalizeDateInputValue(task.dueDate),
|
|
1394
|
-
estimateHours:
|
|
1395
|
-
task.estimateHours != null ? String(task.estimateHours) : '',
|
|
1396
|
-
tags: task.tags ?? '',
|
|
1397
|
-
});
|
|
1398
1434
|
setSelectedTask(null);
|
|
1399
1435
|
setTaskFormOpen(true);
|
|
1400
1436
|
}, []);
|
|
1401
1437
|
|
|
1402
|
-
const handleTaskFormSubmit = useCallback(async () => {
|
|
1403
|
-
if (!taskFormData.name.trim()) return;
|
|
1404
|
-
setTaskFormLoading(true);
|
|
1405
|
-
try {
|
|
1406
|
-
const payload: Record<string, unknown> = {
|
|
1407
|
-
name: taskFormData.name.trim(),
|
|
1408
|
-
description: taskFormData.description || null,
|
|
1409
|
-
priority: taskFormData.priority,
|
|
1410
|
-
status: taskFormData.status,
|
|
1411
|
-
assigneeCollaboratorId:
|
|
1412
|
-
taskFormData.assigneeCollaboratorId !== 'none'
|
|
1413
|
-
? Number(taskFormData.assigneeCollaboratorId)
|
|
1414
|
-
: null,
|
|
1415
|
-
dueDate: taskFormData.dueDate || null,
|
|
1416
|
-
estimateHours: taskFormData.estimateHours
|
|
1417
|
-
? Number(taskFormData.estimateHours)
|
|
1418
|
-
: null,
|
|
1419
|
-
tags: taskFormData.tags || null,
|
|
1420
|
-
};
|
|
1421
|
-
if (editingTaskId) {
|
|
1422
|
-
await mutateOperations(
|
|
1423
|
-
request,
|
|
1424
|
-
`/operations/tasks/${editingTaskId}`,
|
|
1425
|
-
'PATCH',
|
|
1426
|
-
payload
|
|
1427
|
-
);
|
|
1428
|
-
} else {
|
|
1429
|
-
await mutateOperations(request, '/operations/tasks', 'POST', {
|
|
1430
|
-
projectId,
|
|
1431
|
-
...payload,
|
|
1432
|
-
});
|
|
1433
|
-
}
|
|
1434
|
-
setBoardState(null);
|
|
1435
|
-
await refetchTasks();
|
|
1436
|
-
await refetchArchivedTasks();
|
|
1437
|
-
setTaskFormOpen(false);
|
|
1438
|
-
setEditingTaskId(null);
|
|
1439
|
-
setTaskFormData(EMPTY_TASK_FORM);
|
|
1440
|
-
} finally {
|
|
1441
|
-
setTaskFormLoading(false);
|
|
1442
|
-
}
|
|
1443
|
-
}, [
|
|
1444
|
-
taskFormData,
|
|
1445
|
-
editingTaskId,
|
|
1446
|
-
projectId,
|
|
1447
|
-
request,
|
|
1448
|
-
refetchTasks,
|
|
1449
|
-
refetchArchivedTasks,
|
|
1450
|
-
]);
|
|
1451
|
-
|
|
1452
1438
|
const handleArchiveTask = useCallback(
|
|
1453
1439
|
async (taskId: number) => {
|
|
1454
1440
|
setArchivingTaskId(taskId);
|
|
@@ -1548,6 +1534,286 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1548
1534
|
[request, refetchTasks, refetchArchivedTasks]
|
|
1549
1535
|
);
|
|
1550
1536
|
|
|
1537
|
+
const handleDeleteProject = useCallback(async () => {
|
|
1538
|
+
setDeletingProject(true);
|
|
1539
|
+
try {
|
|
1540
|
+
await mutateOperations(
|
|
1541
|
+
request,
|
|
1542
|
+
`/operations/projects/${projectId}`,
|
|
1543
|
+
'DELETE'
|
|
1544
|
+
);
|
|
1545
|
+
router.push('/operations/projects');
|
|
1546
|
+
} catch {
|
|
1547
|
+
// ignore
|
|
1548
|
+
} finally {
|
|
1549
|
+
setDeletingProject(false);
|
|
1550
|
+
setDeleteProjectDialogOpen(false);
|
|
1551
|
+
setDeleteProjectConfirmText('');
|
|
1552
|
+
}
|
|
1553
|
+
}, [request, projectId, router]);
|
|
1554
|
+
|
|
1555
|
+
const openTimesheetEntrySheet = useCallback(
|
|
1556
|
+
(task: TaskDetailSheetData) => {
|
|
1557
|
+
if (!project) {
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
const projectLabel = [project.name, project.code]
|
|
1562
|
+
.filter(Boolean)
|
|
1563
|
+
.join(' • ');
|
|
1564
|
+
|
|
1565
|
+
setTimesheetPrefill({
|
|
1566
|
+
projectId,
|
|
1567
|
+
projectAssignmentId: task.projectAssignmentId ?? null,
|
|
1568
|
+
projectLabel: projectLabel || commonT('labels.notAssigned'),
|
|
1569
|
+
taskId: task.id,
|
|
1570
|
+
taskLabel: task.name,
|
|
1571
|
+
});
|
|
1572
|
+
setSelectedTask(null);
|
|
1573
|
+
setIsTimesheetEntrySheetOpen(true);
|
|
1574
|
+
},
|
|
1575
|
+
[commonT, project, projectId]
|
|
1576
|
+
);
|
|
1577
|
+
|
|
1578
|
+
const loadCollaboratorOptions = useCallback(
|
|
1579
|
+
async ({
|
|
1580
|
+
page,
|
|
1581
|
+
pageSize,
|
|
1582
|
+
search,
|
|
1583
|
+
}: {
|
|
1584
|
+
page: number;
|
|
1585
|
+
pageSize: number;
|
|
1586
|
+
search: string;
|
|
1587
|
+
}) => {
|
|
1588
|
+
const params = new URLSearchParams({
|
|
1589
|
+
page: String(page),
|
|
1590
|
+
pageSize: String(pageSize),
|
|
1591
|
+
sortField: 'displayName',
|
|
1592
|
+
sortOrder: 'asc',
|
|
1593
|
+
});
|
|
1594
|
+
if (search.trim()) params.set('search', search.trim());
|
|
1595
|
+
const result = await fetchOperations<
|
|
1596
|
+
PaginatedResponse<OperationsCollaborator>
|
|
1597
|
+
>(request, `/operations/collaborators?${params.toString()}`);
|
|
1598
|
+
const items = result?.data ?? [];
|
|
1599
|
+
const total = result?.total ?? 0;
|
|
1600
|
+
return { items, hasMore: page * pageSize < total };
|
|
1601
|
+
},
|
|
1602
|
+
[request]
|
|
1603
|
+
);
|
|
1604
|
+
|
|
1605
|
+
const loadAssignmentCollaboratorById = useCallback(
|
|
1606
|
+
async (collaboratorId: number) => {
|
|
1607
|
+
const collaborator = await fetchOperations<OperationsCollaboratorDetails>(
|
|
1608
|
+
request,
|
|
1609
|
+
`/operations/collaborators/${collaboratorId}`
|
|
1610
|
+
);
|
|
1611
|
+
setSelectedAssignmentCollaborator(collaborator);
|
|
1612
|
+
return collaborator;
|
|
1613
|
+
},
|
|
1614
|
+
[request]
|
|
1615
|
+
);
|
|
1616
|
+
|
|
1617
|
+
const createAssignmentCollaborator = useCallback(
|
|
1618
|
+
async (values: Record<string, string>) => {
|
|
1619
|
+
const displayName = values.displayName?.trim() ?? '';
|
|
1620
|
+
const weeklyCapacityHours = parseNumberInput(
|
|
1621
|
+
values.weeklyCapacityHours ?? ''
|
|
1622
|
+
);
|
|
1623
|
+
|
|
1624
|
+
if (!displayName) {
|
|
1625
|
+
return null;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
return mutateOperations<OperationsCollaborator>(
|
|
1629
|
+
request,
|
|
1630
|
+
'/operations/collaborators',
|
|
1631
|
+
'POST',
|
|
1632
|
+
{
|
|
1633
|
+
displayName,
|
|
1634
|
+
weeklyCapacityHours,
|
|
1635
|
+
status: 'active',
|
|
1636
|
+
autoGenerateContractDraft: false,
|
|
1637
|
+
}
|
|
1638
|
+
);
|
|
1639
|
+
},
|
|
1640
|
+
[request]
|
|
1641
|
+
);
|
|
1642
|
+
|
|
1643
|
+
const syncAssignmentFromWeeklyHours = useCallback(
|
|
1644
|
+
(
|
|
1645
|
+
weeklyHours: string,
|
|
1646
|
+
currentFormData: typeof assignmentFormData,
|
|
1647
|
+
collaborator?: OperationsCollaborator | null
|
|
1648
|
+
) => {
|
|
1649
|
+
const capacity = collaborator?.weeklyCapacityHours;
|
|
1650
|
+
if (!capacity || capacity <= 0) {
|
|
1651
|
+
return {
|
|
1652
|
+
weeklyHours,
|
|
1653
|
+
allocationPercent: currentFormData.allocationPercent,
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
const parsedHours = parseNumberInput(weeklyHours);
|
|
1658
|
+
if (parsedHours == null) {
|
|
1659
|
+
return { weeklyHours, allocationPercent: '' };
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
return {
|
|
1663
|
+
weeklyHours,
|
|
1664
|
+
allocationPercent: formatAssignmentNumericValue(
|
|
1665
|
+
(parsedHours / capacity) * 100
|
|
1666
|
+
),
|
|
1667
|
+
};
|
|
1668
|
+
},
|
|
1669
|
+
[]
|
|
1670
|
+
);
|
|
1671
|
+
|
|
1672
|
+
const syncAssignmentFromAllocation = useCallback(
|
|
1673
|
+
(
|
|
1674
|
+
allocationPercent: string,
|
|
1675
|
+
currentFormData: typeof assignmentFormData,
|
|
1676
|
+
collaborator?: OperationsCollaborator | null
|
|
1677
|
+
) => {
|
|
1678
|
+
const capacity = collaborator?.weeklyCapacityHours;
|
|
1679
|
+
if (!capacity || capacity <= 0) {
|
|
1680
|
+
return {
|
|
1681
|
+
weeklyHours: currentFormData.weeklyHours,
|
|
1682
|
+
allocationPercent,
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
const parsedPercent = parseNumberInput(allocationPercent);
|
|
1687
|
+
if (parsedPercent == null) {
|
|
1688
|
+
return { weeklyHours: '', allocationPercent };
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
return {
|
|
1692
|
+
weeklyHours: formatAssignmentNumericValue(
|
|
1693
|
+
(parsedPercent / 100) * capacity
|
|
1694
|
+
),
|
|
1695
|
+
allocationPercent,
|
|
1696
|
+
};
|
|
1697
|
+
},
|
|
1698
|
+
[]
|
|
1699
|
+
);
|
|
1700
|
+
|
|
1701
|
+
const openAddAssignment = useCallback(() => {
|
|
1702
|
+
setEditingAssignment(null);
|
|
1703
|
+
setSelectedAssignmentCollaborator(null);
|
|
1704
|
+
setAssignmentFormData({
|
|
1705
|
+
collaboratorId: '',
|
|
1706
|
+
weeklyHours: '',
|
|
1707
|
+
allocationPercent: '',
|
|
1708
|
+
status: 'active',
|
|
1709
|
+
startDate: '',
|
|
1710
|
+
endDate: '',
|
|
1711
|
+
});
|
|
1712
|
+
setAssignmentSheetOpen(true);
|
|
1713
|
+
}, []);
|
|
1714
|
+
|
|
1715
|
+
const openEditAssignment = useCallback(
|
|
1716
|
+
(assignment: OperationsProjectDetails['assignments'][0]) => {
|
|
1717
|
+
setEditingAssignment(assignment);
|
|
1718
|
+
setSelectedAssignmentCollaborator(null);
|
|
1719
|
+
setAssignmentFormData({
|
|
1720
|
+
collaboratorId: String(assignment.collaboratorId),
|
|
1721
|
+
weeklyHours:
|
|
1722
|
+
assignment.weeklyHours != null ? String(assignment.weeklyHours) : '',
|
|
1723
|
+
allocationPercent:
|
|
1724
|
+
assignment.allocationPercent != null
|
|
1725
|
+
? String(assignment.allocationPercent)
|
|
1726
|
+
: '',
|
|
1727
|
+
status: assignment.status,
|
|
1728
|
+
startDate: assignment.startDate?.slice(0, 10) ?? '',
|
|
1729
|
+
endDate: assignment.endDate?.slice(0, 10) ?? '',
|
|
1730
|
+
});
|
|
1731
|
+
void loadAssignmentCollaboratorById(assignment.collaboratorId);
|
|
1732
|
+
setAssignmentSheetOpen(true);
|
|
1733
|
+
},
|
|
1734
|
+
[loadAssignmentCollaboratorById]
|
|
1735
|
+
);
|
|
1736
|
+
|
|
1737
|
+
const handleSaveAssignment = useCallback(async () => {
|
|
1738
|
+
if (!project) return;
|
|
1739
|
+
const collabId = Number(assignmentFormData.collaboratorId);
|
|
1740
|
+
if (!collabId) return;
|
|
1741
|
+
setSavingAssignment(true);
|
|
1742
|
+
const newEntry = {
|
|
1743
|
+
collaboratorId: collabId,
|
|
1744
|
+
weeklyHours: assignmentFormData.weeklyHours
|
|
1745
|
+
? Number(assignmentFormData.weeklyHours)
|
|
1746
|
+
: null,
|
|
1747
|
+
allocationPercent: assignmentFormData.allocationPercent
|
|
1748
|
+
? Number(assignmentFormData.allocationPercent)
|
|
1749
|
+
: null,
|
|
1750
|
+
status: assignmentFormData.status,
|
|
1751
|
+
startDate: assignmentFormData.startDate || null,
|
|
1752
|
+
endDate: assignmentFormData.endDate || null,
|
|
1753
|
+
};
|
|
1754
|
+
const existingMapped = project.assignments.map((a) => ({
|
|
1755
|
+
collaboratorId: a.collaboratorId,
|
|
1756
|
+
weeklyHours: a.weeklyHours ?? null,
|
|
1757
|
+
allocationPercent: a.allocationPercent ?? null,
|
|
1758
|
+
status: a.status,
|
|
1759
|
+
startDate: a.startDate ?? null,
|
|
1760
|
+
endDate: a.endDate ?? null,
|
|
1761
|
+
}));
|
|
1762
|
+
const updatedAssignments = editingAssignment
|
|
1763
|
+
? existingMapped.map((a) =>
|
|
1764
|
+
a.collaboratorId === editingAssignment.collaboratorId ? newEntry : a
|
|
1765
|
+
)
|
|
1766
|
+
: [...existingMapped, newEntry];
|
|
1767
|
+
try {
|
|
1768
|
+
await mutateOperations(
|
|
1769
|
+
request,
|
|
1770
|
+
`/operations/projects/${projectId}`,
|
|
1771
|
+
'PATCH',
|
|
1772
|
+
{ teamAssignments: updatedAssignments }
|
|
1773
|
+
);
|
|
1774
|
+
await refetch();
|
|
1775
|
+
setAssignmentSheetOpen(false);
|
|
1776
|
+
} catch {
|
|
1777
|
+
// ignore
|
|
1778
|
+
} finally {
|
|
1779
|
+
setSavingAssignment(false);
|
|
1780
|
+
}
|
|
1781
|
+
}, [
|
|
1782
|
+
project,
|
|
1783
|
+
assignmentFormData,
|
|
1784
|
+
editingAssignment,
|
|
1785
|
+
request,
|
|
1786
|
+
projectId,
|
|
1787
|
+
refetch,
|
|
1788
|
+
]);
|
|
1789
|
+
|
|
1790
|
+
const handleConfirmRemoveAssignment = useCallback(async () => {
|
|
1791
|
+
if (!project || removingAssignmentId === null) return;
|
|
1792
|
+
const updatedAssignments = project.assignments
|
|
1793
|
+
.filter((a) => a.collaboratorId !== removingAssignmentId)
|
|
1794
|
+
.map((a) => ({
|
|
1795
|
+
collaboratorId: a.collaboratorId,
|
|
1796
|
+
weeklyHours: a.weeklyHours ?? null,
|
|
1797
|
+
allocationPercent: a.allocationPercent ?? null,
|
|
1798
|
+
status: a.status,
|
|
1799
|
+
startDate: a.startDate ?? null,
|
|
1800
|
+
endDate: a.endDate ?? null,
|
|
1801
|
+
}));
|
|
1802
|
+
try {
|
|
1803
|
+
await mutateOperations(
|
|
1804
|
+
request,
|
|
1805
|
+
`/operations/projects/${projectId}`,
|
|
1806
|
+
'PATCH',
|
|
1807
|
+
{ teamAssignments: updatedAssignments }
|
|
1808
|
+
);
|
|
1809
|
+
await refetch();
|
|
1810
|
+
} catch {
|
|
1811
|
+
// ignore
|
|
1812
|
+
} finally {
|
|
1813
|
+
setRemovingAssignmentId(null);
|
|
1814
|
+
}
|
|
1815
|
+
}, [project, removingAssignmentId, request, projectId, refetch]);
|
|
1816
|
+
|
|
1551
1817
|
const allocationChartData = useMemo(() => {
|
|
1552
1818
|
if (projectStats?.allocationByCollaborator?.length) {
|
|
1553
1819
|
return projectStats.allocationByCollaborator;
|
|
@@ -1621,14 +1887,33 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1621
1887
|
},
|
|
1622
1888
|
});
|
|
1623
1889
|
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1890
|
+
mutateOperations<ApiBoardTask>(
|
|
1891
|
+
request,
|
|
1892
|
+
`/operations/tasks/${taskId}`,
|
|
1893
|
+
'PATCH',
|
|
1894
|
+
{
|
|
1895
|
+
status: targetColumn,
|
|
1896
|
+
}
|
|
1897
|
+
)
|
|
1898
|
+
.then((updatedTask) => {
|
|
1899
|
+
if (!updatedTask) {
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
const updatedBoardTask = apiTaskToBoardTask(updatedTask);
|
|
1904
|
+
setBoardState((current) => ({
|
|
1905
|
+
projectId: current?.projectId ?? project.id,
|
|
1906
|
+
columns: replaceTaskInColumns(
|
|
1907
|
+
current?.columns ?? taskColumns,
|
|
1908
|
+
updatedBoardTask
|
|
1909
|
+
),
|
|
1910
|
+
}));
|
|
1911
|
+
})
|
|
1912
|
+
.catch(() => {
|
|
1913
|
+
// Rollback optimistic update on error
|
|
1914
|
+
setBoardState(null);
|
|
1915
|
+
void refetchTasks();
|
|
1916
|
+
});
|
|
1632
1917
|
},
|
|
1633
1918
|
[findColumnByTask, taskColumns, project, request, refetchTasks]
|
|
1634
1919
|
);
|
|
@@ -2109,6 +2394,17 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
2109
2394
|
{commonT('actions.edit')}
|
|
2110
2395
|
</Button>
|
|
2111
2396
|
) : null}
|
|
2397
|
+
{access.isDirector && project?.status === 'archived' ? (
|
|
2398
|
+
<Button
|
|
2399
|
+
size="sm"
|
|
2400
|
+
variant="destructive"
|
|
2401
|
+
className="cursor-pointer gap-1.5"
|
|
2402
|
+
onClick={() => setDeleteProjectDialogOpen(true)}
|
|
2403
|
+
>
|
|
2404
|
+
<Trash2 className="size-3.5" />
|
|
2405
|
+
{t('deleteProjectButton')}
|
|
2406
|
+
</Button>
|
|
2407
|
+
) : null}
|
|
2112
2408
|
</div>
|
|
2113
2409
|
</div>
|
|
2114
2410
|
|
|
@@ -2118,10 +2414,13 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
2118
2414
|
<TooltipTrigger asChild>
|
|
2119
2415
|
<div className="flex cursor-default items-center gap-1.5 border-r px-3 py-2 transition hover:bg-muted/30">
|
|
2120
2416
|
<Avatar className="size-5 shrink-0 border bg-muted">
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2417
|
+
<AvatarImage
|
|
2418
|
+
src={
|
|
2419
|
+
getUserPhotoUrl(project.clientUserPhotoId) ||
|
|
2420
|
+
getPersonAvatarUrl(project.clientAvatarId)
|
|
2421
|
+
}
|
|
2422
|
+
alt={project.clientName || ''}
|
|
2423
|
+
/>
|
|
2125
2424
|
<AvatarFallback className="text-[9px]">
|
|
2126
2425
|
{getInitials(project.clientName)}
|
|
2127
2426
|
</AvatarFallback>
|
|
@@ -2406,6 +2705,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
2406
2705
|
<TabsTrigger value="team">{t('tabs.team')}</TabsTrigger>
|
|
2407
2706
|
<TabsTrigger value="timeline">{t('tabs.timeline')}</TabsTrigger>
|
|
2408
2707
|
<TabsTrigger value="archive">{t('tabs.archive')}</TabsTrigger>
|
|
2708
|
+
<TabsTrigger value="files">{t('tabs.files')}</TabsTrigger>
|
|
2409
2709
|
<TabsTrigger value="costs">{t('tabs.costs')}</TabsTrigger>
|
|
2410
2710
|
</TabsList>
|
|
2411
2711
|
|
|
@@ -2439,7 +2739,10 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
2439
2739
|
<div className="flex items-center gap-2">
|
|
2440
2740
|
<Avatar className="h-8 w-8 border border-border/60 bg-muted">
|
|
2441
2741
|
<AvatarImage
|
|
2442
|
-
src={
|
|
2742
|
+
src={
|
|
2743
|
+
getUserPhotoUrl(project.clientUserPhotoId) ||
|
|
2744
|
+
getPersonAvatarUrl(project.clientAvatarId)
|
|
2745
|
+
}
|
|
2443
2746
|
alt={project.clientName || commonT('labels.client')}
|
|
2444
2747
|
/>
|
|
2445
2748
|
<AvatarFallback className="bg-muted text-xs font-semibold text-foreground">
|
|
@@ -2581,13 +2884,32 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
2581
2884
|
<div className="font-medium">
|
|
2582
2885
|
{project.relatedContract.name}
|
|
2583
2886
|
</div>
|
|
2584
|
-
<div className="text-sm text-muted-foreground">
|
|
2585
|
-
|
|
2586
|
-
project.relatedContract.code
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2887
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
2888
|
+
<span className="truncate">
|
|
2889
|
+
{project.relatedContract.code || '—'}
|
|
2890
|
+
</span>
|
|
2891
|
+
{project.relatedContract.clientName ? (
|
|
2892
|
+
<>
|
|
2893
|
+
<span>•</span>
|
|
2894
|
+
<Avatar className="h-4 w-4 shrink-0">
|
|
2895
|
+
<AvatarImage
|
|
2896
|
+
src={
|
|
2897
|
+
getUserPhotoUrl(project.clientUserPhotoId) ||
|
|
2898
|
+
getPersonAvatarUrl(project.clientAvatarId)
|
|
2899
|
+
}
|
|
2900
|
+
alt={project.relatedContract.clientName}
|
|
2901
|
+
/>
|
|
2902
|
+
<AvatarFallback className="text-[8px] font-medium">
|
|
2903
|
+
{getInitials(
|
|
2904
|
+
project.relatedContract.clientName
|
|
2905
|
+
)}
|
|
2906
|
+
</AvatarFallback>
|
|
2907
|
+
</Avatar>
|
|
2908
|
+
<span className="truncate">
|
|
2909
|
+
{project.relatedContract.clientName}
|
|
2910
|
+
</span>
|
|
2911
|
+
</>
|
|
2912
|
+
) : null}
|
|
2591
2913
|
</div>
|
|
2592
2914
|
</div>
|
|
2593
2915
|
<div className="flex items-center gap-3">
|
|
@@ -3059,7 +3381,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
3059
3381
|
{(isOver) => (
|
|
3060
3382
|
<div
|
|
3061
3383
|
className={[
|
|
3062
|
-
'flex min-h-
|
|
3384
|
+
'flex min-h-48 max-h-160 flex-col rounded-3xl border bg-linear-to-b p-3 transition-all',
|
|
3063
3385
|
getColumnClassName(column.id),
|
|
3064
3386
|
isOver
|
|
3065
3387
|
? 'border-primary shadow-lg ring-2 ring-primary/15'
|
|
@@ -3102,7 +3424,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
3102
3424
|
</div>
|
|
3103
3425
|
</div>
|
|
3104
3426
|
|
|
3105
|
-
<div className="flex flex-1 flex-col gap-2">
|
|
3427
|
+
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto pb-1 pr-0.5">
|
|
3106
3428
|
<AnimatePresence initial={false}>
|
|
3107
3429
|
{filteredTaskColumns[column.id].map((task) => {
|
|
3108
3430
|
const tags = getTaskTags(task);
|
|
@@ -3830,6 +4152,19 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
3830
4152
|
'rounded-2xl border bg-card p-4 shadow-sm',
|
|
3831
4153
|
isLimitedView ? 'xl:col-span-12' : 'xl:col-span-8',
|
|
3832
4154
|
].join(' ')}
|
|
4155
|
+
actions={
|
|
4156
|
+
!isLimitedView ? (
|
|
4157
|
+
<Button
|
|
4158
|
+
size="sm"
|
|
4159
|
+
variant="outline"
|
|
4160
|
+
className="cursor-pointer"
|
|
4161
|
+
onClick={openAddAssignment}
|
|
4162
|
+
>
|
|
4163
|
+
<UserPlus className="mr-1.5 size-4" />
|
|
4164
|
+
{t('teamPanel.addCollaborator')}
|
|
4165
|
+
</Button>
|
|
4166
|
+
) : undefined
|
|
4167
|
+
}
|
|
3833
4168
|
>
|
|
3834
4169
|
{project.assignments.length > 0 ? (
|
|
3835
4170
|
<div className="space-y-4">
|
|
@@ -3937,6 +4272,34 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
3937
4272
|
<ToneIcon className="size-3" />
|
|
3938
4273
|
{t(`teamPanel.status.${tone.labelKey}`)}
|
|
3939
4274
|
</span>
|
|
4275
|
+
{!isLimitedView ? (
|
|
4276
|
+
<div className="flex items-center gap-1">
|
|
4277
|
+
<Button
|
|
4278
|
+
type="button"
|
|
4279
|
+
variant="ghost"
|
|
4280
|
+
size="icon"
|
|
4281
|
+
className="size-7 cursor-pointer"
|
|
4282
|
+
onClick={() =>
|
|
4283
|
+
openEditAssignment(assignment)
|
|
4284
|
+
}
|
|
4285
|
+
>
|
|
4286
|
+
<Pencil className="size-3.5" />
|
|
4287
|
+
</Button>
|
|
4288
|
+
<Button
|
|
4289
|
+
type="button"
|
|
4290
|
+
variant="ghost"
|
|
4291
|
+
size="icon"
|
|
4292
|
+
className="size-7 cursor-pointer text-destructive hover:text-destructive"
|
|
4293
|
+
onClick={() =>
|
|
4294
|
+
setRemovingAssignmentId(
|
|
4295
|
+
assignment.collaboratorId
|
|
4296
|
+
)
|
|
4297
|
+
}
|
|
4298
|
+
>
|
|
4299
|
+
<Trash2 className="size-3.5" />
|
|
4300
|
+
</Button>
|
|
4301
|
+
</div>
|
|
4302
|
+
) : null}
|
|
3940
4303
|
</div>
|
|
3941
4304
|
</div>
|
|
3942
4305
|
</div>
|
|
@@ -4152,6 +4515,16 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
4152
4515
|
/>
|
|
4153
4516
|
</SectionCard>
|
|
4154
4517
|
</TabsContent>
|
|
4518
|
+
|
|
4519
|
+
<TabsContent value="files">
|
|
4520
|
+
<SectionCard
|
|
4521
|
+
title={t('sections.files')}
|
|
4522
|
+
description={t('sections.filesDescription')}
|
|
4523
|
+
className="rounded-2xl border bg-card p-4 shadow-sm"
|
|
4524
|
+
>
|
|
4525
|
+
<ProjectFileAttachments projectId={projectId} />
|
|
4526
|
+
</SectionCard>
|
|
4527
|
+
</TabsContent>
|
|
4155
4528
|
</Tabs>
|
|
4156
4529
|
|
|
4157
4530
|
<TaskDetailSheet
|
|
@@ -4168,13 +4541,13 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
4168
4541
|
}
|
|
4169
4542
|
footer={
|
|
4170
4543
|
selectedTask && !isLimitedView ? (
|
|
4171
|
-
<div className="grid grid-cols-
|
|
4544
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
|
4172
4545
|
{archivedTasks.some((task) => task.id === selectedTask.id) ? (
|
|
4173
4546
|
<>
|
|
4174
4547
|
<Button
|
|
4175
4548
|
variant="outline"
|
|
4176
4549
|
size="sm"
|
|
4177
|
-
className="h-10 gap-2"
|
|
4550
|
+
className="h-10 gap-2 sm:col-span-2"
|
|
4178
4551
|
disabled={restoringTaskId === selectedTask.id}
|
|
4179
4552
|
onClick={() => void handleRestoreTask(selectedTask.id)}
|
|
4180
4553
|
>
|
|
@@ -4196,26 +4569,408 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
4196
4569
|
</Button>
|
|
4197
4570
|
</>
|
|
4198
4571
|
) : (
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4572
|
+
<>
|
|
4573
|
+
<Button
|
|
4574
|
+
variant="outline"
|
|
4575
|
+
size="sm"
|
|
4576
|
+
className="h-10 gap-2"
|
|
4577
|
+
onClick={() => openTimesheetEntrySheet(selectedTask)}
|
|
4578
|
+
>
|
|
4579
|
+
<Send className="size-3.5" />
|
|
4580
|
+
{commonT('actions.logHours')}
|
|
4581
|
+
</Button>
|
|
4582
|
+
<Button
|
|
4583
|
+
variant="outline"
|
|
4584
|
+
size="sm"
|
|
4585
|
+
className="h-10 gap-2 sm:col-span-2"
|
|
4586
|
+
disabled={archivingTaskId === selectedTask.id}
|
|
4587
|
+
onClick={() => void handleArchiveTask(selectedTask.id)}
|
|
4588
|
+
>
|
|
4589
|
+
{archivingTaskId === selectedTask.id ? (
|
|
4590
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
4591
|
+
) : (
|
|
4592
|
+
<Archive className="size-3.5" />
|
|
4593
|
+
)}
|
|
4594
|
+
{commonT('actions.archive')}
|
|
4595
|
+
</Button>
|
|
4596
|
+
</>
|
|
4213
4597
|
)}
|
|
4214
4598
|
</div>
|
|
4215
4599
|
) : null
|
|
4216
4600
|
}
|
|
4217
4601
|
/>
|
|
4218
4602
|
|
|
4603
|
+
{/* Assignment Add/Edit Sheet */}
|
|
4604
|
+
<Sheet
|
|
4605
|
+
open={assignmentSheetOpen}
|
|
4606
|
+
onOpenChange={(open) => {
|
|
4607
|
+
if (!open) {
|
|
4608
|
+
setAssignmentSheetOpen(false);
|
|
4609
|
+
setSelectedAssignmentCollaborator(null);
|
|
4610
|
+
}
|
|
4611
|
+
}}
|
|
4612
|
+
>
|
|
4613
|
+
<SheetContent className="w-full overflow-y-auto sm:max-w-[min(92vw,28rem)]">
|
|
4614
|
+
<SheetHeader>
|
|
4615
|
+
<SheetTitle>
|
|
4616
|
+
{editingAssignment
|
|
4617
|
+
? t('teamPanel.editTitle')
|
|
4618
|
+
: t('teamPanel.addTitle')}
|
|
4619
|
+
</SheetTitle>
|
|
4620
|
+
<SheetDescription>
|
|
4621
|
+
{t('teamPanel.formDescription')}
|
|
4622
|
+
</SheetDescription>
|
|
4623
|
+
</SheetHeader>
|
|
4624
|
+
<div className="mt-6 space-y-4 px-1 sm:px-4">
|
|
4625
|
+
{!editingAssignment ? (
|
|
4626
|
+
<div className="space-y-2">
|
|
4627
|
+
<Label className="text-sm font-medium">
|
|
4628
|
+
{commonT('labels.collaborator')}
|
|
4629
|
+
</Label>
|
|
4630
|
+
<EntityPicker<OperationsCollaborator>
|
|
4631
|
+
value={
|
|
4632
|
+
assignmentFormData.collaboratorId
|
|
4633
|
+
? Number(assignmentFormData.collaboratorId)
|
|
4634
|
+
: null
|
|
4635
|
+
}
|
|
4636
|
+
onChange={(val, option) => {
|
|
4637
|
+
const collaborator = option ?? null;
|
|
4638
|
+
setSelectedAssignmentCollaborator(collaborator);
|
|
4639
|
+
setAssignmentFormData((prev) => {
|
|
4640
|
+
const collaboratorId = val ? String(val) : '';
|
|
4641
|
+
if (!collaboratorId) {
|
|
4642
|
+
return {
|
|
4643
|
+
...prev,
|
|
4644
|
+
collaboratorId: '',
|
|
4645
|
+
weeklyHours: '',
|
|
4646
|
+
allocationPercent: '',
|
|
4647
|
+
};
|
|
4648
|
+
}
|
|
4649
|
+
|
|
4650
|
+
const capacity = collaborator?.weeklyCapacityHours;
|
|
4651
|
+
if (!capacity || capacity <= 0) {
|
|
4652
|
+
return {
|
|
4653
|
+
...prev,
|
|
4654
|
+
collaboratorId,
|
|
4655
|
+
};
|
|
4656
|
+
}
|
|
4657
|
+
|
|
4658
|
+
const parsedHours = parseNumberInput(prev.weeklyHours);
|
|
4659
|
+
const parsedPercent = parseNumberInput(
|
|
4660
|
+
prev.allocationPercent
|
|
4661
|
+
);
|
|
4662
|
+
|
|
4663
|
+
if (parsedHours != null) {
|
|
4664
|
+
return {
|
|
4665
|
+
...prev,
|
|
4666
|
+
collaboratorId,
|
|
4667
|
+
...syncAssignmentFromWeeklyHours(
|
|
4668
|
+
prev.weeklyHours,
|
|
4669
|
+
prev,
|
|
4670
|
+
collaborator
|
|
4671
|
+
),
|
|
4672
|
+
};
|
|
4673
|
+
}
|
|
4674
|
+
|
|
4675
|
+
if (parsedPercent != null) {
|
|
4676
|
+
return {
|
|
4677
|
+
...prev,
|
|
4678
|
+
collaboratorId,
|
|
4679
|
+
...syncAssignmentFromAllocation(
|
|
4680
|
+
prev.allocationPercent,
|
|
4681
|
+
prev,
|
|
4682
|
+
collaborator
|
|
4683
|
+
),
|
|
4684
|
+
};
|
|
4685
|
+
}
|
|
4686
|
+
|
|
4687
|
+
return {
|
|
4688
|
+
...prev,
|
|
4689
|
+
collaboratorId,
|
|
4690
|
+
weeklyHours: formatAssignmentNumericValue(capacity),
|
|
4691
|
+
allocationPercent: '100',
|
|
4692
|
+
};
|
|
4693
|
+
});
|
|
4694
|
+
}}
|
|
4695
|
+
placeholder={commonT('labels.collaborator')}
|
|
4696
|
+
searchPlaceholder={commonT('labels.collaborator')}
|
|
4697
|
+
loadOptions={loadCollaboratorOptions}
|
|
4698
|
+
getOptionValue={(opt) => opt.id}
|
|
4699
|
+
getOptionLabel={(opt) => opt.displayName}
|
|
4700
|
+
renderOption={({ option }) => (
|
|
4701
|
+
<div className="flex min-w-0 items-center gap-2.5">
|
|
4702
|
+
<Avatar className="size-6 shrink-0">
|
|
4703
|
+
<AvatarImage
|
|
4704
|
+
src={
|
|
4705
|
+
getUserPhotoUrl(option.userPhotoId) ||
|
|
4706
|
+
getPersonAvatarUrl(option.personAvatarId)
|
|
4707
|
+
}
|
|
4708
|
+
alt={option.displayName}
|
|
4709
|
+
/>
|
|
4710
|
+
<AvatarFallback className="text-[9px]">
|
|
4711
|
+
{getInitials(option.displayName)}
|
|
4712
|
+
</AvatarFallback>
|
|
4713
|
+
</Avatar>
|
|
4714
|
+
<div className="min-w-0">
|
|
4715
|
+
<div className="truncate text-sm">
|
|
4716
|
+
{option.displayName}
|
|
4717
|
+
</div>
|
|
4718
|
+
{option.department ? (
|
|
4719
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
4720
|
+
{option.department}
|
|
4721
|
+
</div>
|
|
4722
|
+
) : null}
|
|
4723
|
+
</div>
|
|
4724
|
+
</div>
|
|
4725
|
+
)}
|
|
4726
|
+
valueType="number"
|
|
4727
|
+
clearable
|
|
4728
|
+
allowEmptySelection
|
|
4729
|
+
showCreateButton
|
|
4730
|
+
entityLabel={commonT('labels.collaborator').toLowerCase()}
|
|
4731
|
+
createActionLabel={`${commonT('actions.create')} ${commonT(
|
|
4732
|
+
'labels.collaborator'
|
|
4733
|
+
).toLowerCase()}`}
|
|
4734
|
+
createTitle={`${commonT('actions.create')} ${commonT(
|
|
4735
|
+
'labels.collaborator'
|
|
4736
|
+
).toLowerCase()}`}
|
|
4737
|
+
createDescription={t('teamPanel.formDescription')}
|
|
4738
|
+
createFields={[
|
|
4739
|
+
{
|
|
4740
|
+
name: 'displayName',
|
|
4741
|
+
label: collaboratorFormT('fields.displayName'),
|
|
4742
|
+
placeholder: collaboratorFormT('fields.displayName'),
|
|
4743
|
+
required: true,
|
|
4744
|
+
},
|
|
4745
|
+
{
|
|
4746
|
+
name: 'weeklyCapacityHours',
|
|
4747
|
+
label: collaboratorFormT('fields.weeklyCapacityHours'),
|
|
4748
|
+
placeholder: '40',
|
|
4749
|
+
type: 'number',
|
|
4750
|
+
},
|
|
4751
|
+
]}
|
|
4752
|
+
mapSearchToCreateValues={(search) => ({
|
|
4753
|
+
displayName: search,
|
|
4754
|
+
weeklyCapacityHours: '40',
|
|
4755
|
+
})}
|
|
4756
|
+
onCreate={createAssignmentCollaborator}
|
|
4757
|
+
/>
|
|
4758
|
+
</div>
|
|
4759
|
+
) : (
|
|
4760
|
+
<div className="flex items-center gap-3 rounded-xl border bg-muted/30 p-3">
|
|
4761
|
+
<Avatar className="size-10 border bg-muted">
|
|
4762
|
+
<AvatarImage
|
|
4763
|
+
src={
|
|
4764
|
+
getUserPhotoUrl(editingAssignment.userPhotoId) ||
|
|
4765
|
+
getPersonAvatarUrl(editingAssignment.personAvatarId)
|
|
4766
|
+
}
|
|
4767
|
+
alt={editingAssignment.collaboratorName}
|
|
4768
|
+
/>
|
|
4769
|
+
<AvatarFallback className="text-xs">
|
|
4770
|
+
{getInitials(editingAssignment.collaboratorName)}
|
|
4771
|
+
</AvatarFallback>
|
|
4772
|
+
</Avatar>
|
|
4773
|
+
<div className="min-w-0">
|
|
4774
|
+
<div className="truncate text-sm font-semibold">
|
|
4775
|
+
{editingAssignment.collaboratorName}
|
|
4776
|
+
</div>
|
|
4777
|
+
{editingAssignment.roleLabel ? (
|
|
4778
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
4779
|
+
{editingAssignment.roleLabel}
|
|
4780
|
+
</div>
|
|
4781
|
+
) : null}
|
|
4782
|
+
</div>
|
|
4783
|
+
</div>
|
|
4784
|
+
)}
|
|
4785
|
+
<div className="space-y-2">
|
|
4786
|
+
<Label className="text-sm font-medium">
|
|
4787
|
+
{commonT('labels.allocationPercent')}
|
|
4788
|
+
</Label>
|
|
4789
|
+
<Input
|
|
4790
|
+
type="number"
|
|
4791
|
+
min="0"
|
|
4792
|
+
max="200"
|
|
4793
|
+
value={assignmentFormData.allocationPercent}
|
|
4794
|
+
onChange={(e) =>
|
|
4795
|
+
setAssignmentFormData((prev) => ({
|
|
4796
|
+
...prev,
|
|
4797
|
+
...syncAssignmentFromAllocation(
|
|
4798
|
+
e.target.value,
|
|
4799
|
+
prev,
|
|
4800
|
+
selectedAssignmentCollaborator
|
|
4801
|
+
),
|
|
4802
|
+
}))
|
|
4803
|
+
}
|
|
4804
|
+
placeholder="100"
|
|
4805
|
+
/>
|
|
4806
|
+
</div>
|
|
4807
|
+
<div className="space-y-2">
|
|
4808
|
+
<Label className="text-sm font-medium">
|
|
4809
|
+
{commonT('labels.weeklyCapacity')}
|
|
4810
|
+
</Label>
|
|
4811
|
+
<Input
|
|
4812
|
+
type="number"
|
|
4813
|
+
min="0"
|
|
4814
|
+
value={assignmentFormData.weeklyHours}
|
|
4815
|
+
onChange={(e) =>
|
|
4816
|
+
setAssignmentFormData((prev) => ({
|
|
4817
|
+
...prev,
|
|
4818
|
+
...syncAssignmentFromWeeklyHours(
|
|
4819
|
+
e.target.value,
|
|
4820
|
+
prev,
|
|
4821
|
+
selectedAssignmentCollaborator
|
|
4822
|
+
),
|
|
4823
|
+
}))
|
|
4824
|
+
}
|
|
4825
|
+
placeholder="40"
|
|
4826
|
+
/>
|
|
4827
|
+
</div>
|
|
4828
|
+
<div className="space-y-2">
|
|
4829
|
+
<Label className="text-sm font-medium">
|
|
4830
|
+
{commonT('labels.status')}
|
|
4831
|
+
</Label>
|
|
4832
|
+
<Select
|
|
4833
|
+
value={assignmentFormData.status}
|
|
4834
|
+
onValueChange={(val) =>
|
|
4835
|
+
setAssignmentFormData((prev) => ({ ...prev, status: val }))
|
|
4836
|
+
}
|
|
4837
|
+
>
|
|
4838
|
+
<SelectTrigger className="cursor-pointer">
|
|
4839
|
+
<SelectValue />
|
|
4840
|
+
</SelectTrigger>
|
|
4841
|
+
<SelectContent>
|
|
4842
|
+
<SelectItem value="active">
|
|
4843
|
+
{formatEnumLabel('active')}
|
|
4844
|
+
</SelectItem>
|
|
4845
|
+
<SelectItem value="inactive">
|
|
4846
|
+
{formatEnumLabel('inactive')}
|
|
4847
|
+
</SelectItem>
|
|
4848
|
+
<SelectItem value="completed">
|
|
4849
|
+
{formatEnumLabel('completed')}
|
|
4850
|
+
</SelectItem>
|
|
4851
|
+
</SelectContent>
|
|
4852
|
+
</Select>
|
|
4853
|
+
</div>
|
|
4854
|
+
<div className="grid grid-cols-2 gap-3">
|
|
4855
|
+
<div className="space-y-2">
|
|
4856
|
+
<Label className="text-sm font-medium">
|
|
4857
|
+
{commonT('labels.startDate')}
|
|
4858
|
+
</Label>
|
|
4859
|
+
<Input
|
|
4860
|
+
type="date"
|
|
4861
|
+
value={assignmentFormData.startDate}
|
|
4862
|
+
onChange={(e) =>
|
|
4863
|
+
setAssignmentFormData((prev) => ({
|
|
4864
|
+
...prev,
|
|
4865
|
+
startDate: e.target.value,
|
|
4866
|
+
}))
|
|
4867
|
+
}
|
|
4868
|
+
/>
|
|
4869
|
+
</div>
|
|
4870
|
+
<div className="space-y-2">
|
|
4871
|
+
<Label className="text-sm font-medium">
|
|
4872
|
+
{commonT('labels.endDate')}
|
|
4873
|
+
</Label>
|
|
4874
|
+
<Input
|
|
4875
|
+
type="date"
|
|
4876
|
+
value={assignmentFormData.endDate}
|
|
4877
|
+
onChange={(e) =>
|
|
4878
|
+
setAssignmentFormData((prev) => ({
|
|
4879
|
+
...prev,
|
|
4880
|
+
endDate: e.target.value,
|
|
4881
|
+
}))
|
|
4882
|
+
}
|
|
4883
|
+
/>
|
|
4884
|
+
</div>
|
|
4885
|
+
</div>
|
|
4886
|
+
<div className="flex gap-2 pt-2">
|
|
4887
|
+
<Button
|
|
4888
|
+
variant="outline"
|
|
4889
|
+
className="flex-1 cursor-pointer"
|
|
4890
|
+
onClick={() => setAssignmentSheetOpen(false)}
|
|
4891
|
+
disabled={savingAssignment}
|
|
4892
|
+
>
|
|
4893
|
+
{commonT('actions.cancel')}
|
|
4894
|
+
</Button>
|
|
4895
|
+
<Button
|
|
4896
|
+
className="flex-1 cursor-pointer"
|
|
4897
|
+
disabled={
|
|
4898
|
+
savingAssignment ||
|
|
4899
|
+
(!editingAssignment && !assignmentFormData.collaboratorId)
|
|
4900
|
+
}
|
|
4901
|
+
onClick={() => void handleSaveAssignment()}
|
|
4902
|
+
>
|
|
4903
|
+
{savingAssignment ? (
|
|
4904
|
+
<Loader2 className="mr-2 size-4 animate-spin" />
|
|
4905
|
+
) : null}
|
|
4906
|
+
{commonT('actions.save')}
|
|
4907
|
+
</Button>
|
|
4908
|
+
</div>
|
|
4909
|
+
</div>
|
|
4910
|
+
</SheetContent>
|
|
4911
|
+
</Sheet>
|
|
4912
|
+
|
|
4913
|
+
{/* Remove Assignment Confirm Dialog */}
|
|
4914
|
+
<Dialog
|
|
4915
|
+
open={removingAssignmentId !== null}
|
|
4916
|
+
onOpenChange={(open) => {
|
|
4917
|
+
if (!open) setRemovingAssignmentId(null);
|
|
4918
|
+
}}
|
|
4919
|
+
>
|
|
4920
|
+
<DialogContent className="sm:max-w-sm">
|
|
4921
|
+
<DialogHeader>
|
|
4922
|
+
<DialogTitle>{t('teamPanel.removeTitle')}</DialogTitle>
|
|
4923
|
+
<DialogDescription>
|
|
4924
|
+
{t('teamPanel.removeDescription')}
|
|
4925
|
+
</DialogDescription>
|
|
4926
|
+
</DialogHeader>
|
|
4927
|
+
<DialogFooter className="mt-4">
|
|
4928
|
+
<Button
|
|
4929
|
+
variant="outline"
|
|
4930
|
+
onClick={() => setRemovingAssignmentId(null)}
|
|
4931
|
+
>
|
|
4932
|
+
{commonT('actions.cancel')}
|
|
4933
|
+
</Button>
|
|
4934
|
+
<Button
|
|
4935
|
+
variant="destructive"
|
|
4936
|
+
onClick={() => void handleConfirmRemoveAssignment()}
|
|
4937
|
+
>
|
|
4938
|
+
{commonT('actions.delete')}
|
|
4939
|
+
</Button>
|
|
4940
|
+
</DialogFooter>
|
|
4941
|
+
</DialogContent>
|
|
4942
|
+
</Dialog>
|
|
4943
|
+
|
|
4944
|
+
<TimesheetEntryCreateSheet
|
|
4945
|
+
open={isTimesheetEntrySheetOpen}
|
|
4946
|
+
onOpenChange={(open) => {
|
|
4947
|
+
setIsTimesheetEntrySheetOpen(open);
|
|
4948
|
+
if (!open) {
|
|
4949
|
+
setTimesheetPrefill(null);
|
|
4950
|
+
}
|
|
4951
|
+
}}
|
|
4952
|
+
project={
|
|
4953
|
+
timesheetPrefill
|
|
4954
|
+
? {
|
|
4955
|
+
id: timesheetPrefill.projectId,
|
|
4956
|
+
label: timesheetPrefill.projectLabel,
|
|
4957
|
+
projectAssignmentId: timesheetPrefill.projectAssignmentId,
|
|
4958
|
+
}
|
|
4959
|
+
: null
|
|
4960
|
+
}
|
|
4961
|
+
task={
|
|
4962
|
+
timesheetPrefill
|
|
4963
|
+
? {
|
|
4964
|
+
id: timesheetPrefill.taskId,
|
|
4965
|
+
label: timesheetPrefill.taskLabel,
|
|
4966
|
+
}
|
|
4967
|
+
: null
|
|
4968
|
+
}
|
|
4969
|
+
onCreated={() => {
|
|
4970
|
+
void refetchTasks();
|
|
4971
|
+
}}
|
|
4972
|
+
/>
|
|
4973
|
+
|
|
4219
4974
|
{!isLimitedView ? (
|
|
4220
4975
|
<Sheet
|
|
4221
4976
|
open={isEditSheetOpen}
|
|
@@ -4244,284 +4999,39 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
4244
4999
|
) : null}
|
|
4245
5000
|
|
|
4246
5001
|
{!isLimitedView ? (
|
|
4247
|
-
<
|
|
5002
|
+
<TaskFormSheet
|
|
4248
5003
|
open={taskFormOpen}
|
|
4249
5004
|
onOpenChange={(open) => {
|
|
4250
5005
|
if (!open) {
|
|
4251
5006
|
setTaskFormOpen(false);
|
|
4252
5007
|
setEditingTaskId(null);
|
|
4253
|
-
setTaskFormData(EMPTY_TASK_FORM);
|
|
4254
5008
|
}
|
|
4255
5009
|
}}
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
<Label htmlFor="task-name">
|
|
4282
|
-
{t('taskForm.nameLabel')} *
|
|
4283
|
-
</Label>
|
|
4284
|
-
<Input
|
|
4285
|
-
id="task-name"
|
|
4286
|
-
placeholder={t('taskForm.namePlaceholder')}
|
|
4287
|
-
value={taskFormData.name}
|
|
4288
|
-
onChange={(e) =>
|
|
4289
|
-
setTaskFormData((prev) => ({
|
|
4290
|
-
...prev,
|
|
4291
|
-
name: e.target.value,
|
|
4292
|
-
}))
|
|
4293
|
-
}
|
|
4294
|
-
/>
|
|
4295
|
-
</div>
|
|
4296
|
-
|
|
4297
|
-
<div className="space-y-1.5">
|
|
4298
|
-
<Label htmlFor="task-description">
|
|
4299
|
-
{t('taskForm.descriptionLabel')}
|
|
4300
|
-
</Label>
|
|
4301
|
-
<RichTextEditor
|
|
4302
|
-
value={taskFormData.description}
|
|
4303
|
-
onChange={(val) =>
|
|
4304
|
-
setTaskFormData((prev) => ({
|
|
4305
|
-
...prev,
|
|
4306
|
-
description: val,
|
|
4307
|
-
}))
|
|
4308
|
-
}
|
|
4309
|
-
mentions={mentionItems}
|
|
4310
|
-
/>
|
|
4311
|
-
</div>
|
|
4312
|
-
|
|
4313
|
-
<div className="grid grid-cols-2 gap-3">
|
|
4314
|
-
<div className="space-y-1.5">
|
|
4315
|
-
<Label>{t('taskForm.priorityLabel')}</Label>
|
|
4316
|
-
<Select
|
|
4317
|
-
value={taskFormData.priority}
|
|
4318
|
-
onValueChange={(v) =>
|
|
4319
|
-
setTaskFormData((prev) => ({
|
|
4320
|
-
...prev,
|
|
4321
|
-
priority: v as TaskFormState['priority'],
|
|
4322
|
-
}))
|
|
4323
|
-
}
|
|
4324
|
-
>
|
|
4325
|
-
<SelectTrigger className="w-full">
|
|
4326
|
-
<SelectValue />
|
|
4327
|
-
</SelectTrigger>
|
|
4328
|
-
<SelectContent>
|
|
4329
|
-
<SelectItem value="low">
|
|
4330
|
-
{getTaskPriorityLabel('low')}
|
|
4331
|
-
</SelectItem>
|
|
4332
|
-
<SelectItem value="medium">
|
|
4333
|
-
{getTaskPriorityLabel('medium')}
|
|
4334
|
-
</SelectItem>
|
|
4335
|
-
<SelectItem value="high">
|
|
4336
|
-
{getTaskPriorityLabel('high')}
|
|
4337
|
-
</SelectItem>
|
|
4338
|
-
</SelectContent>
|
|
4339
|
-
</Select>
|
|
4340
|
-
</div>
|
|
4341
|
-
|
|
4342
|
-
<div className="space-y-1.5">
|
|
4343
|
-
<Label>{t('taskForm.columnLabel')}</Label>
|
|
4344
|
-
<Select
|
|
4345
|
-
value={taskFormData.status}
|
|
4346
|
-
onValueChange={(v) =>
|
|
4347
|
-
setTaskFormData((prev) => ({
|
|
4348
|
-
...prev,
|
|
4349
|
-
status: v as BoardColumnId,
|
|
4350
|
-
}))
|
|
4351
|
-
}
|
|
4352
|
-
>
|
|
4353
|
-
<SelectTrigger className="w-full">
|
|
4354
|
-
<SelectValue />
|
|
4355
|
-
</SelectTrigger>
|
|
4356
|
-
<SelectContent>
|
|
4357
|
-
{KANBAN_COLUMNS.map((col) => (
|
|
4358
|
-
<SelectItem key={col.id} value={col.id}>
|
|
4359
|
-
{col.label}
|
|
4360
|
-
</SelectItem>
|
|
4361
|
-
))}
|
|
4362
|
-
</SelectContent>
|
|
4363
|
-
</Select>
|
|
4364
|
-
</div>
|
|
4365
|
-
</div>
|
|
4366
|
-
|
|
4367
|
-
<div className="space-y-1.5">
|
|
4368
|
-
<Label>Responsável</Label>
|
|
4369
|
-
<Select
|
|
4370
|
-
value={taskFormData.assigneeCollaboratorId}
|
|
4371
|
-
onValueChange={(value) =>
|
|
4372
|
-
setTaskFormData((prev) => ({
|
|
4373
|
-
...prev,
|
|
4374
|
-
assigneeCollaboratorId: value,
|
|
4375
|
-
}))
|
|
4376
|
-
}
|
|
4377
|
-
>
|
|
4378
|
-
<SelectTrigger className="w-full">
|
|
4379
|
-
<SelectValue
|
|
4380
|
-
placeholder={commonT('labels.notAssigned')}
|
|
4381
|
-
/>
|
|
4382
|
-
</SelectTrigger>
|
|
4383
|
-
<SelectContent>
|
|
4384
|
-
<SelectItem value="none">
|
|
4385
|
-
{commonT('labels.notAssigned')}
|
|
4386
|
-
</SelectItem>
|
|
4387
|
-
{taskAssigneeOptions.map((option) => (
|
|
4388
|
-
<SelectItem key={option.id} value={option.id}>
|
|
4389
|
-
{option.label}
|
|
4390
|
-
</SelectItem>
|
|
4391
|
-
))}
|
|
4392
|
-
</SelectContent>
|
|
4393
|
-
</Select>
|
|
4394
|
-
</div>
|
|
4395
|
-
|
|
4396
|
-
<div className="grid grid-cols-2 gap-3">
|
|
4397
|
-
<div className="space-y-1.5">
|
|
4398
|
-
<Label htmlFor="task-due-date">
|
|
4399
|
-
{t('taskForm.deadlineLabel')}
|
|
4400
|
-
</Label>
|
|
4401
|
-
<Input
|
|
4402
|
-
id="task-due-date"
|
|
4403
|
-
type="date"
|
|
4404
|
-
value={taskFormData.dueDate}
|
|
4405
|
-
onChange={(e) =>
|
|
4406
|
-
setTaskFormData((prev) => ({
|
|
4407
|
-
...prev,
|
|
4408
|
-
dueDate: e.target.value,
|
|
4409
|
-
}))
|
|
4410
|
-
}
|
|
4411
|
-
/>
|
|
4412
|
-
</div>
|
|
4413
|
-
|
|
4414
|
-
<div className="space-y-1.5">
|
|
4415
|
-
<Label htmlFor="task-estimate">
|
|
4416
|
-
{t('taskForm.estimateLabel')}
|
|
4417
|
-
</Label>
|
|
4418
|
-
<Input
|
|
4419
|
-
id="task-estimate"
|
|
4420
|
-
type="number"
|
|
4421
|
-
min="0"
|
|
4422
|
-
step="0.5"
|
|
4423
|
-
placeholder="0"
|
|
4424
|
-
value={taskFormData.estimateHours}
|
|
4425
|
-
onChange={(e) =>
|
|
4426
|
-
setTaskFormData((prev) => ({
|
|
4427
|
-
...prev,
|
|
4428
|
-
estimateHours: e.target.value,
|
|
4429
|
-
}))
|
|
4430
|
-
}
|
|
4431
|
-
/>
|
|
4432
|
-
</div>
|
|
4433
|
-
</div>
|
|
4434
|
-
|
|
4435
|
-
<div className="space-y-1.5">
|
|
4436
|
-
<Label htmlFor="task-tags">{t('taskForm.tagsLabel')}</Label>
|
|
4437
|
-
<Input
|
|
4438
|
-
id="task-tags"
|
|
4439
|
-
placeholder={t('taskForm.tagsPlaceholder')}
|
|
4440
|
-
value={taskFormData.tags}
|
|
4441
|
-
onChange={(e) =>
|
|
4442
|
-
setTaskFormData((prev) => ({
|
|
4443
|
-
...prev,
|
|
4444
|
-
tags: e.target.value,
|
|
4445
|
-
}))
|
|
4446
|
-
}
|
|
4447
|
-
/>
|
|
4448
|
-
</div>
|
|
4449
|
-
|
|
4450
|
-
{editingTaskId ? (
|
|
4451
|
-
<div className="space-y-1.5">
|
|
4452
|
-
<Label className="flex items-center gap-1.5">
|
|
4453
|
-
<Paperclip className="size-3.5" />
|
|
4454
|
-
{t('taskForm.attachmentsLabel')}
|
|
4455
|
-
</Label>
|
|
4456
|
-
<TaskFileAttachments taskId={editingTaskId} />
|
|
4457
|
-
</div>
|
|
4458
|
-
) : null}
|
|
4459
|
-
</div>
|
|
4460
|
-
|
|
4461
|
-
<div className="mt-4 flex flex-wrap items-center justify-between gap-2 border-t px-4 pb-4 pt-4">
|
|
4462
|
-
<div className="flex gap-2">
|
|
4463
|
-
{editingTaskId ? (
|
|
4464
|
-
<Button
|
|
4465
|
-
type="button"
|
|
4466
|
-
variant="outline"
|
|
4467
|
-
disabled={
|
|
4468
|
-
taskFormLoading || archivingTaskId === editingTaskId
|
|
4469
|
-
}
|
|
4470
|
-
onClick={() => {
|
|
4471
|
-
if (!editingTaskId) return;
|
|
4472
|
-
const id = editingTaskId;
|
|
4473
|
-
setTaskFormOpen(false);
|
|
4474
|
-
setEditingTaskId(null);
|
|
4475
|
-
setTaskFormData(EMPTY_TASK_FORM);
|
|
4476
|
-
void handleArchiveTask(id);
|
|
4477
|
-
}}
|
|
4478
|
-
>
|
|
4479
|
-
{archivingTaskId === editingTaskId ? (
|
|
4480
|
-
<Loader2 className="mr-2 size-4 animate-spin" />
|
|
4481
|
-
) : (
|
|
4482
|
-
<Archive className="mr-2 size-4" />
|
|
4483
|
-
)}
|
|
4484
|
-
{commonT('actions.archive')}
|
|
4485
|
-
</Button>
|
|
4486
|
-
) : null}
|
|
4487
|
-
</div>
|
|
4488
|
-
<div className="flex gap-2">
|
|
4489
|
-
<Button
|
|
4490
|
-
variant="outline"
|
|
4491
|
-
onClick={() => {
|
|
4492
|
-
setTaskFormOpen(false);
|
|
4493
|
-
setEditingTaskId(null);
|
|
4494
|
-
setTaskFormData(EMPTY_TASK_FORM);
|
|
4495
|
-
}}
|
|
4496
|
-
disabled={taskFormLoading}
|
|
4497
|
-
>
|
|
4498
|
-
{commonT('actions.cancel')}
|
|
4499
|
-
</Button>
|
|
4500
|
-
<Button
|
|
4501
|
-
onClick={() => void handleTaskFormSubmit()}
|
|
4502
|
-
disabled={taskFormLoading || !taskFormData.name.trim()}
|
|
4503
|
-
>
|
|
4504
|
-
{taskFormLoading
|
|
4505
|
-
? t('taskForm.saving')
|
|
4506
|
-
: editingTaskId
|
|
4507
|
-
? commonT('actions.save')
|
|
4508
|
-
: commonT('actions.create')}
|
|
4509
|
-
</Button>
|
|
4510
|
-
</div>
|
|
4511
|
-
</div>
|
|
4512
|
-
</TabsContent>
|
|
4513
|
-
|
|
4514
|
-
{editingTaskId ? (
|
|
4515
|
-
<TabsContent
|
|
4516
|
-
value="comments"
|
|
4517
|
-
className="min-h-0 flex-1 overflow-y-auto px-4 py-2 data-[state=inactive]:hidden"
|
|
4518
|
-
>
|
|
4519
|
-
<TaskCommentsSection taskId={editingTaskId} />
|
|
4520
|
-
</TabsContent>
|
|
4521
|
-
) : null}
|
|
4522
|
-
</Tabs>
|
|
4523
|
-
</SheetContent>
|
|
4524
|
-
</Sheet>
|
|
5010
|
+
request={request}
|
|
5011
|
+
editingTask={editingTaskAsOption}
|
|
5012
|
+
showProject={false}
|
|
5013
|
+
defaultProjectId={projectId}
|
|
5014
|
+
defaultStatus={createDefaultStatus}
|
|
5015
|
+
assigneeOptions={taskAssigneeOptions}
|
|
5016
|
+
onArchive={handleArchiveTask}
|
|
5017
|
+
onLogHours={
|
|
5018
|
+
editingTask
|
|
5019
|
+
? () =>
|
|
5020
|
+
openTimesheetEntrySheet(
|
|
5021
|
+
editingTask as unknown as TaskDetailSheetData
|
|
5022
|
+
)
|
|
5023
|
+
: undefined
|
|
5024
|
+
}
|
|
5025
|
+
onCountChanged={() => {
|
|
5026
|
+
setBoardState(null);
|
|
5027
|
+
void refetchTasks();
|
|
5028
|
+
}}
|
|
5029
|
+
onSaved={() => {
|
|
5030
|
+
setBoardState(null);
|
|
5031
|
+
void refetchTasks();
|
|
5032
|
+
void refetchArchivedTasks();
|
|
5033
|
+
}}
|
|
5034
|
+
/>
|
|
4525
5035
|
) : null}
|
|
4526
5036
|
|
|
4527
5037
|
{!isLimitedView ? (
|
|
@@ -4535,10 +5045,10 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
4535
5045
|
<DialogContent className="sm:max-w-sm">
|
|
4536
5046
|
<DialogHeader>
|
|
4537
5047
|
<DialogTitle>{t('dialogs.deleteTitle')}</DialogTitle>
|
|
5048
|
+
<DialogDescription>
|
|
5049
|
+
{t('dialogs.deleteDescription')}
|
|
5050
|
+
</DialogDescription>
|
|
4538
5051
|
</DialogHeader>
|
|
4539
|
-
<p className="text-sm text-muted-foreground">
|
|
4540
|
-
{t('dialogs.deleteDescription')}
|
|
4541
|
-
</p>
|
|
4542
5052
|
<DialogFooter className="mt-4">
|
|
4543
5053
|
<Button
|
|
4544
5054
|
variant="outline"
|
|
@@ -4561,6 +5071,57 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
4561
5071
|
</DialogFooter>
|
|
4562
5072
|
</DialogContent>
|
|
4563
5073
|
</Dialog>
|
|
5074
|
+
|
|
5075
|
+
<Dialog
|
|
5076
|
+
open={deleteProjectDialogOpen}
|
|
5077
|
+
onOpenChange={(open) => {
|
|
5078
|
+
if (!open) {
|
|
5079
|
+
setDeleteProjectDialogOpen(false);
|
|
5080
|
+
setDeleteProjectConfirmText('');
|
|
5081
|
+
}
|
|
5082
|
+
}}
|
|
5083
|
+
>
|
|
5084
|
+
<DialogContent className="sm:max-w-md">
|
|
5085
|
+
<DialogHeader>
|
|
5086
|
+
<DialogTitle>{t('deleteConfirmTitle')}</DialogTitle>
|
|
5087
|
+
</DialogHeader>
|
|
5088
|
+
<p className="text-sm text-muted-foreground">
|
|
5089
|
+
{t('deleteConfirmDescription')}
|
|
5090
|
+
</p>
|
|
5091
|
+
<div className="space-y-1.5">
|
|
5092
|
+
<Label className="text-xs">{t('deleteConfirmLabel')}</Label>
|
|
5093
|
+
<Input
|
|
5094
|
+
value={deleteProjectConfirmText}
|
|
5095
|
+
onChange={(e) => setDeleteProjectConfirmText(e.target.value)}
|
|
5096
|
+
placeholder={project?.name ?? ''}
|
|
5097
|
+
/>
|
|
5098
|
+
</div>
|
|
5099
|
+
<DialogFooter className="mt-2">
|
|
5100
|
+
<Button
|
|
5101
|
+
variant="outline"
|
|
5102
|
+
onClick={() => {
|
|
5103
|
+
setDeleteProjectDialogOpen(false);
|
|
5104
|
+
setDeleteProjectConfirmText('');
|
|
5105
|
+
}}
|
|
5106
|
+
>
|
|
5107
|
+
{commonT('actions.cancel')}
|
|
5108
|
+
</Button>
|
|
5109
|
+
<Button
|
|
5110
|
+
variant="destructive"
|
|
5111
|
+
disabled={
|
|
5112
|
+
deletingProject ||
|
|
5113
|
+
deleteProjectConfirmText !== (project?.name ?? '')
|
|
5114
|
+
}
|
|
5115
|
+
onClick={() => void handleDeleteProject()}
|
|
5116
|
+
>
|
|
5117
|
+
{deletingProject ? (
|
|
5118
|
+
<Loader2 className="mr-2 size-3.5 animate-spin" />
|
|
5119
|
+
) : null}
|
|
5120
|
+
{t('deleteConfirmButton')}
|
|
5121
|
+
</Button>
|
|
5122
|
+
</DialogFooter>
|
|
5123
|
+
</DialogContent>
|
|
5124
|
+
</Dialog>
|
|
4564
5125
|
</>
|
|
4565
5126
|
) : null}
|
|
4566
5127
|
</Page>
|