@open-mercato/core 0.7.1-develop.7178.1.ab93fbaf4e → 0.7.1-develop.7180.1.9717fbbb43
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/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/staff_time_entry/index.js +14 -0
- package/dist/generated/entities/staff_time_entry/index.js.map +2 -2
- package/dist/generated/entities/staff_time_entry_tag/index.js +15 -0
- package/dist/generated/entities/staff_time_entry_tag/index.js.map +7 -0
- package/dist/generated/entities/staff_time_project/index.js +16 -0
- package/dist/generated/entities/staff_time_project/index.js.map +2 -2
- package/dist/generated/entities/staff_time_report/index.js +57 -0
- package/dist/generated/entities/staff_time_report/index.js.map +7 -0
- package/dist/generated/entities/staff_time_report_entry/index.js +27 -0
- package/dist/generated/entities/staff_time_report_entry/index.js.map +7 -0
- package/dist/generated/entities/staff_time_report_event/index.js +21 -0
- package/dist/generated/entities/staff_time_report_event/index.js.map +7 -0
- package/dist/generated/entities/staff_time_report_project/index.js +15 -0
- package/dist/generated/entities/staff_time_report_project/index.js.map +7 -0
- package/dist/generated/entities/staff_time_tag/index.js +21 -0
- package/dist/generated/entities/staff_time_tag/index.js.map +7 -0
- package/dist/generated/entities/staff_time_task/index.js +37 -0
- package/dist/generated/entities/staff_time_task/index.js.map +7 -0
- package/dist/generated/entities/staff_time_task_comment/index.js +21 -0
- package/dist/generated/entities/staff_time_task_comment/index.js.map +7 -0
- package/dist/generated/entities/staff_time_task_status/index.js +29 -0
- package/dist/generated/entities/staff_time_task_status/index.js.map +7 -0
- package/dist/generated/entities/staff_time_task_tag/index.js +15 -0
- package/dist/generated/entities/staff_time_task_tag/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +11 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +149 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/timesheetFixtures.js +7 -0
- package/dist/helpers/integration/timesheetFixtures.js.map +2 -2
- package/dist/modules/auth/cli.js +16 -0
- package/dist/modules/auth/cli.js.map +2 -2
- package/dist/modules/auth/lib/backendChrome.js +1 -0
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerRoleAcls.js +44 -0
- package/dist/modules/customer_accounts/lib/customerRoleAcls.js.map +7 -0
- package/dist/modules/customer_accounts/setup.js +1 -28
- package/dist/modules/customer_accounts/setup.js.map +2 -2
- package/dist/modules/data_sync/api/options.js +2 -0
- package/dist/modules/data_sync/api/options.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +39 -17
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/lib/start-controls.js +42 -0
- package/dist/modules/data_sync/lib/start-controls.js.map +7 -0
- package/dist/modules/staff/acl.js +9 -1
- package/dist/modules/staff/acl.js.map +2 -2
- package/dist/modules/staff/ai-agents.js +142 -0
- package/dist/modules/staff/ai-agents.js.map +7 -0
- package/dist/modules/staff/ai-tools/time-tracking-pack.js +348 -0
- package/dist/modules/staff/ai-tools/time-tracking-pack.js.map +7 -0
- package/dist/modules/staff/ai-tools/types.js +54 -0
- package/dist/modules/staff/ai-tools/types.js.map +7 -0
- package/dist/modules/staff/ai-tools.js +8 -0
- package/dist/modules/staff/ai-tools.js.map +7 -0
- package/dist/modules/staff/analytics.js +84 -1
- package/dist/modules/staff/analytics.js.map +2 -2
- package/dist/modules/staff/api/guards.js +37 -3
- package/dist/modules/staff/api/guards.js.map +2 -2
- package/dist/modules/staff/api/leave-requests/accept/route.js +1 -3
- package/dist/modules/staff/api/leave-requests/accept/route.js.map +2 -2
- package/dist/modules/staff/api/leave-requests/reject/route.js +1 -3
- package/dist/modules/staff/api/leave-requests/reject/route.js.map +2 -2
- package/dist/modules/staff/api/portal/time-reports/[id]/route.js +131 -0
- package/dist/modules/staff/api/portal/time-reports/[id]/route.js.map +7 -0
- package/dist/modules/staff/api/portal/time-reports/route.js +179 -0
- package/dist/modules/staff/api/portal/time-reports/route.js.map +7 -0
- package/dist/modules/staff/api/team-members/self/route.js +1 -3
- package/dist/modules/staff/api/team-members/self/route.js.map +2 -2
- package/dist/modules/staff/api/team-members/tags/assign/route.js +1 -3
- package/dist/modules/staff/api/team-members/tags/assign/route.js.map +2 -2
- package/dist/modules/staff/api/team-members/tags/unassign/route.js +1 -3
- package/dist/modules/staff/api/team-members/tags/unassign/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/_shared/withTimesheetInterceptors.js +174 -0
- package/dist/modules/staff/api/timesheets/_shared/withTimesheetInterceptors.js.map +7 -0
- package/dist/modules/staff/api/timesheets/access-requests/route.js +226 -0
- package/dist/modules/staff/api/timesheets/access-requests/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/my-projects/[projectId]/route.js +15 -8
- package/dist/modules/staff/api/timesheets/my-projects/[projectId]/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/my-projects/route.js +13 -1
- package/dist/modules/staff/api/timesheets/my-projects/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/my-work/myWorkAggregate.js +42 -0
- package/dist/modules/staff/api/timesheets/my-work/myWorkAggregate.js.map +7 -0
- package/dist/modules/staff/api/timesheets/my-work/route.js +383 -0
- package/dist/modules/staff/api/timesheets/my-work/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/projects/kpis/route.js +14 -2
- package/dist/modules/staff/api/timesheets/projects/kpis/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/reports/[id]/close/route.js +168 -0
- package/dist/modules/staff/api/timesheets/reports/[id]/close/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/reports/[id]/export/route.js +246 -0
- package/dist/modules/staff/api/timesheets/reports/[id]/export/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/reports/[id]/sheet/route.js +183 -0
- package/dist/modules/staff/api/timesheets/reports/[id]/sheet/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/reports/[id]/unlock/route.js +164 -0
- package/dist/modules/staff/api/timesheets/reports/[id]/unlock/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/reports/preview/route.js +360 -0
- package/dist/modules/staff/api/timesheets/reports/preview/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/reports/route.js +316 -0
- package/dist/modules/staff/api/timesheets/reports/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/reports/shared.js +63 -0
- package/dist/modules/staff/api/timesheets/reports/shared.js.map +7 -0
- package/dist/modules/staff/api/timesheets/settings/reapply-rounding/route.js +164 -0
- package/dist/modules/staff/api/timesheets/settings/reapply-rounding/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/settings/rounding-impact/route.js +168 -0
- package/dist/modules/staff/api/timesheets/settings/rounding-impact/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/settings/route.js +199 -0
- package/dist/modules/staff/api/timesheets/settings/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tags/access.js +76 -0
- package/dist/modules/staff/api/timesheets/tags/access.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tags/entry-assignments/route.js +235 -0
- package/dist/modules/staff/api/timesheets/tags/entry-assignments/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tags/route.js +160 -0
- package/dist/modules/staff/api/timesheets/tags/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tags/task-assignments/route.js +233 -0
- package/dist/modules/staff/api/timesheets/tags/task-assignments/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/task-statuses/route.js +188 -0
- package/dist/modules/staff/api/timesheets/task-statuses/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js +399 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/status/route.js +210 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/status/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/route.js +333 -0
- package/dist/modules/staff/api/timesheets/tasks/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/time-entries/[id]/duplicate/route.js +228 -0
- package/dist/modules/staff/api/timesheets/time-entries/[id]/duplicate/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.js +29 -10
- package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/route.js +31 -18
- package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js +21 -7
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.js +27 -7
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/bulk/route.js +215 -32
- package/dist/modules/staff/api/timesheets/time-entries/bulk/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/copy-day/route.js +378 -0
- package/dist/modules/staff/api/timesheets/time-entries/copy-day/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/time-entries/overlaps/route.js +340 -0
- package/dist/modules/staff/api/timesheets/time-entries/overlaps/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/time-entries/route.js +238 -26
- package/dist/modules/staff/api/timesheets/time-entries/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/start-timer/route.js +23 -8
- package/dist/modules/staff/api/timesheets/time-entries/start-timer/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/[id]/change-currency/route.js +209 -0
- package/dist/modules/staff/api/timesheets/time-projects/[id]/change-currency/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +59 -2
- package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/route.js +264 -22
- package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/board/page.js +110 -0
- package/dist/modules/staff/backend/staff/time-tracking/board/page.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/board/page.meta.js +18 -0
- package/dist/modules/staff/backend/staff/time-tracking/board/page.meta.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/entries/page.js +981 -0
- package/dist/modules/staff/backend/staff/time-tracking/entries/page.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/entries/page.meta.js +18 -0
- package/dist/modules/staff/backend/staff/time-tracking/entries/page.meta.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/page.js +583 -0
- package/dist/modules/staff/backend/staff/time-tracking/page.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/page.meta.js +15 -0
- package/dist/modules/staff/backend/staff/time-tracking/page.meta.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/board/page.js +65 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/board/page.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/board/page.meta.js +16 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/board/page.meta.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +229 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.meta.js +16 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.meta.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +1014 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.meta.js +16 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.meta.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/create/page.js +97 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/create/page.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/create/page.meta.js +16 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/create/page.meta.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/page.js +1040 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/page.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/page.meta.js +19 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/page.meta.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js +353 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.js +457 -0
- package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.meta.js +16 -0
- package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.meta.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/reports/create/page.js +697 -0
- package/dist/modules/staff/backend/staff/time-tracking/reports/create/page.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/reports/create/page.meta.js +16 -0
- package/dist/modules/staff/backend/staff/time-tracking/reports/create/page.meta.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/reports/page.js +174 -0
- package/dist/modules/staff/backend/staff/time-tracking/reports/page.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/reports/page.meta.js +18 -0
- package/dist/modules/staff/backend/staff/time-tracking/reports/page.meta.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/settings/page.js +546 -0
- package/dist/modules/staff/backend/staff/time-tracking/settings/page.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/settings/page.meta.js +18 -0
- package/dist/modules/staff/backend/staff/time-tracking/settings/page.meta.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/timesheet/GridView.js +710 -0
- package/dist/modules/staff/backend/staff/time-tracking/timesheet/GridView.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/timesheet/page.js +646 -0
- package/dist/modules/staff/backend/staff/time-tracking/timesheet/page.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/timesheet/page.meta.js +18 -0
- package/dist/modules/staff/backend/staff/time-tracking/timesheet/page.meta.js.map +7 -0
- package/dist/modules/staff/backend/staff/timesheets/page.js +4 -793
- package/dist/modules/staff/backend/staff/timesheets/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/timesheets/page.meta.js +2 -7
- package/dist/modules/staff/backend/staff/timesheets/page.meta.js.map +2 -2
- package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.js +5 -136
- package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.js +1 -7
- package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.js.map +2 -2
- package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.js +5 -445
- package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.js.map +3 -3
- package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.js +1 -7
- package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.js.map +2 -2
- package/dist/modules/staff/backend/staff/timesheets/projects/create/page.js +4 -76
- package/dist/modules/staff/backend/staff/timesheets/projects/create/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/timesheets/projects/create/page.meta.js +1 -7
- package/dist/modules/staff/backend/staff/timesheets/projects/create/page.meta.js.map +2 -2
- package/dist/modules/staff/backend/staff/timesheets/projects/page.js +4 -613
- package/dist/modules/staff/backend/staff/timesheets/projects/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/timesheets/projects/page.meta.js +2 -17
- package/dist/modules/staff/backend/staff/timesheets/projects/page.meta.js.map +2 -2
- package/dist/modules/staff/ce.js +40 -0
- package/dist/modules/staff/ce.js.map +2 -2
- package/dist/modules/staff/cli.js +166 -1
- package/dist/modules/staff/cli.js.map +2 -2
- package/dist/modules/staff/commands/index.js +5 -0
- package/dist/modules/staff/commands/index.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-entries.js +569 -21
- package/dist/modules/staff/commands/timesheets-entries.js.map +3 -3
- package/dist/modules/staff/commands/timesheets-projects.js +450 -7
- package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-reports.js +1036 -0
- package/dist/modules/staff/commands/timesheets-reports.js.map +7 -0
- package/dist/modules/staff/commands/timesheets-tags.js +1015 -0
- package/dist/modules/staff/commands/timesheets-tags.js.map +7 -0
- package/dist/modules/staff/commands/timesheets-task-comments.js +438 -0
- package/dist/modules/staff/commands/timesheets-task-comments.js.map +7 -0
- package/dist/modules/staff/commands/timesheets-task-statuses.js +723 -0
- package/dist/modules/staff/commands/timesheets-task-statuses.js.map +7 -0
- package/dist/modules/staff/commands/timesheets-tasks.js +834 -0
- package/dist/modules/staff/commands/timesheets-tasks.js.map +7 -0
- package/dist/modules/staff/data/enrichers.js +426 -12
- package/dist/modules/staff/data/enrichers.js.map +2 -2
- package/dist/modules/staff/data/entities.js +552 -3
- package/dist/modules/staff/data/entities.js.map +2 -2
- package/dist/modules/staff/data/extensions.js +41 -0
- package/dist/modules/staff/data/extensions.js.map +7 -0
- package/dist/modules/staff/data/validators.js +254 -9
- package/dist/modules/staff/data/validators.js.map +2 -2
- package/dist/modules/staff/di.js +42 -1
- package/dist/modules/staff/di.js.map +2 -2
- package/dist/modules/staff/events.js +84 -6
- package/dist/modules/staff/events.js.map +2 -2
- package/dist/modules/staff/events.payloads.js +158 -0
- package/dist/modules/staff/events.payloads.js.map +7 -0
- package/dist/modules/staff/extension-points.js +261 -0
- package/dist/modules/staff/extension-points.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.js +138 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.meta.js +12 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.meta.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.js +133 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.meta.js +19 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.meta.js.map +7 -0
- package/dist/modules/staff/lib/crud.js +11 -1
- package/dist/modules/staff/lib/crud.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/access.js +92 -0
- package/dist/modules/staff/lib/time-tracking/access.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/billability.js +39 -0
- package/dist/modules/staff/lib/time-tracking/billability.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/capacity.js +68 -0
- package/dist/modules/staff/lib/time-tracking/capacity.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/componentContracts.js +15 -0
- package/dist/modules/staff/lib/time-tracking/componentContracts.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/cost.js +82 -0
- package/dist/modules/staff/lib/time-tracking/cost.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/duration.js +55 -0
- package/dist/modules/staff/lib/time-tracking/duration.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/featureAccess.js +41 -0
- package/dist/modules/staff/lib/time-tracking/featureAccess.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/interval.js +72 -0
- package/dist/modules/staff/lib/time-tracking/interval.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/migrateProjectCodes.js +88 -0
- package/dist/modules/staff/lib/time-tracking/migrateProjectCodes.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/moneyVisibility.js +10 -0
- package/dist/modules/staff/lib/time-tracking/moneyVisibility.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/overlap.js +87 -0
- package/dist/modules/staff/lib/time-tracking/overlap.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/portalRecipients.js +27 -0
- package/dist/modules/staff/lib/time-tracking/portalRecipients.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/portalReports.js +16 -0
- package/dist/modules/staff/lib/time-tracking/portalReports.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/projectCode.js +135 -0
- package/dist/modules/staff/lib/time-tracking/projectCode.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/reapplyRounding.js +187 -0
- package/dist/modules/staff/lib/time-tracking/reapplyRounding.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/recalculationRunner.js +80 -0
- package/dist/modules/staff/lib/time-tracking/recalculationRunner.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/recalculations.js +56 -0
- package/dist/modules/staff/lib/time-tracking/recalculations.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/registries/invoke.js +63 -0
- package/dist/modules/staff/lib/time-tracking/registries/invoke.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/registries/registry.js +62 -0
- package/dist/modules/staff/lib/time-tracking/registries/registry.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/registries/scope.js +21 -0
- package/dist/modules/staff/lib/time-tracking/registries/scope.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/rollup.js +34 -0
- package/dist/modules/staff/lib/time-tracking/rollup.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/rounding.js +66 -0
- package/dist/modules/staff/lib/time-tracking/rounding.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/roundingImpact.js +39 -0
- package/dist/modules/staff/lib/time-tracking/roundingImpact.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/settingKeys.js +214 -0
- package/dist/modules/staff/lib/time-tracking/settingKeys.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/settings.js +87 -0
- package/dist/modules/staff/lib/time-tracking/settings.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/sqlInClause.js +9 -0
- package/dist/modules/staff/lib/time-tracking/sqlInClause.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/timeEntrySources.js +48 -0
- package/dist/modules/staff/lib/time-tracking/timeEntrySources.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js +283 -0
- package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/CustomerPicker.js +324 -0
- package/dist/modules/staff/lib/time-tracking-ui/CustomerPicker.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/DurationInput.js +121 -0
- package/dist/modules/staff/lib/time-tracking-ui/DurationInput.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/EntryDetailsFields.js +188 -0
- package/dist/modules/staff/lib/time-tracking-ui/EntryDetailsFields.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/EntryFilterBar.js +89 -0
- package/dist/modules/staff/lib/time-tracking-ui/EntryFilterBar.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js +855 -0
- package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js +360 -0
- package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/KanbanColumn.js +259 -0
- package/dist/modules/staff/lib/time-tracking-ui/KanbanColumn.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/NewTaskDialog.js +238 -0
- package/dist/modules/staff/lib/time-tracking-ui/NewTaskDialog.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js +130 -0
- package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/PeriodSelector.js +98 -0
- package/dist/modules/staff/lib/time-tracking-ui/PeriodSelector.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/PhasePlaceholder.js +27 -0
- package/dist/modules/staff/lib/time-tracking-ui/PhasePlaceholder.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/ProjectCodeField.js +166 -0
- package/dist/modules/staff/lib/time-tracking-ui/ProjectCodeField.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/ProjectCurrencyField.js +295 -0
- package/dist/modules/staff/lib/time-tracking-ui/ProjectCurrencyField.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js +212 -0
- package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/ProjectTeamDrawer.js +729 -0
- package/dist/modules/staff/lib/time-tracking-ui/ProjectTeamDrawer.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/ReportSheet.js +217 -0
- package/dist/modules/staff/lib/time-tracking-ui/ReportSheet.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/TagPicker.js +158 -0
- package/dist/modules/staff/lib/time-tracking-ui/TagPicker.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js +224 -0
- package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js +93 -0
- package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js +1196 -0
- package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/TaskListView.js +176 -0
- package/dist/modules/staff/lib/time-tracking-ui/TaskListView.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/TaskPicker.js +307 -0
- package/dist/modules/staff/lib/time-tracking-ui/TaskPicker.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/TaskQuickLog.js +284 -0
- package/dist/modules/staff/lib/time-tracking-ui/TaskQuickLog.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/TimeEntriesSummaryFooter.js +73 -0
- package/dist/modules/staff/lib/time-tracking-ui/TimeEntriesSummaryFooter.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/TimeEntryDialog.js +1499 -0
- package/dist/modules/staff/lib/time-tracking-ui/TimeEntryDialog.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/TimeEntryDurationCell.js +95 -0
- package/dist/modules/staff/lib/time-tracking-ui/TimeEntryDurationCell.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/TimesheetCalendar.js +188 -0
- package/dist/modules/staff/lib/time-tracking-ui/TimesheetCalendar.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/TimesheetPeriodFooter.js +61 -0
- package/dist/modules/staff/lib/time-tracking-ui/TimesheetPeriodFooter.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/TimesheetViewSwitch.js +45 -0
- package/dist/modules/staff/lib/time-tracking-ui/TimesheetViewSwitch.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js +176 -0
- package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js +82 -0
- package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/boardSummary.js +5 -0
- package/dist/modules/staff/lib/time-tracking-ui/boardSummary.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js +152 -0
- package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectTeamAssignment.js +92 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectTeamAssignment.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js +111 -0
- package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/timeEntryDialogState.js +243 -0
- package/dist/modules/staff/lib/time-tracking-ui/timeEntryDialogState.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/timeEntryListData.js +151 -0
- package/dist/modules/staff/lib/time-tracking-ui/timeEntryListData.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js +105 -0
- package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/timesheetData.js +132 -0
- package/dist/modules/staff/lib/time-tracking-ui/timesheetData.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/timesheetPeriod.js +205 -0
- package/dist/modules/staff/lib/time-tracking-ui/timesheetPeriod.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/timesheetTargets.js +53 -0
- package/dist/modules/staff/lib/time-tracking-ui/timesheetTargets.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/useBoardFilters.js +77 -0
- package/dist/modules/staff/lib/time-tracking-ui/useBoardFilters.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/useTimesheetPreferences.js +151 -0
- package/dist/modules/staff/lib/time-tracking-ui/useTimesheetPreferences.js.map +7 -0
- package/dist/modules/staff/lib/timeTrackingSeeds.js +809 -0
- package/dist/modules/staff/lib/timeTrackingSeeds.js.map +7 -0
- package/dist/modules/staff/lib/timesheets/timeEntryCacheInvalidation.js +2 -3
- package/dist/modules/staff/lib/timesheets/timeEntryCacheInvalidation.js.map +2 -2
- package/dist/modules/staff/lib/timesheets/timeEntryDecoration.js +152 -0
- package/dist/modules/staff/lib/timesheets/timeEntryDecoration.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-projects/budgetBurn.js +36 -0
- package/dist/modules/staff/lib/timesheets-projects/budgetBurn.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-projects/budgetThreshold.js +66 -0
- package/dist/modules/staff/lib/timesheets-projects/budgetThreshold.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-projects/budgetThresholdState.js +78 -0
- package/dist/modules/staff/lib/timesheets-projects/budgetThresholdState.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-projects/computeProjectFinancials.js +81 -0
- package/dist/modules/staff/lib/timesheets-projects/computeProjectFinancials.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-projects/reportSelection.js +44 -0
- package/dist/modules/staff/lib/timesheets-projects/reportSelection.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-projects-ui/AssignMembersDialog.js +169 -0
- package/dist/modules/staff/lib/timesheets-projects-ui/AssignMembersDialog.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-projects-ui/ProjectBudgetCell.js +22 -0
- package/dist/modules/staff/lib/timesheets-projects-ui/ProjectBudgetCell.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-projects-ui/ProjectCard.js +51 -1
- package/dist/modules/staff/lib/timesheets-projects-ui/ProjectCard.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-projects-ui/ProjectsBulkActionsBar.js +70 -0
- package/dist/modules/staff/lib/timesheets-projects-ui/ProjectsBulkActionsBar.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-reports/buildReportSheet.js +63 -0
- package/dist/modules/staff/lib/timesheets-reports/buildReportSheet.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-reports/loadReportData.js +170 -0
- package/dist/modules/staff/lib/timesheets-reports/loadReportData.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-reports/pdf.js +205 -0
- package/dist/modules/staff/lib/timesheets-reports/pdf.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-reports/reportApprovalPolicies.js +68 -0
- package/dist/modules/staff/lib/timesheets-reports/reportApprovalPolicies.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-reports/reportConfigData.js +135 -0
- package/dist/modules/staff/lib/timesheets-reports/reportConfigData.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-reports/reportConfigState.js +48 -0
- package/dist/modules/staff/lib/timesheets-reports/reportConfigState.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-reports/reportExport.js +262 -0
- package/dist/modules/staff/lib/timesheets-reports/reportExport.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-reports/reportExportFormats.js +34 -0
- package/dist/modules/staff/lib/timesheets-reports/reportExportFormats.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-reports/reportGroupings.js +79 -0
- package/dist/modules/staff/lib/timesheets-reports/reportGroupings.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-reports/reportLabels.js +11 -0
- package/dist/modules/staff/lib/timesheets-reports/reportLabels.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-reports/reportReference.js +83 -0
- package/dist/modules/staff/lib/timesheets-reports/reportReference.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-reports/reportRows.js +42 -0
- package/dist/modules/staff/lib/timesheets-reports/reportRows.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-reports/reportSheetData.js +107 -0
- package/dist/modules/staff/lib/timesheets-reports/reportSheetData.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-reports/reportTotals.js +289 -0
- package/dist/modules/staff/lib/timesheets-reports/reportTotals.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-reports/xlsx.js +145 -0
- package/dist/modules/staff/lib/timesheets-reports/xlsx.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-tasks/computeTaskRollups.js +116 -0
- package/dist/modules/staff/lib/timesheets-tasks/computeTaskRollups.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-tasks/defaultStatusTemplate.js +50 -0
- package/dist/modules/staff/lib/timesheets-tasks/defaultStatusTemplate.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-tasks/seedProjectStatuses.js +27 -0
- package/dist/modules/staff/lib/timesheets-tasks/seedProjectStatuses.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-tasks/statusPositions.js +65 -0
- package/dist/modules/staff/lib/timesheets-tasks/statusPositions.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-tasks/statusSlug.js +31 -0
- package/dist/modules/staff/lib/timesheets-tasks/statusSlug.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-tasks/taskHierarchy.js +31 -0
- package/dist/modules/staff/lib/timesheets-tasks/taskHierarchy.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-tasks/taskHoursTotals.js +17 -0
- package/dist/modules/staff/lib/timesheets-tasks/taskHoursTotals.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-tasks/taskReference.js +51 -0
- package/dist/modules/staff/lib/timesheets-tasks/taskReference.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-ui/CreateProjectDialog.js +11 -4
- package/dist/modules/staff/lib/timesheets-ui/CreateProjectDialog.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/ListView.js +369 -111
- package/dist/modules/staff/lib/timesheets-ui/ListView.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +47 -2
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
- package/dist/modules/staff/migrations/Migration20260813073131_staff.js +79 -0
- package/dist/modules/staff/migrations/Migration20260813073131_staff.js.map +7 -0
- package/dist/modules/staff/migrations/Migration20260824143357_staff.js +15 -0
- package/dist/modules/staff/migrations/Migration20260824143357_staff.js.map +7 -0
- package/dist/modules/staff/notifications.client.js +16 -0
- package/dist/modules/staff/notifications.client.js.map +7 -0
- package/dist/modules/staff/notifications.handlers.js +80 -0
- package/dist/modules/staff/notifications.handlers.js.map +7 -0
- package/dist/modules/staff/notifications.js +168 -0
- package/dist/modules/staff/notifications.js.map +2 -2
- package/dist/modules/staff/search.js +159 -1
- package/dist/modules/staff/search.js.map +2 -2
- package/dist/modules/staff/setup.js +20 -1
- package/dist/modules/staff/setup.js.map +2 -2
- package/dist/modules/staff/subscribers/time-project-access-requested-notification.js +68 -0
- package/dist/modules/staff/subscribers/time-project-access-requested-notification.js.map +7 -0
- package/dist/modules/staff/subscribers/time-project-budget-threshold-notification.js +149 -0
- package/dist/modules/staff/subscribers/time-project-budget-threshold-notification.js.map +7 -0
- package/dist/modules/staff/subscribers/time-report-approved-notification.js +78 -0
- package/dist/modules/staff/subscribers/time-report-approved-notification.js.map +7 -0
- package/dist/modules/staff/subscribers/time-report-portal-broadcast.js +78 -0
- package/dist/modules/staff/subscribers/time-report-portal-broadcast.js.map +7 -0
- package/dist/modules/staff/translations.js +4 -1
- package/dist/modules/staff/translations.js.map +2 -2
- package/dist/modules/staff/widgets/components.js +7 -0
- package/dist/modules/staff/widgets/components.js.map +7 -0
- package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js +5 -4
- package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js.map +2 -2
- package/dist/modules/staff/widgets/notifications/TimeProjectAccessRequestedRenderer.js +135 -0
- package/dist/modules/staff/widgets/notifications/TimeProjectAccessRequestedRenderer.js.map +7 -0
- package/dist/modules/staff/widgets/notifications/index.js +5 -0
- package/dist/modules/staff/widgets/notifications/index.js.map +7 -0
- package/dist/modules/staff/workers/timesheets-reapply-rounding.js +43 -0
- package/dist/modules/staff/workers/timesheets-reapply-rounding.js.map +7 -0
- package/generated/entities/staff_time_entry/index.ts +7 -0
- package/generated/entities/staff_time_entry_tag/index.ts +6 -0
- package/generated/entities/staff_time_project/index.ts +8 -0
- package/generated/entities/staff_time_report/index.ts +27 -0
- package/generated/entities/staff_time_report_entry/index.ts +12 -0
- package/generated/entities/staff_time_report_event/index.ts +9 -0
- package/generated/entities/staff_time_report_project/index.ts +6 -0
- package/generated/entities/staff_time_tag/index.ts +9 -0
- package/generated/entities/staff_time_task/index.ts +17 -0
- package/generated/entities/staff_time_task_comment/index.ts +9 -0
- package/generated/entities/staff_time_task_status/index.ts +13 -0
- package/generated/entities/staff_time_task_tag/index.ts +6 -0
- package/generated/entities.ids.generated.ts +11 -1
- package/generated/entity-fields-registry.ts +149 -0
- package/package.json +7 -7
- package/src/helpers/integration/timesheetFixtures.ts +31 -1
- package/src/modules/auth/README.md +1 -1
- package/src/modules/auth/cli.ts +35 -1
- package/src/modules/auth/i18n/de.json +7 -0
- package/src/modules/auth/i18n/en.json +7 -0
- package/src/modules/auth/i18n/es.json +7 -0
- package/src/modules/auth/i18n/ko.json +7 -0
- package/src/modules/auth/i18n/pl.json +7 -0
- package/src/modules/auth/lib/backendChrome.tsx +1 -0
- package/src/modules/customer_accounts/lib/customerRoleAcls.ts +79 -0
- package/src/modules/customer_accounts/setup.ts +1 -45
- package/src/modules/data_sync/AGENTS.md +34 -0
- package/src/modules/data_sync/api/options.ts +2 -0
- package/src/modules/data_sync/backend/data-sync/page.tsx +82 -33
- package/src/modules/data_sync/i18n/de.json +1 -0
- package/src/modules/data_sync/i18n/en.json +1 -0
- package/src/modules/data_sync/i18n/es.json +1 -0
- package/src/modules/data_sync/i18n/ko.json +1 -0
- package/src/modules/data_sync/i18n/pl.json +1 -0
- package/src/modules/data_sync/lib/adapter.ts +45 -0
- package/src/modules/data_sync/lib/start-controls.ts +89 -0
- package/src/modules/staff/AGENTS.md +769 -1
- package/src/modules/staff/acl.ts +9 -0
- package/src/modules/staff/ai-agents.ts +189 -0
- package/src/modules/staff/ai-tools/time-tracking-pack.ts +494 -0
- package/src/modules/staff/ai-tools/types.ts +82 -0
- package/src/modules/staff/ai-tools.ts +13 -0
- package/src/modules/staff/analytics.ts +113 -0
- package/src/modules/staff/api/guards.ts +85 -3
- package/src/modules/staff/api/leave-requests/accept/route.ts +0 -2
- package/src/modules/staff/api/leave-requests/reject/route.ts +0 -2
- package/src/modules/staff/api/portal/time-reports/[id]/route.ts +182 -0
- package/src/modules/staff/api/portal/time-reports/route.ts +270 -0
- package/src/modules/staff/api/team-members/self/route.ts +0 -2
- package/src/modules/staff/api/team-members/tags/assign/route.ts +0 -2
- package/src/modules/staff/api/team-members/tags/unassign/route.ts +0 -2
- package/src/modules/staff/api/timesheets/_shared/withTimesheetInterceptors.ts +292 -0
- package/src/modules/staff/api/timesheets/access-requests/route.ts +299 -0
- package/src/modules/staff/api/timesheets/my-projects/[projectId]/route.ts +15 -7
- package/src/modules/staff/api/timesheets/my-projects/route.ts +14 -1
- package/src/modules/staff/api/timesheets/my-work/myWorkAggregate.ts +90 -0
- package/src/modules/staff/api/timesheets/my-work/route.ts +475 -0
- package/src/modules/staff/api/timesheets/projects/kpis/route.ts +15 -2
- package/src/modules/staff/api/timesheets/reports/[id]/close/route.ts +203 -0
- package/src/modules/staff/api/timesheets/reports/[id]/export/route.ts +315 -0
- package/src/modules/staff/api/timesheets/reports/[id]/sheet/route.ts +220 -0
- package/src/modules/staff/api/timesheets/reports/[id]/unlock/route.ts +208 -0
- package/src/modules/staff/api/timesheets/reports/preview/route.ts +449 -0
- package/src/modules/staff/api/timesheets/reports/route.ts +386 -0
- package/src/modules/staff/api/timesheets/reports/shared.ts +121 -0
- package/src/modules/staff/api/timesheets/settings/reapply-rounding/route.ts +205 -0
- package/src/modules/staff/api/timesheets/settings/rounding-impact/route.ts +226 -0
- package/src/modules/staff/api/timesheets/settings/route.ts +242 -0
- package/src/modules/staff/api/timesheets/tags/access.ts +134 -0
- package/src/modules/staff/api/timesheets/tags/entry-assignments/route.ts +290 -0
- package/src/modules/staff/api/timesheets/tags/route.ts +177 -0
- package/src/modules/staff/api/timesheets/tags/task-assignments/route.ts +294 -0
- package/src/modules/staff/api/timesheets/task-statuses/route.ts +232 -0
- package/src/modules/staff/api/timesheets/tasks/[id]/comments/route.ts +532 -0
- package/src/modules/staff/api/timesheets/tasks/[id]/status/route.ts +271 -0
- package/src/modules/staff/api/timesheets/tasks/route.ts +443 -0
- package/src/modules/staff/api/timesheets/time-entries/[id]/duplicate/route.ts +306 -0
- package/src/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.ts +32 -10
- package/src/modules/staff/api/timesheets/time-entries/[id]/segments/route.ts +32 -17
- package/src/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.ts +27 -6
- package/src/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.ts +36 -6
- package/src/modules/staff/api/timesheets/time-entries/bulk/route.ts +331 -34
- package/src/modules/staff/api/timesheets/time-entries/copy-day/route.ts +533 -0
- package/src/modules/staff/api/timesheets/time-entries/overlaps/route.ts +457 -0
- package/src/modules/staff/api/timesheets/time-entries/route.ts +378 -24
- package/src/modules/staff/api/timesheets/time-entries/start-timer/route.ts +31 -9
- package/src/modules/staff/api/timesheets/time-projects/[id]/change-currency/route.ts +246 -0
- package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +62 -2
- package/src/modules/staff/api/timesheets/time-projects/route.ts +397 -22
- package/src/modules/staff/backend/staff/time-tracking/board/page.meta.ts +14 -0
- package/src/modules/staff/backend/staff/time-tracking/board/page.tsx +151 -0
- package/src/modules/staff/backend/staff/time-tracking/entries/page.meta.ts +14 -0
- package/src/modules/staff/backend/staff/time-tracking/entries/page.tsx +1229 -0
- package/src/modules/staff/backend/staff/time-tracking/page.meta.ts +11 -0
- package/src/modules/staff/backend/staff/time-tracking/page.tsx +823 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/board/page.meta.ts +12 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/board/page.tsx +81 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.meta.ts +12 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +275 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.meta.ts +12 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +1420 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/create/page.meta.ts +12 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/create/page.tsx +109 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/page.meta.ts +15 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/page.tsx +1254 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.ts +466 -0
- package/src/modules/staff/backend/staff/time-tracking/reports/[id]/page.meta.ts +12 -0
- package/src/modules/staff/backend/staff/time-tracking/reports/[id]/page.tsx +632 -0
- package/src/modules/staff/backend/staff/time-tracking/reports/create/page.meta.ts +12 -0
- package/src/modules/staff/backend/staff/time-tracking/reports/create/page.tsx +867 -0
- package/src/modules/staff/backend/staff/time-tracking/reports/page.meta.ts +14 -0
- package/src/modules/staff/backend/staff/time-tracking/reports/page.tsx +246 -0
- package/src/modules/staff/backend/staff/time-tracking/settings/page.meta.ts +14 -0
- package/src/modules/staff/backend/staff/time-tracking/settings/page.tsx +701 -0
- package/src/modules/staff/backend/staff/time-tracking/timesheet/GridView.tsx +953 -0
- package/src/modules/staff/backend/staff/time-tracking/timesheet/page.meta.ts +14 -0
- package/src/modules/staff/backend/staff/time-tracking/timesheet/page.tsx +800 -0
- package/src/modules/staff/backend/staff/timesheets/page.meta.ts +6 -6
- package/src/modules/staff/backend/staff/timesheets/page.tsx +12 -1010
- package/src/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.ts +0 -6
- package/src/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.tsx +4 -158
- package/src/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.ts +0 -6
- package/src/modules/staff/backend/staff/timesheets/projects/[id]/page.tsx +4 -609
- package/src/modules/staff/backend/staff/timesheets/projects/create/page.meta.ts +0 -6
- package/src/modules/staff/backend/staff/timesheets/projects/create/page.tsx +3 -89
- package/src/modules/staff/backend/staff/timesheets/projects/page.meta.ts +2 -19
- package/src/modules/staff/backend/staff/timesheets/projects/page.tsx +3 -778
- package/src/modules/staff/ce.ts +63 -0
- package/src/modules/staff/cli.ts +207 -1
- package/src/modules/staff/commands/index.ts +5 -0
- package/src/modules/staff/commands/timesheets-entries.ts +1006 -28
- package/src/modules/staff/commands/timesheets-projects.ts +593 -7
- package/src/modules/staff/commands/timesheets-reports.ts +1329 -0
- package/src/modules/staff/commands/timesheets-tags.ts +1315 -0
- package/src/modules/staff/commands/timesheets-task-comments.ts +583 -0
- package/src/modules/staff/commands/timesheets-task-statuses.ts +889 -0
- package/src/modules/staff/commands/timesheets-tasks.ts +1121 -0
- package/src/modules/staff/data/enrichers.ts +675 -10
- package/src/modules/staff/data/entities.ts +494 -2
- package/src/modules/staff/data/extensions.ts +68 -0
- package/src/modules/staff/data/validators.ts +348 -3
- package/src/modules/staff/di.ts +96 -0
- package/src/modules/staff/events.payloads.ts +268 -0
- package/src/modules/staff/events.ts +87 -5
- package/src/modules/staff/extension-points.ts +308 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.meta.ts +10 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.tsx +225 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/page.meta.ts +17 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/page.tsx +182 -0
- package/src/modules/staff/i18n/de.json +899 -0
- package/src/modules/staff/i18n/en.json +899 -0
- package/src/modules/staff/i18n/es.json +899 -0
- package/src/modules/staff/i18n/ko.json +899 -0
- package/src/modules/staff/i18n/pl.json +899 -0
- package/src/modules/staff/lib/crud.ts +14 -3
- package/src/modules/staff/lib/time-tracking/access.ts +155 -0
- package/src/modules/staff/lib/time-tracking/billability.ts +68 -0
- package/src/modules/staff/lib/time-tracking/capacity.ts +124 -0
- package/src/modules/staff/lib/time-tracking/componentContracts.ts +26 -0
- package/src/modules/staff/lib/time-tracking/cost.ts +144 -0
- package/src/modules/staff/lib/time-tracking/duration.ts +68 -0
- package/src/modules/staff/lib/time-tracking/featureAccess.ts +115 -0
- package/src/modules/staff/lib/time-tracking/interval.ts +90 -0
- package/src/modules/staff/lib/time-tracking/migrateProjectCodes.ts +161 -0
- package/src/modules/staff/lib/time-tracking/moneyVisibility.ts +37 -0
- package/src/modules/staff/lib/time-tracking/overlap.ts +159 -0
- package/src/modules/staff/lib/time-tracking/portalRecipients.ts +76 -0
- package/src/modules/staff/lib/time-tracking/portalReports.ts +31 -0
- package/src/modules/staff/lib/time-tracking/projectCode.ts +221 -0
- package/src/modules/staff/lib/time-tracking/reapplyRounding.ts +313 -0
- package/src/modules/staff/lib/time-tracking/recalculationRunner.ts +118 -0
- package/src/modules/staff/lib/time-tracking/recalculations.ts +135 -0
- package/src/modules/staff/lib/time-tracking/registries/invoke.ts +122 -0
- package/src/modules/staff/lib/time-tracking/registries/registry.ts +124 -0
- package/src/modules/staff/lib/time-tracking/registries/scope.ts +56 -0
- package/src/modules/staff/lib/time-tracking/rollup.ts +59 -0
- package/src/modules/staff/lib/time-tracking/rounding.ts +126 -0
- package/src/modules/staff/lib/time-tracking/roundingImpact.ts +75 -0
- package/src/modules/staff/lib/time-tracking/settingKeys.ts +274 -0
- package/src/modules/staff/lib/time-tracking/settings.ts +168 -0
- package/src/modules/staff/lib/time-tracking/sqlInClause.ts +22 -0
- package/src/modules/staff/lib/time-tracking/timeEntrySources.ts +85 -0
- package/src/modules/staff/lib/time-tracking-ui/BoardFilterChips.tsx +360 -0
- package/src/modules/staff/lib/time-tracking-ui/CustomerPicker.tsx +427 -0
- package/src/modules/staff/lib/time-tracking-ui/DurationInput.tsx +192 -0
- package/src/modules/staff/lib/time-tracking-ui/EntryDetailsFields.tsx +224 -0
- package/src/modules/staff/lib/time-tracking-ui/EntryFilterBar.tsx +144 -0
- package/src/modules/staff/lib/time-tracking-ui/KanbanBoard.tsx +1065 -0
- package/src/modules/staff/lib/time-tracking-ui/KanbanCard.tsx +435 -0
- package/src/modules/staff/lib/time-tracking-ui/KanbanColumn.tsx +316 -0
- package/src/modules/staff/lib/time-tracking-ui/NewTaskDialog.tsx +291 -0
- package/src/modules/staff/lib/time-tracking-ui/NoProjectAccess.tsx +162 -0
- package/src/modules/staff/lib/time-tracking-ui/PeriodSelector.tsx +149 -0
- package/src/modules/staff/lib/time-tracking-ui/PhasePlaceholder.tsx +31 -0
- package/src/modules/staff/lib/time-tracking-ui/ProjectCodeField.tsx +230 -0
- package/src/modules/staff/lib/time-tracking-ui/ProjectCurrencyField.tsx +432 -0
- package/src/modules/staff/lib/time-tracking-ui/ProjectFormSections.tsx +323 -0
- package/src/modules/staff/lib/time-tracking-ui/ProjectTeamDrawer.tsx +930 -0
- package/src/modules/staff/lib/time-tracking-ui/ReportSheet.tsx +357 -0
- package/src/modules/staff/lib/time-tracking-ui/TagPicker.tsx +195 -0
- package/src/modules/staff/lib/time-tracking-ui/TaskBoardScreen.tsx +243 -0
- package/src/modules/staff/lib/time-tracking-ui/TaskCommentThread.tsx +137 -0
- package/src/modules/staff/lib/time-tracking-ui/TaskDrawer.tsx +1405 -0
- package/src/modules/staff/lib/time-tracking-ui/TaskListView.tsx +260 -0
- package/src/modules/staff/lib/time-tracking-ui/TaskPicker.tsx +435 -0
- package/src/modules/staff/lib/time-tracking-ui/TaskQuickLog.tsx +296 -0
- package/src/modules/staff/lib/time-tracking-ui/TimeEntriesSummaryFooter.tsx +97 -0
- package/src/modules/staff/lib/time-tracking-ui/TimeEntryDialog.tsx +1929 -0
- package/src/modules/staff/lib/time-tracking-ui/TimeEntryDurationCell.tsx +126 -0
- package/src/modules/staff/lib/time-tracking-ui/TimesheetCalendar.tsx +265 -0
- package/src/modules/staff/lib/time-tracking-ui/TimesheetPeriodFooter.tsx +78 -0
- package/src/modules/staff/lib/time-tracking-ui/TimesheetViewSwitch.tsx +70 -0
- package/src/modules/staff/lib/time-tracking-ui/boardDirectory.ts +231 -0
- package/src/modules/staff/lib/time-tracking-ui/boardFilters.ts +128 -0
- package/src/modules/staff/lib/time-tracking-ui/boardSummary.ts +20 -0
- package/src/modules/staff/lib/time-tracking-ui/kanbanBoardData.ts +228 -0
- package/src/modules/staff/lib/time-tracking-ui/projectTeamAssignment.ts +143 -0
- package/src/modules/staff/lib/time-tracking-ui/taskDrawerData.ts +194 -0
- package/src/modules/staff/lib/time-tracking-ui/timeEntryDialogState.ts +400 -0
- package/src/modules/staff/lib/time-tracking-ui/timeEntryListData.ts +271 -0
- package/src/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.ts +168 -0
- package/src/modules/staff/lib/time-tracking-ui/timesheetData.ts +225 -0
- package/src/modules/staff/lib/time-tracking-ui/timesheetPeriod.ts +278 -0
- package/src/modules/staff/lib/time-tracking-ui/timesheetTargets.ts +93 -0
- package/src/modules/staff/lib/time-tracking-ui/useBoardFilters.ts +107 -0
- package/src/modules/staff/lib/time-tracking-ui/useTimesheetPreferences.ts +245 -0
- package/src/modules/staff/lib/timeTrackingSeeds.ts +1037 -0
- package/src/modules/staff/lib/timesheets/timeEntryCacheInvalidation.ts +11 -10
- package/src/modules/staff/lib/timesheets/timeEntryDecoration.ts +256 -0
- package/src/modules/staff/lib/timesheets-projects/budgetBurn.ts +60 -0
- package/src/modules/staff/lib/timesheets-projects/budgetThreshold.ts +135 -0
- package/src/modules/staff/lib/timesheets-projects/budgetThresholdState.ts +137 -0
- package/src/modules/staff/lib/timesheets-projects/computeProjectFinancials.ts +142 -0
- package/src/modules/staff/lib/timesheets-projects/reportSelection.ts +82 -0
- package/src/modules/staff/lib/timesheets-projects-ui/AssignMembersDialog.tsx +237 -0
- package/src/modules/staff/lib/timesheets-projects-ui/ProjectBudgetCell.tsx +33 -0
- package/src/modules/staff/lib/timesheets-projects-ui/ProjectCard.tsx +56 -1
- package/src/modules/staff/lib/timesheets-projects-ui/ProjectsBulkActionsBar.tsx +90 -0
- package/src/modules/staff/lib/timesheets-reports/buildReportSheet.ts +125 -0
- package/src/modules/staff/lib/timesheets-reports/loadReportData.ts +251 -0
- package/src/modules/staff/lib/timesheets-reports/pdf.ts +263 -0
- package/src/modules/staff/lib/timesheets-reports/reportApprovalPolicies.ts +137 -0
- package/src/modules/staff/lib/timesheets-reports/reportConfigData.ts +247 -0
- package/src/modules/staff/lib/timesheets-reports/reportConfigState.ts +89 -0
- package/src/modules/staff/lib/timesheets-reports/reportExport.ts +373 -0
- package/src/modules/staff/lib/timesheets-reports/reportExportFormats.ts +71 -0
- package/src/modules/staff/lib/timesheets-reports/reportGroupings.ts +153 -0
- package/src/modules/staff/lib/timesheets-reports/reportLabels.ts +23 -0
- package/src/modules/staff/lib/timesheets-reports/reportReference.ts +146 -0
- package/src/modules/staff/lib/timesheets-reports/reportRows.ts +89 -0
- package/src/modules/staff/lib/timesheets-reports/reportSheetData.ts +196 -0
- package/src/modules/staff/lib/timesheets-reports/reportTotals.ts +544 -0
- package/src/modules/staff/lib/timesheets-reports/xlsx.ts +207 -0
- package/src/modules/staff/lib/timesheets-tasks/computeTaskRollups.ts +238 -0
- package/src/modules/staff/lib/timesheets-tasks/defaultStatusTemplate.ts +90 -0
- package/src/modules/staff/lib/timesheets-tasks/seedProjectStatuses.ts +43 -0
- package/src/modules/staff/lib/timesheets-tasks/statusPositions.ts +132 -0
- package/src/modules/staff/lib/timesheets-tasks/statusSlug.ts +43 -0
- package/src/modules/staff/lib/timesheets-tasks/taskHierarchy.ts +99 -0
- package/src/modules/staff/lib/timesheets-tasks/taskHoursTotals.ts +42 -0
- package/src/modules/staff/lib/timesheets-tasks/taskReference.ts +108 -0
- package/src/modules/staff/lib/timesheets-ui/CreateProjectDialog.tsx +18 -4
- package/src/modules/staff/lib/timesheets-ui/ListView.tsx +504 -149
- package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +50 -2
- package/src/modules/staff/migrations/.snapshot-open-mercato.json +3244 -228
- package/src/modules/staff/migrations/Migration20260813073131_staff.ts +94 -0
- package/src/modules/staff/migrations/Migration20260824143357_staff.ts +17 -0
- package/src/modules/staff/notifications.client.ts +16 -0
- package/src/modules/staff/notifications.handlers.ts +88 -0
- package/src/modules/staff/notifications.ts +163 -0
- package/src/modules/staff/search.ts +191 -1
- package/src/modules/staff/setup.ts +22 -0
- package/src/modules/staff/subscribers/time-project-access-requested-notification.ts +95 -0
- package/src/modules/staff/subscribers/time-project-budget-threshold-notification.ts +226 -0
- package/src/modules/staff/subscribers/time-report-approved-notification.ts +116 -0
- package/src/modules/staff/subscribers/time-report-portal-broadcast.ts +120 -0
- package/src/modules/staff/translations.ts +17 -0
- package/src/modules/staff/widgets/components.ts +34 -0
- package/src/modules/staff/widgets/injection/timer-sidebar-indicator/widget.tsx +6 -5
- package/src/modules/staff/widgets/notifications/TimeProjectAccessRequestedRenderer.tsx +138 -0
- package/src/modules/staff/widgets/notifications/index.ts +1 -0
- package/src/modules/staff/workers/timesheets-reapply-rounding.ts +59 -0
- package/dist/modules/staff/backend/staff/timesheets/projects/projectFormConfig.js +0 -123
- package/dist/modules/staff/backend/staff/timesheets/projects/projectFormConfig.js.map +0 -7
- package/src/modules/staff/backend/staff/timesheets/projects/projectFormConfig.ts +0 -138
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { CommandBus, CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
2
3
|
import { registerCommand } from '@open-mercato/shared/lib/commands'
|
|
3
4
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
5
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
@@ -7,8 +8,28 @@ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
|
7
8
|
import { buildChanges, emitCrudSideEffects, emitCrudUndoSideEffects } from '@open-mercato/shared/lib/commands/helpers'
|
|
8
9
|
import { makeCreateRedo } from '@open-mercato/shared/lib/commands/redo'
|
|
9
10
|
import type { CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'
|
|
10
|
-
import {
|
|
11
|
+
import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'
|
|
12
|
+
import {
|
|
13
|
+
StaffTimeEntry,
|
|
14
|
+
StaffTimeEntrySegment,
|
|
15
|
+
StaffTimeProject,
|
|
16
|
+
StaffTimeTask,
|
|
17
|
+
type StaffTimeEntrySource,
|
|
18
|
+
} from '../data/entities'
|
|
11
19
|
import { emitStaffEvent } from '../events'
|
|
20
|
+
import { deriveInterval, formatClock, parseClock } from '../lib/time-tracking/interval'
|
|
21
|
+
import { roundMinutes } from '../lib/time-tracking/rounding'
|
|
22
|
+
import { resolveBillability } from '../lib/time-tracking/billability'
|
|
23
|
+
import type { ScopedResolverContext } from '../lib/time-tracking/registries/scope'
|
|
24
|
+
import {
|
|
25
|
+
DEFAULT_TIME_TRACKING_SETTINGS,
|
|
26
|
+
readTimeTrackingSettings,
|
|
27
|
+
type TimeTrackingSettings,
|
|
28
|
+
} from '../lib/time-tracking/settings'
|
|
29
|
+
import { resolveProjectAccess, type ProjectAccess } from '../lib/time-tracking/access'
|
|
30
|
+
// Imported for the command ids AND for the registration side effect: the tag
|
|
31
|
+
// commands must be in the registry before this file asks the bus to run one.
|
|
32
|
+
import { staffTimeTagCommandIds, TAG_TARGET_LOCKED_CODE } from './timesheets-tags'
|
|
12
33
|
|
|
13
34
|
const timeEntryCrudIndexer: CrudIndexerConfig<StaffTimeEntry> = {
|
|
14
35
|
entityType: 'staff:staff_time_entry',
|
|
@@ -41,12 +62,20 @@ import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
|
41
62
|
|
|
42
63
|
const logger = createLogger('staff')
|
|
43
64
|
|
|
65
|
+
type ContainerLike = { resolve: (token: string) => unknown }
|
|
66
|
+
|
|
67
|
+
type Translate = (key: string, fallback?: string) => string
|
|
68
|
+
|
|
44
69
|
type RbacServiceLike = {
|
|
45
70
|
userHasAllFeatures: (
|
|
46
71
|
userId: string,
|
|
47
72
|
required: string[],
|
|
48
73
|
scope: { tenantId: string | null; organizationId: string | null },
|
|
49
74
|
) => Promise<boolean>
|
|
75
|
+
getGrantedFeatures?: (
|
|
76
|
+
userId: string,
|
|
77
|
+
scope: { tenantId: string | null; organizationId: string | null },
|
|
78
|
+
) => Promise<string[]>
|
|
50
79
|
}
|
|
51
80
|
|
|
52
81
|
/**
|
|
@@ -91,22 +120,25 @@ async function resolveCallerStaffMemberId(
|
|
|
91
120
|
|
|
92
121
|
/**
|
|
93
122
|
* Verifies the referenced time project exists and is in-scope (same tenant + org,
|
|
94
|
-
* not soft-deleted)
|
|
95
|
-
*
|
|
123
|
+
* not soft-deleted) and hands the row back — the write path reads its
|
|
124
|
+
* `billableByDefault` and `currencyCode` to seed the entry. Throws 422 if the ID
|
|
125
|
+
* is provided but unresolvable. Returns null when projectId is null/undefined
|
|
126
|
+
* (timeProjectId is optional on entries).
|
|
96
127
|
*/
|
|
97
|
-
async function
|
|
128
|
+
async function requireTimeProjectInScope(
|
|
98
129
|
em: EntityManager,
|
|
99
130
|
projectId: string | null | undefined,
|
|
100
131
|
tenantId: string,
|
|
101
132
|
organizationId: string,
|
|
102
|
-
): Promise<
|
|
103
|
-
if (!projectId) return
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
133
|
+
): Promise<StaffTimeProject | null> {
|
|
134
|
+
if (!projectId) return null
|
|
135
|
+
const project = await em.findOne(StaffTimeProject, {
|
|
136
|
+
id: projectId,
|
|
137
|
+
tenantId,
|
|
138
|
+
organizationId,
|
|
139
|
+
deletedAt: null,
|
|
140
|
+
})
|
|
141
|
+
if (!project) {
|
|
110
142
|
const { translate } = await resolveTranslations()
|
|
111
143
|
throw new CrudHttpError(422, {
|
|
112
144
|
error: translate('staff.timesheets.errors.projectNotFound', 'Time project not found or not accessible.'),
|
|
@@ -115,6 +147,543 @@ async function assertTimeProjectInScope(
|
|
|
115
147
|
},
|
|
116
148
|
})
|
|
117
149
|
}
|
|
150
|
+
return project
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Same contract as the project check, for `task_id`. A dangling task reference is
|
|
155
|
+
* refused rather than stored: the task rollups on screens 6 and 7 aggregate entries
|
|
156
|
+
* by `task_id`, so a stale or foreign UUID would silently move hours onto another
|
|
157
|
+
* project's card.
|
|
158
|
+
*/
|
|
159
|
+
async function requireTaskInScope(
|
|
160
|
+
em: EntityManager,
|
|
161
|
+
taskId: string | null | undefined,
|
|
162
|
+
tenantId: string,
|
|
163
|
+
organizationId: string,
|
|
164
|
+
): Promise<StaffTimeTask | null> {
|
|
165
|
+
if (!taskId) return null
|
|
166
|
+
const task = await em.findOne(StaffTimeTask, {
|
|
167
|
+
id: taskId,
|
|
168
|
+
tenantId,
|
|
169
|
+
organizationId,
|
|
170
|
+
deletedAt: null,
|
|
171
|
+
})
|
|
172
|
+
if (!task) {
|
|
173
|
+
const { translate } = await resolveTranslations()
|
|
174
|
+
const message = translate('staff.timesheets.errors.taskNotFound', 'Task not found or not accessible.')
|
|
175
|
+
throw new CrudHttpError(422, { error: message, fieldErrors: { taskId: message } })
|
|
176
|
+
}
|
|
177
|
+
return task
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* A task belongs to exactly one project, so an entry naming both must name the
|
|
182
|
+
* same one. Letting them disagree would file the hours under a project whose
|
|
183
|
+
* board never shows the task — the same rollup corruption the task-scope check
|
|
184
|
+
* above prevents, one level up.
|
|
185
|
+
*/
|
|
186
|
+
async function assertTaskBelongsToProject(
|
|
187
|
+
task: StaffTimeTask | null,
|
|
188
|
+
projectId: string | null,
|
|
189
|
+
): Promise<void> {
|
|
190
|
+
if (timeEntryTaskMatchesProject(task, projectId)) return
|
|
191
|
+
const { translate } = await resolveTranslations()
|
|
192
|
+
const message = translate('staff.timesheets.errors.taskNotFound', 'Task not found or not accessible.')
|
|
193
|
+
throw new CrudHttpError(422, { error: message, fieldErrors: { taskId: message } })
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The rule the assertion above enforces, as a predicate: a task belongs to
|
|
198
|
+
* exactly one project, so an entry naming both must name the same one. Exported
|
|
199
|
+
* because the grid bulk save validates a whole batch at once and answers with a
|
|
200
|
+
* per-row error list rather than a thrown 422 — it reuses the rule and only
|
|
201
|
+
* differs in how it reports a violation.
|
|
202
|
+
*/
|
|
203
|
+
export function timeEntryTaskMatchesProject(
|
|
204
|
+
task: { timeProjectId?: string | null } | null | undefined,
|
|
205
|
+
projectId: string | null | undefined,
|
|
206
|
+
): boolean {
|
|
207
|
+
if (!task || !projectId) return true
|
|
208
|
+
return task.timeProjectId === projectId
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Screen 8 picks a task and lets the project follow from it ("Projekt i klient
|
|
213
|
+
* wynikają z zadania"), so a write that names only a task still lands on the
|
|
214
|
+
* project the task belongs to — and with it the billable and currency defaults.
|
|
215
|
+
* One derivation for every write path, including the grid bulk save.
|
|
216
|
+
*/
|
|
217
|
+
export function resolveTimeEntryProjectId(
|
|
218
|
+
requestedProjectId: string | null | undefined,
|
|
219
|
+
task: { timeProjectId?: string | null } | null | undefined,
|
|
220
|
+
): string | null {
|
|
221
|
+
return requestedProjectId ?? task?.timeProjectId ?? null
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The billable default chain, in one place: an explicit value wins, then the
|
|
226
|
+
* project's own default — a fixed-price engagement is non-billable however the
|
|
227
|
+
* tenant normally works — and the tenant setting only decides what is left.
|
|
228
|
+
*
|
|
229
|
+
* EP-34 moved that chain into the billability registry's built-in resolver; this
|
|
230
|
+
* stays the module's entry point and forwards the scope so a contributed
|
|
231
|
+
* resolver can be consulted ahead of it.
|
|
232
|
+
*/
|
|
233
|
+
export function resolveTimeEntryBillable(params: {
|
|
234
|
+
requested?: boolean | null
|
|
235
|
+
project?: { id?: string | null; billableByDefault?: boolean | null } | null
|
|
236
|
+
settings: TimeTrackingSettings
|
|
237
|
+
scope?: ScopedResolverContext | null
|
|
238
|
+
}): boolean {
|
|
239
|
+
return resolveBillability({
|
|
240
|
+
...(params.scope ?? {}),
|
|
241
|
+
requested: params.requested,
|
|
242
|
+
project: params.project ?? null,
|
|
243
|
+
settings: params.settings,
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Rounding and the billable default are tenant settings. A settings read that
|
|
249
|
+
* fails must not fail the write, so it degrades to the documented defaults —
|
|
250
|
+
* the same fail-safe the projects and tasks routes apply to the grace window.
|
|
251
|
+
*/
|
|
252
|
+
async function readEntrySettingsFrom(
|
|
253
|
+
container: ContainerLike,
|
|
254
|
+
tenantId: string,
|
|
255
|
+
): Promise<TimeTrackingSettings> {
|
|
256
|
+
try {
|
|
257
|
+
const configService = container.resolve('moduleConfigService') as ModuleConfigService | undefined
|
|
258
|
+
if (!configService?.getRecord) return DEFAULT_TIME_TRACKING_SETTINGS
|
|
259
|
+
return await readTimeTrackingSettings(configService, { tenantId })
|
|
260
|
+
} catch {
|
|
261
|
+
return DEFAULT_TIME_TRACKING_SETTINGS
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function readEntrySettings(ctx: CommandRuntimeContext, tenantId: string): Promise<TimeTrackingSettings> {
|
|
266
|
+
return readEntrySettingsFrom(ctx.container, tenantId)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The same tenant-settings read, for write paths that run outside a command —
|
|
271
|
+
* the grid bulk save reads it ONCE per request (it is identical for every row)
|
|
272
|
+
* and then seeds rounding and the billable default from that one snapshot.
|
|
273
|
+
*/
|
|
274
|
+
export async function resolveTimeEntrySettings(
|
|
275
|
+
container: ContainerLike,
|
|
276
|
+
tenantId: string,
|
|
277
|
+
): Promise<TimeTrackingSettings> {
|
|
278
|
+
return readEntrySettingsFrom(container, tenantId)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The single rounding entry point for write paths that cannot run inside this
|
|
283
|
+
* command file — the grid bulk save and timer-stop both compute a duration in
|
|
284
|
+
* their own transaction, and D-7 makes `rounded_minutes` the ONLY input to cost,
|
|
285
|
+
* so a duration written without its rounded twin bills nothing. They call this
|
|
286
|
+
* rather than reaching for `roundMinutes` themselves, which keeps the tenant
|
|
287
|
+
* settings read, its fail-safe, and the rule itself in one place.
|
|
288
|
+
*/
|
|
289
|
+
export async function resolveRoundedMinutes(
|
|
290
|
+
container: ContainerLike,
|
|
291
|
+
tenantId: string,
|
|
292
|
+
durationMinutes: number,
|
|
293
|
+
organizationId?: string | null,
|
|
294
|
+
): Promise<number> {
|
|
295
|
+
return roundedMinutesFor(durationMinutes, await readEntrySettingsFrom(container, tenantId), {
|
|
296
|
+
tenantId,
|
|
297
|
+
organizationId: organizationId ?? undefined,
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function resolveGrantedFeatures(
|
|
302
|
+
ctx: CommandRuntimeContext,
|
|
303
|
+
tenantId: string,
|
|
304
|
+
organizationId: string,
|
|
305
|
+
): Promise<string[]> {
|
|
306
|
+
const userId = ctx.auth?.sub
|
|
307
|
+
if (!userId) return []
|
|
308
|
+
try {
|
|
309
|
+
const rbac = ctx.container.resolve('rbacService') as RbacServiceLike | undefined
|
|
310
|
+
if (!rbac?.getGrantedFeatures) return []
|
|
311
|
+
return (await rbac.getGrantedFeatures(userId, { tenantId, organizationId })) ?? []
|
|
312
|
+
} catch {
|
|
313
|
+
// Fail closed: an unreadable grant set is no grant set, so the caller falls
|
|
314
|
+
// back to their project memberships instead of being treated as a manager.
|
|
315
|
+
return []
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function resolveEntryProjectAccess(
|
|
320
|
+
em: EntityManager,
|
|
321
|
+
ctx: CommandRuntimeContext,
|
|
322
|
+
tenantId: string,
|
|
323
|
+
organizationId: string,
|
|
324
|
+
settings: TimeTrackingSettings,
|
|
325
|
+
): Promise<ProjectAccess> {
|
|
326
|
+
return resolveProjectAccess({
|
|
327
|
+
em: em.fork(),
|
|
328
|
+
userId: ctx.auth?.sub ?? null,
|
|
329
|
+
tenantId,
|
|
330
|
+
organizationId,
|
|
331
|
+
userFeatures: await resolveGrantedFeatures(ctx, tenantId, organizationId),
|
|
332
|
+
assignmentGraceDays: settings.access.assignmentGraceDays,
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Every entry write intersects the caller's project access. An entry that names a
|
|
338
|
+
* project is writable by a project manager or an assigned member; an entry with no
|
|
339
|
+
* project belongs to nobody else, so only the member who logged it may touch it.
|
|
340
|
+
* `staff.timesheets.manage_all` stays a superset of both, which is what it has
|
|
341
|
+
* always meant on this route.
|
|
342
|
+
*/
|
|
343
|
+
async function assertEntryProjectAccess(params: {
|
|
344
|
+
access: ProjectAccess
|
|
345
|
+
hasManageAll: boolean
|
|
346
|
+
projectId: string | null
|
|
347
|
+
entryStaffMemberId: string
|
|
348
|
+
}): Promise<void> {
|
|
349
|
+
if (params.hasManageAll || params.access.canManageAll) return
|
|
350
|
+
const { translate } = await resolveTranslations()
|
|
351
|
+
if (params.projectId) {
|
|
352
|
+
if (params.access.projectIds.includes(params.projectId)) return
|
|
353
|
+
throw new CrudHttpError(403, {
|
|
354
|
+
error: translate('staff.timesheets.errors.notAssigned', 'You are not assigned to this project.'),
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
if (params.access.staffMemberId && params.access.staffMemberId === params.entryStaffMemberId) return
|
|
358
|
+
throw new CrudHttpError(403, {
|
|
359
|
+
error: translate('staff.timesheets.errors.notOwner', 'You can only manage your own time entries.'),
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* `notes` is the column; `description` is the name every consulting-suite screen
|
|
365
|
+
* uses for it. Both keys are accepted on write and `description` wins when a
|
|
366
|
+
* request carries both — the precedence UPGRADE_NOTES.md already published, and
|
|
367
|
+
* the one that matches intent: a client sending the new key means it.
|
|
368
|
+
* Returns `undefined` when neither key was supplied, so a PUT that omits both
|
|
369
|
+
* leaves the stored note alone.
|
|
370
|
+
*
|
|
371
|
+
* Exported so the grid bulk save applies the SAME precedence — it accepts both
|
|
372
|
+
* keys too, and a second interpretation of which one wins is exactly the kind of
|
|
373
|
+
* divergence that makes one screen's edit disappear on another.
|
|
374
|
+
*/
|
|
375
|
+
export function resolveTimeEntryNotesInput(parsed: {
|
|
376
|
+
notes?: string | null
|
|
377
|
+
description?: string | null
|
|
378
|
+
}): string | null | undefined {
|
|
379
|
+
if (parsed.description !== undefined) return parsed.description ?? null
|
|
380
|
+
if (parsed.notes !== undefined) return parsed.notes ?? null
|
|
381
|
+
return undefined
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* `rate_override_amount` is `numeric(14,4)`, which MikroORM surfaces as a string;
|
|
386
|
+
* the validator accepts a JSON number. One conversion point keeps the two apart,
|
|
387
|
+
* for the grid bulk save as much as for this file.
|
|
388
|
+
*/
|
|
389
|
+
export function toStoredTimeEntryRateOverride(amount: number | null | undefined): string | null {
|
|
390
|
+
return amount === null || amount === undefined ? null : String(amount)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* D-7: `duration_minutes` keeps the RAW value the user entered — it is what the
|
|
395
|
+
* timesheet and the project hour totals display — while `rounded_minutes` is
|
|
396
|
+
* derived from the tenant's rounding rule at write time and is the only input to
|
|
397
|
+
* cost. Storing the rounded value rather than deriving it on read is what stops a
|
|
398
|
+
* later settings change from silently restating money that has already been
|
|
399
|
+
* invoiced (risk R1).
|
|
400
|
+
*/
|
|
401
|
+
export function roundedMinutesFor(
|
|
402
|
+
durationMinutes: number,
|
|
403
|
+
settings: TimeTrackingSettings,
|
|
404
|
+
scope?: ScopedResolverContext | null,
|
|
405
|
+
): number {
|
|
406
|
+
return roundMinutes(durationMinutes, settings.rounding, scope)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export type TimeEntryInterval = {
|
|
410
|
+
startedAt: Date | null
|
|
411
|
+
endedAt: Date | null
|
|
412
|
+
durationMinutes: number
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* `started_at` / `ended_at` are absolute timestamps; `deriveInterval` reasons in
|
|
417
|
+
* wall-clock minutes. Both directions read and write the SAME basis (UTC), so a
|
|
418
|
+
* timestamp that needs no correction survives the round trip untouched.
|
|
419
|
+
*/
|
|
420
|
+
function clockOfTimestamp(value: Date): string {
|
|
421
|
+
return formatClock(value.getUTCHours() * 60 + value.getUTCMinutes())
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function timestampOnDayOf(reference: Date, clock: string, dayOffset: number): Date {
|
|
425
|
+
const minutesOfDay = parseClock(clock) ?? 0
|
|
426
|
+
return new Date(
|
|
427
|
+
Date.UTC(
|
|
428
|
+
reference.getUTCFullYear(),
|
|
429
|
+
reference.getUTCMonth(),
|
|
430
|
+
reference.getUTCDate() + dayOffset,
|
|
431
|
+
Math.floor(minutesOfDay / 60),
|
|
432
|
+
minutesOfDay % 60,
|
|
433
|
+
0,
|
|
434
|
+
0,
|
|
435
|
+
),
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* D-8. An end whose clock is earlier than its start's is a midnight crossing, not
|
|
441
|
+
* a validation error, so the end belongs on the NEXT calendar day while `date`
|
|
442
|
+
* keeps naming the day the work started. A caller that already sent the end on
|
|
443
|
+
* the following day (the entry dialog does) is left exactly as it sent it.
|
|
444
|
+
*/
|
|
445
|
+
function alignEndToStart(startedAt: Date, endedAt: Date, endClock: string, crossesMidnight: boolean): Date {
|
|
446
|
+
if (endedAt.getTime() >= startedAt.getTime()) return endedAt
|
|
447
|
+
return timestampOnDayOf(startedAt, endClock, crossesMidnight ? 1 : 0)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* US-D3, on the update path: `duration_minutes`, `started_at` and `ended_at` can
|
|
452
|
+
* each be written on their own, and the server must never store a set that
|
|
453
|
+
* contradicts itself — a two-hour duration on a 09:00–10:00 span is a
|
|
454
|
+
* defensible-invoice problem, not a cosmetic one (D-7 bills the rounded twin of
|
|
455
|
+
* whatever duration lands here).
|
|
456
|
+
*
|
|
457
|
+
* The rule, in one line: **the clocks are the observation and the duration is
|
|
458
|
+
* their summary, so the clocks win — unless the duration is the field the request
|
|
459
|
+
* actually pinned, in which case the untouched clock moves to match it.**
|
|
460
|
+
*
|
|
461
|
+
* - both clocks in the request → the duration is recomputed from them, and a
|
|
462
|
+
* duration sent alongside is advisory (this is what the entry dialog sends);
|
|
463
|
+
* - one clock plus a duration → the OTHER clock is derived from that pair;
|
|
464
|
+
* - one clock alone → the duration is recomputed against the stored counterpart;
|
|
465
|
+
* - the duration alone → `started_at` anchors and `ended_at` shifts to match;
|
|
466
|
+
* - none of the three → every one of them is left exactly as stored.
|
|
467
|
+
*
|
|
468
|
+
* Derivation only ever RESTATES a clock that already exists: with one side null
|
|
469
|
+
* there is nothing for a duration to contradict, and manufacturing the missing
|
|
470
|
+
* side would silently stop a running timer.
|
|
471
|
+
*
|
|
472
|
+
* Exported because the grid bulk save writes durations in its own transaction
|
|
473
|
+
* (per-row, pessimistically locked) and cannot go through this command — it calls
|
|
474
|
+
* the same reconciliation rather than repeating the arithmetic, which is what
|
|
475
|
+
* keeps one rule for one invariant.
|
|
476
|
+
*/
|
|
477
|
+
export function reconcileTimeEntryInterval(params: {
|
|
478
|
+
stored: TimeEntryInterval
|
|
479
|
+
startedAt: Date | null | undefined
|
|
480
|
+
endedAt: Date | null | undefined
|
|
481
|
+
durationMinutes: number | undefined
|
|
482
|
+
}): TimeEntryInterval {
|
|
483
|
+
const startProvided = params.startedAt !== undefined
|
|
484
|
+
const endProvided = params.endedAt !== undefined
|
|
485
|
+
const durationProvided = params.durationMinutes !== undefined
|
|
486
|
+
if (!startProvided && !endProvided && !durationProvided) return params.stored
|
|
487
|
+
|
|
488
|
+
const startedAt = startProvided ? params.startedAt ?? null : params.stored.startedAt
|
|
489
|
+
const endedAt = endProvided ? params.endedAt ?? null : params.stored.endedAt
|
|
490
|
+
const durationMinutes = durationProvided
|
|
491
|
+
? (params.durationMinutes as number)
|
|
492
|
+
: params.stored.durationMinutes
|
|
493
|
+
if (!startedAt || !endedAt) return { startedAt, endedAt, durationMinutes }
|
|
494
|
+
|
|
495
|
+
const startClock = clockOfTimestamp(startedAt)
|
|
496
|
+
const endClock = clockOfTimestamp(endedAt)
|
|
497
|
+
const durationAnchored = durationProvided && !(startProvided && endProvided)
|
|
498
|
+
|
|
499
|
+
if (durationAnchored && !endProvided) {
|
|
500
|
+
const derived = deriveInterval({ start: startClock, durationMinutes })
|
|
501
|
+
return {
|
|
502
|
+
startedAt,
|
|
503
|
+
endedAt: timestampOnDayOf(startedAt, derived.end ?? endClock, derived.crossesMidnight ? 1 : 0),
|
|
504
|
+
durationMinutes,
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (durationAnchored && !startProvided) {
|
|
509
|
+
const derived = deriveInterval({ end: endClock, durationMinutes })
|
|
510
|
+
return {
|
|
511
|
+
startedAt: timestampOnDayOf(endedAt, derived.start ?? startClock, derived.crossesMidnight ? -1 : 0),
|
|
512
|
+
endedAt,
|
|
513
|
+
durationMinutes,
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const derived = deriveInterval({ start: startClock, end: endClock, durationMinutes })
|
|
518
|
+
return {
|
|
519
|
+
startedAt,
|
|
520
|
+
endedAt: alignEndToStart(startedAt, endedAt, endClock, derived.crossesMidnight),
|
|
521
|
+
durationMinutes: derived.durationMinutes ?? durationMinutes,
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const MS_PER_DAY = 86_400_000
|
|
526
|
+
|
|
527
|
+
function toDateOrNull(value: Date | string | null | undefined): Date | null {
|
|
528
|
+
if (value === null || value === undefined) return null
|
|
529
|
+
const date = value instanceof Date ? value : new Date(value)
|
|
530
|
+
return Number.isNaN(date.getTime()) ? null : date
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function utcDayIndex(value: Date): number {
|
|
534
|
+
return Math.round(
|
|
535
|
+
Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()) / MS_PER_DAY,
|
|
536
|
+
)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* T4.10(b): moving an entry to another day moves the work with it.
|
|
541
|
+
*
|
|
542
|
+
* `date` names the day the work STARTED (D-8), so an entry re-dated to Tuesday
|
|
543
|
+
* whose clocks stayed on Monday is the same self-contradicting triple T4.9
|
|
544
|
+
* eliminated one field over — a 09:00–10:00 span filed against a day it did not
|
|
545
|
+
* happen on, and a defensible-invoice problem the moment anyone reads the times
|
|
546
|
+
* back. "Move this entry to Tuesday" means the whole observation moves.
|
|
547
|
+
*
|
|
548
|
+
* The clocks are SHIFTED by whole days rather than recomposed from the new date,
|
|
549
|
+
* which preserves two things a recomposition would destroy: the stored offset
|
|
550
|
+
* between `date` and the timestamps (for a tenant east of UTC a 01:00 local start
|
|
551
|
+
* legitimately sits on the previous UTC day), and a midnight crossing (an end
|
|
552
|
+
* already on the following day stays on the following day).
|
|
553
|
+
*
|
|
554
|
+
* Two things it never does, both inherited from the reconciliation rule above:
|
|
555
|
+
*
|
|
556
|
+
* - a clock that does not exist is not manufactured, and
|
|
557
|
+
* - a RUNNING timer — a start with no end — is left exactly where it is, because
|
|
558
|
+
* re-anchoring its start would restate the elapsed time of work still in
|
|
559
|
+
* progress, and nothing about re-dating the row says the timer began later.
|
|
560
|
+
*/
|
|
561
|
+
export function rebaseTimeEntryIntervalToDate(params: {
|
|
562
|
+
stored: TimeEntryInterval
|
|
563
|
+
fromDate: Date | string | null | undefined
|
|
564
|
+
toDate: Date | string | null | undefined
|
|
565
|
+
}): TimeEntryInterval {
|
|
566
|
+
const fromDate = toDateOrNull(params.fromDate)
|
|
567
|
+
const toDate = toDateOrNull(params.toDate)
|
|
568
|
+
if (!fromDate || !toDate) return params.stored
|
|
569
|
+
|
|
570
|
+
const { startedAt, endedAt } = params.stored
|
|
571
|
+
if (!startedAt && !endedAt) return params.stored
|
|
572
|
+
if (startedAt && !endedAt) return params.stored
|
|
573
|
+
|
|
574
|
+
const shiftMs = (utcDayIndex(toDate) - utcDayIndex(fromDate)) * MS_PER_DAY
|
|
575
|
+
if (shiftMs === 0) return params.stored
|
|
576
|
+
return {
|
|
577
|
+
startedAt: startedAt ? new Date(startedAt.getTime() + shiftMs) : null,
|
|
578
|
+
endedAt: endedAt ? new Date(endedAt.getTime() + shiftMs) : null,
|
|
579
|
+
durationMinutes: params.stored.durationMinutes,
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Reuses the code the tag commands already refuse a frozen entry with, rather
|
|
585
|
+
* than inventing a second one — a client that learned to handle `time_entry_locked`
|
|
586
|
+
* from `…/tags/entry-assignments` handles it identically everywhere else.
|
|
587
|
+
*/
|
|
588
|
+
export const TIME_ENTRY_LOCKED_CODE = TAG_TARGET_LOCKED_CODE
|
|
589
|
+
|
|
590
|
+
export type LockedTimeEntryRef = { id: string; lockedReportId?: string | null }
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* The 409 body names the report that froze the entries so screen 10 note 4 can
|
|
594
|
+
* link straight to it, and lists the offending ids so a grid save can mark those
|
|
595
|
+
* cells read-only instead of showing an opaque failure. `lockedReportId` is kept
|
|
596
|
+
* beside the arrays for shape-compatibility with the tag commands' single-target
|
|
597
|
+
* refusal.
|
|
598
|
+
*/
|
|
599
|
+
export function buildTimeEntryLockedError(
|
|
600
|
+
lockedEntries: LockedTimeEntryRef[],
|
|
601
|
+
translate: Translate,
|
|
602
|
+
): CrudHttpError {
|
|
603
|
+
const lockedEntryIds = lockedEntries.map((entry) => entry.id)
|
|
604
|
+
const lockedReportIds = Array.from(
|
|
605
|
+
new Set(
|
|
606
|
+
lockedEntries
|
|
607
|
+
.map((entry) => entry.lockedReportId)
|
|
608
|
+
.filter((reportId): reportId is string => typeof reportId === 'string' && reportId.length > 0),
|
|
609
|
+
),
|
|
610
|
+
)
|
|
611
|
+
return new CrudHttpError(409, {
|
|
612
|
+
code: TIME_ENTRY_LOCKED_CODE,
|
|
613
|
+
error:
|
|
614
|
+
lockedEntryIds.length > 1
|
|
615
|
+
? translate(
|
|
616
|
+
'staff.time_tracking.entries.errors.entriesLocked',
|
|
617
|
+
'Some of these time entries are locked in a closed report and cannot be changed. Unlock the report first.',
|
|
618
|
+
)
|
|
619
|
+
: translate(
|
|
620
|
+
'staff.time_tracking.entries.errors.entryLocked',
|
|
621
|
+
'This time entry is locked in a closed report and cannot be changed. Unlock the report first.',
|
|
622
|
+
),
|
|
623
|
+
lockedReportId: lockedReportIds[0] ?? null,
|
|
624
|
+
lockedEntryIds,
|
|
625
|
+
lockedReportIds,
|
|
626
|
+
})
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* The lock gate (risk R3). It sits in the command layer rather than in each write
|
|
631
|
+
* route because the routes are the part that keeps growing: duplicate, copy-day
|
|
632
|
+
* and the grid bulk save all arrived after the lock did, and any one of them
|
|
633
|
+
* could have forgotten a per-route check. Every command that resolves an entry
|
|
634
|
+
* for mutation runs this immediately after the row is loaded, so a field added to
|
|
635
|
+
* the update command later is frozen without anybody remembering to freeze it.
|
|
636
|
+
*
|
|
637
|
+
* No permission lifts it — not `staff.timesheets.manage_all`. An entry frozen in
|
|
638
|
+
* a closed report has already been billed at those minutes (D-7), so the only way
|
|
639
|
+
* back is the Phase-6 unlock command, which clears `locked_report_id` itself and
|
|
640
|
+
* therefore never reaches this gate.
|
|
641
|
+
*/
|
|
642
|
+
export function assertTimeEntryUnlocked(
|
|
643
|
+
entry: { id: string; lockedReportId?: string | null },
|
|
644
|
+
translate: Translate,
|
|
645
|
+
): void {
|
|
646
|
+
if (!entry.lockedReportId) return
|
|
647
|
+
throw buildTimeEntryLockedError([{ id: entry.id, lockedReportId: entry.lockedReportId }], translate)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Tags are written through the tag commands, never through the junction table, so
|
|
652
|
+
* an entry write inherits their guarantees unchanged: unknown ids are refused,
|
|
653
|
+
* re-sending a tag is idempotent, an entry frozen in a closed report is refused
|
|
654
|
+
* with 409 `time_entry_locked`, and each change lands in the audit log with its
|
|
655
|
+
* own undo.
|
|
656
|
+
*
|
|
657
|
+
* `tagIds` is a SET, not an append. The assign command answers with the target's
|
|
658
|
+
* full tag list, and that answer is what tells us which tags must then be
|
|
659
|
+
* unassigned — so the desired state is reached without this file ever reading or
|
|
660
|
+
* writing `staff_time_entry_tags` itself.
|
|
661
|
+
*/
|
|
662
|
+
async function syncEntryTags(
|
|
663
|
+
ctx: CommandRuntimeContext,
|
|
664
|
+
params: { timeEntryId: string; tenantId: string; organizationId: string; tagIds: string[] },
|
|
665
|
+
): Promise<void> {
|
|
666
|
+
const bus = ctx.container.resolve('commandBus') as CommandBus
|
|
667
|
+
const desired = Array.from(new Set(params.tagIds))
|
|
668
|
+
const scoped = {
|
|
669
|
+
tenantId: params.tenantId,
|
|
670
|
+
organizationId: params.organizationId,
|
|
671
|
+
timeEntryId: params.timeEntryId,
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const assigned = await bus.execute<unknown, { tagIds?: string[] }>(staffTimeTagCommandIds.assignEntry, {
|
|
675
|
+
input: { ...scoped, tagIds: desired },
|
|
676
|
+
ctx,
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
const current = assigned?.result?.tagIds ?? desired
|
|
680
|
+
const stale = current.filter((tagId) => !desired.includes(tagId))
|
|
681
|
+
if (stale.length === 0) return
|
|
682
|
+
|
|
683
|
+
await bus.execute(staffTimeTagCommandIds.unassignEntry, {
|
|
684
|
+
input: { ...scoped, tagIds: stale },
|
|
685
|
+
ctx,
|
|
686
|
+
})
|
|
118
687
|
}
|
|
119
688
|
|
|
120
689
|
type TimeEntrySnapshot = {
|
|
@@ -124,13 +693,18 @@ type TimeEntrySnapshot = {
|
|
|
124
693
|
staffMemberId: string
|
|
125
694
|
date: string
|
|
126
695
|
durationMinutes: number
|
|
696
|
+
roundedMinutes: number | null
|
|
127
697
|
startedAt: string | null
|
|
128
698
|
endedAt: string | null
|
|
129
699
|
notes: string | null
|
|
130
700
|
timeProjectId: string | null
|
|
701
|
+
taskId: string | null
|
|
131
702
|
customerId: string | null
|
|
132
703
|
dealId: string | null
|
|
133
704
|
orderId: string | null
|
|
705
|
+
isBillable: boolean
|
|
706
|
+
rateOverrideAmount: string | null
|
|
707
|
+
rateCurrencyCode: string | null
|
|
134
708
|
source: string
|
|
135
709
|
deletedAt: string | null
|
|
136
710
|
}
|
|
@@ -156,13 +730,18 @@ async function loadTimeEntrySnapshot(em: EntityManager, id: string, scope?: Staf
|
|
|
156
730
|
staffMemberId: entry.staffMemberId,
|
|
157
731
|
date: entry.date instanceof Date ? entry.date.toISOString().split('T')[0] : String(entry.date),
|
|
158
732
|
durationMinutes: entry.durationMinutes,
|
|
733
|
+
roundedMinutes: entry.roundedMinutes ?? null,
|
|
159
734
|
startedAt: entry.startedAt ? entry.startedAt.toISOString() : null,
|
|
160
735
|
endedAt: entry.endedAt ? entry.endedAt.toISOString() : null,
|
|
161
736
|
notes: entry.notes ?? null,
|
|
162
737
|
timeProjectId: entry.timeProjectId ?? null,
|
|
738
|
+
taskId: entry.taskId ?? null,
|
|
163
739
|
customerId: entry.customerId ?? null,
|
|
164
740
|
dealId: entry.dealId ?? null,
|
|
165
741
|
orderId: entry.orderId ?? null,
|
|
742
|
+
isBillable: entry.isBillable,
|
|
743
|
+
rateOverrideAmount: entry.rateOverrideAmount ?? null,
|
|
744
|
+
rateCurrencyCode: entry.rateCurrencyCode ?? null,
|
|
166
745
|
source: entry.source,
|
|
167
746
|
deletedAt: entry.deletedAt ? entry.deletedAt.toISOString() : null,
|
|
168
747
|
}
|
|
@@ -176,13 +755,18 @@ function timeEntrySeedFromSnapshot(snapshot: TimeEntrySnapshot): Record<string,
|
|
|
176
755
|
staffMemberId: snapshot.staffMemberId,
|
|
177
756
|
date: snapshot.date,
|
|
178
757
|
durationMinutes: snapshot.durationMinutes,
|
|
758
|
+
roundedMinutes: snapshot.roundedMinutes ?? null,
|
|
179
759
|
startedAt: snapshot.startedAt ? new Date(snapshot.startedAt) : null,
|
|
180
760
|
endedAt: snapshot.endedAt ? new Date(snapshot.endedAt) : null,
|
|
181
761
|
notes: snapshot.notes ?? null,
|
|
182
762
|
timeProjectId: snapshot.timeProjectId ?? null,
|
|
763
|
+
taskId: snapshot.taskId ?? null,
|
|
183
764
|
customerId: snapshot.customerId ?? null,
|
|
184
765
|
dealId: snapshot.dealId ?? null,
|
|
185
766
|
orderId: snapshot.orderId ?? null,
|
|
767
|
+
isBillable: snapshot.isBillable ?? true,
|
|
768
|
+
rateOverrideAmount: snapshot.rateOverrideAmount ?? null,
|
|
769
|
+
rateCurrencyCode: snapshot.rateCurrencyCode ?? null,
|
|
186
770
|
source: (snapshot.source ?? 'manual') as StaffTimeEntrySource,
|
|
187
771
|
createdAt: new Date(),
|
|
188
772
|
updatedAt: new Date(),
|
|
@@ -216,9 +800,20 @@ const createTimeEntryCommand: CommandHandler<StaffTimeEntryCreateInput, { timeEn
|
|
|
216
800
|
effectiveStaffMemberId = callerStaffMemberId
|
|
217
801
|
}
|
|
218
802
|
|
|
219
|
-
// Validate referenced timeProjectId
|
|
220
|
-
// Without
|
|
221
|
-
await
|
|
803
|
+
// Validate referenced task and timeProjectId are in-scope before persisting.
|
|
804
|
+
// Without these checks a foreign or stale UUID would produce a dangling reference.
|
|
805
|
+
const task = await requireTaskInScope(em, parsed.taskId ?? null, parsed.tenantId, parsed.organizationId)
|
|
806
|
+
const requestedProjectId = resolveTimeEntryProjectId(parsed.timeProjectId, task)
|
|
807
|
+
const project = await requireTimeProjectInScope(em, requestedProjectId, parsed.tenantId, parsed.organizationId)
|
|
808
|
+
await assertTaskBelongsToProject(task, project?.id ?? null)
|
|
809
|
+
|
|
810
|
+
const settings = await readEntrySettings(ctx, parsed.tenantId)
|
|
811
|
+
await assertEntryProjectAccess({
|
|
812
|
+
access: await resolveEntryProjectAccess(em, ctx, parsed.tenantId, parsed.organizationId, settings),
|
|
813
|
+
hasManageAll: await callerHasManageAll(ctx),
|
|
814
|
+
projectId: project?.id ?? null,
|
|
815
|
+
entryStaffMemberId: effectiveStaffMemberId,
|
|
816
|
+
})
|
|
222
817
|
|
|
223
818
|
const now = new Date()
|
|
224
819
|
const entry = em.create(StaffTimeEntry, {
|
|
@@ -227,13 +822,28 @@ const createTimeEntryCommand: CommandHandler<StaffTimeEntryCreateInput, { timeEn
|
|
|
227
822
|
staffMemberId: effectiveStaffMemberId,
|
|
228
823
|
date: parsed.date,
|
|
229
824
|
durationMinutes: parsed.durationMinutes,
|
|
825
|
+
roundedMinutes: roundedMinutesFor(parsed.durationMinutes, settings, {
|
|
826
|
+
tenantId: parsed.tenantId,
|
|
827
|
+
organizationId: parsed.organizationId,
|
|
828
|
+
}),
|
|
230
829
|
startedAt: parsed.startedAt ?? null,
|
|
231
830
|
endedAt: parsed.endedAt ?? null,
|
|
232
|
-
notes: parsed
|
|
233
|
-
timeProjectId:
|
|
831
|
+
notes: resolveTimeEntryNotesInput(parsed) ?? null,
|
|
832
|
+
timeProjectId: project?.id ?? null,
|
|
833
|
+
taskId: task?.id ?? null,
|
|
234
834
|
customerId: parsed.customerId ?? null,
|
|
235
835
|
dealId: parsed.dealId ?? null,
|
|
236
836
|
orderId: parsed.orderId ?? null,
|
|
837
|
+
isBillable: resolveTimeEntryBillable({
|
|
838
|
+
requested: parsed.isBillable,
|
|
839
|
+
project,
|
|
840
|
+
settings,
|
|
841
|
+
scope: { tenantId: parsed.tenantId, organizationId: parsed.organizationId },
|
|
842
|
+
}),
|
|
843
|
+
rateOverrideAmount: toStoredTimeEntryRateOverride(parsed.rateOverrideAmount),
|
|
844
|
+
// Snapshotted rather than joined at read time so a later project currency
|
|
845
|
+
// change cannot re-denominate money that has already been reported (D-3).
|
|
846
|
+
rateCurrencyCode: project?.currencyCode ?? null,
|
|
237
847
|
source: parsed.source ?? 'manual',
|
|
238
848
|
createdAt: now,
|
|
239
849
|
updatedAt: now,
|
|
@@ -242,6 +852,15 @@ const createTimeEntryCommand: CommandHandler<StaffTimeEntryCreateInput, { timeEn
|
|
|
242
852
|
em.persist(entry)
|
|
243
853
|
await em.flush()
|
|
244
854
|
|
|
855
|
+
if (parsed.tagIds !== undefined) {
|
|
856
|
+
await syncEntryTags(ctx, {
|
|
857
|
+
timeEntryId: entry.id,
|
|
858
|
+
tenantId: parsed.tenantId,
|
|
859
|
+
organizationId: parsed.organizationId,
|
|
860
|
+
tagIds: parsed.tagIds,
|
|
861
|
+
})
|
|
862
|
+
}
|
|
863
|
+
|
|
245
864
|
await emitCrudSideEffects({
|
|
246
865
|
dataEngine: ctx.container.resolve('dataEngine'),
|
|
247
866
|
action: 'created',
|
|
@@ -333,7 +952,23 @@ const startTimerCommand: CommandHandler<StaffTimeEntryStartTimerInput, { timeEnt
|
|
|
333
952
|
effectiveStaffMemberId = callerStaffMemberId
|
|
334
953
|
}
|
|
335
954
|
|
|
336
|
-
|
|
955
|
+
// Starting a timer on a task is ONE write. The task is validated here, in the
|
|
956
|
+
// same command, so the entry is created already carrying it — the alternative
|
|
957
|
+
// (start, then link) can fail after the timer is running and strand it against
|
|
958
|
+
// the project instead of the task.
|
|
959
|
+
const task = await requireTaskInScope(em, parsed.taskId ?? null, parsed.tenantId, parsed.organizationId)
|
|
960
|
+
// A card knows its task, not always its project, and a task belongs to exactly
|
|
961
|
+
// one project — so the project follows from the task, exactly as it does when
|
|
962
|
+
// an entry is created from screen 8.
|
|
963
|
+
const requestedProjectId = resolveTimeEntryProjectId(parsed.timeProjectId, task)
|
|
964
|
+
const project = await requireTimeProjectInScope(em, requestedProjectId, parsed.tenantId, parsed.organizationId)
|
|
965
|
+
await assertTaskBelongsToProject(task, project?.id ?? null)
|
|
966
|
+
|
|
967
|
+
// A timer entry starts at zero minutes, and D-7 makes `rounded_minutes` the
|
|
968
|
+
// only input to cost — leaving it null here would price the entry at nothing
|
|
969
|
+
// until something else happened to rewrite it. Seeded from the same rule as
|
|
970
|
+
// every other create so timer-stop only ever restates it.
|
|
971
|
+
const settings = await readEntrySettings(ctx, parsed.tenantId)
|
|
337
972
|
|
|
338
973
|
const scopeCtx = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
|
|
339
974
|
|
|
@@ -374,10 +1009,15 @@ const startTimerCommand: CommandHandler<StaffTimeEntryStartTimerInput, { timeEnt
|
|
|
374
1009
|
staffMemberId: effectiveStaffMemberId,
|
|
375
1010
|
date: parsed.date,
|
|
376
1011
|
durationMinutes: 0,
|
|
1012
|
+
roundedMinutes: roundedMinutesFor(0, settings, {
|
|
1013
|
+
tenantId: parsed.tenantId,
|
|
1014
|
+
organizationId: parsed.organizationId,
|
|
1015
|
+
}),
|
|
377
1016
|
startedAt,
|
|
378
1017
|
endedAt: null,
|
|
379
1018
|
notes: parsed.notes ?? null,
|
|
380
|
-
timeProjectId:
|
|
1019
|
+
timeProjectId: project?.id ?? null,
|
|
1020
|
+
taskId: task?.id ?? null,
|
|
381
1021
|
customerId: null,
|
|
382
1022
|
dealId: null,
|
|
383
1023
|
orderId: null,
|
|
@@ -495,9 +1135,15 @@ const updateTimeEntryCommand: CommandHandler<StaffTimeEntryUpdateInput, { timeEn
|
|
|
495
1135
|
ensureTenantScope(ctx, entry.tenantId)
|
|
496
1136
|
ensureOrganizationScope(ctx, entry.organizationId)
|
|
497
1137
|
|
|
1138
|
+
// Before ownership, before access, before a single field is read off the
|
|
1139
|
+
// request: a frozen entry is frozen for everyone, so nothing below can be
|
|
1140
|
+
// reached with a lock in place.
|
|
1141
|
+
assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate)
|
|
1142
|
+
|
|
498
1143
|
// Ownership enforcement: callers without `staff.timesheets.manage_all`
|
|
499
1144
|
// can only update entries they own.
|
|
500
|
-
|
|
1145
|
+
const hasManageAll = await callerHasManageAll(ctx)
|
|
1146
|
+
if (!hasManageAll) {
|
|
501
1147
|
const callerStaffMemberId = await resolveCallerStaffMemberId(em, ctx)
|
|
502
1148
|
if (!callerStaffMemberId || entry.staffMemberId !== callerStaffMemberId) {
|
|
503
1149
|
const { translate } = await resolveTranslations()
|
|
@@ -507,21 +1153,109 @@ const updateTimeEntryCommand: CommandHandler<StaffTimeEntryUpdateInput, { timeEn
|
|
|
507
1153
|
}
|
|
508
1154
|
}
|
|
509
1155
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
1156
|
+
const task =
|
|
1157
|
+
parsed.taskId !== undefined
|
|
1158
|
+
? await requireTaskInScope(em, parsed.taskId, entry.tenantId, entry.organizationId)
|
|
1159
|
+
: null
|
|
1160
|
+
|
|
1161
|
+
// A task carries its project (screen 8), so re-pointing an entry at a task
|
|
1162
|
+
// moves the entry with it unless the request names a project explicitly.
|
|
1163
|
+
const nextProjectId =
|
|
1164
|
+
parsed.timeProjectId !== undefined
|
|
1165
|
+
? parsed.timeProjectId ?? null
|
|
1166
|
+
: task?.timeProjectId ?? entry.timeProjectId ?? null
|
|
1167
|
+
const projectChanged = nextProjectId !== (entry.timeProjectId ?? null)
|
|
1168
|
+
const project = projectChanged
|
|
1169
|
+
? await requireTimeProjectInScope(em, nextProjectId, entry.tenantId, entry.organizationId)
|
|
1170
|
+
: null
|
|
1171
|
+
await assertTaskBelongsToProject(task, nextProjectId)
|
|
1172
|
+
|
|
1173
|
+
const settings = await readEntrySettings(ctx, entry.tenantId)
|
|
1174
|
+
const access = await resolveEntryProjectAccess(em, ctx, entry.tenantId, entry.organizationId, settings)
|
|
1175
|
+
// Both ends of a move are checked: a caller may not lift an entry out of a
|
|
1176
|
+
// project they cannot see, nor drop one into a project they cannot see.
|
|
1177
|
+
await assertEntryProjectAccess({
|
|
1178
|
+
access,
|
|
1179
|
+
hasManageAll,
|
|
1180
|
+
projectId: entry.timeProjectId ?? null,
|
|
1181
|
+
entryStaffMemberId: entry.staffMemberId,
|
|
1182
|
+
})
|
|
1183
|
+
if (projectChanged) {
|
|
1184
|
+
await assertEntryProjectAccess({
|
|
1185
|
+
access,
|
|
1186
|
+
hasManageAll,
|
|
1187
|
+
projectId: nextProjectId,
|
|
1188
|
+
entryStaffMemberId: entry.staffMemberId,
|
|
1189
|
+
})
|
|
513
1190
|
}
|
|
514
1191
|
|
|
1192
|
+
const notes = resolveTimeEntryNotesInput(parsed)
|
|
1193
|
+
|
|
1194
|
+
// A `date` move re-anchors the STORED clocks first, so what reconciliation
|
|
1195
|
+
// reasons about is the entry as it will exist on its new day. Order matters:
|
|
1196
|
+
// clocks the request sends explicitly still win over the rebased ones (the
|
|
1197
|
+
// entry dialog rebuilds all three from the new date and would otherwise be
|
|
1198
|
+
// shifted twice), while clocks it omits travel with the entry instead of
|
|
1199
|
+
// being stranded on the old day.
|
|
1200
|
+
const stored = rebaseTimeEntryIntervalToDate({
|
|
1201
|
+
stored: {
|
|
1202
|
+
startedAt: entry.startedAt ?? null,
|
|
1203
|
+
endedAt: entry.endedAt ?? null,
|
|
1204
|
+
durationMinutes: entry.durationMinutes,
|
|
1205
|
+
},
|
|
1206
|
+
fromDate: entry.date,
|
|
1207
|
+
toDate: parsed.date,
|
|
1208
|
+
})
|
|
1209
|
+
|
|
1210
|
+
// The three time fields are settled together, before anything is assigned, so
|
|
1211
|
+
// no combination of them can be stored contradicting itself (US-D3, D-8).
|
|
1212
|
+
const interval = reconcileTimeEntryInterval({
|
|
1213
|
+
stored,
|
|
1214
|
+
startedAt: parsed.startedAt,
|
|
1215
|
+
endedAt: parsed.endedAt,
|
|
1216
|
+
durationMinutes: parsed.durationMinutes,
|
|
1217
|
+
})
|
|
1218
|
+
|
|
515
1219
|
if (parsed.date !== undefined) entry.date = parsed.date
|
|
516
|
-
if (
|
|
517
|
-
if (
|
|
1220
|
+
if (interval.startedAt !== (entry.startedAt ?? null)) entry.startedAt = interval.startedAt
|
|
1221
|
+
if (interval.endedAt !== (entry.endedAt ?? null)) entry.endedAt = interval.endedAt
|
|
1222
|
+
if (parsed.durationMinutes !== undefined || interval.durationMinutes !== entry.durationMinutes) {
|
|
1223
|
+
entry.durationMinutes = interval.durationMinutes
|
|
1224
|
+
// Raw minutes moved — because the duration was written, or because the
|
|
1225
|
+
// clocks were edited and restated it — so the frozen rounded value is
|
|
1226
|
+
// recomputed from the tenant's current rule. D-7 makes it the only input to
|
|
1227
|
+
// cost, so a clock edit that changes the length of the work and leaves the
|
|
1228
|
+
// rounded twin behind would mis-bill it.
|
|
1229
|
+
entry.roundedMinutes = roundedMinutesFor(interval.durationMinutes, settings, {
|
|
1230
|
+
tenantId: entry.tenantId,
|
|
1231
|
+
organizationId: entry.organizationId,
|
|
1232
|
+
})
|
|
1233
|
+
}
|
|
1234
|
+
if (projectChanged) {
|
|
1235
|
+
entry.timeProjectId = nextProjectId
|
|
1236
|
+
entry.rateCurrencyCode = project?.currencyCode ?? null
|
|
1237
|
+
}
|
|
1238
|
+
if (parsed.taskId !== undefined) entry.taskId = task?.id ?? null
|
|
518
1239
|
if (parsed.customerId !== undefined) entry.customerId = parsed.customerId ?? null
|
|
519
1240
|
if (parsed.dealId !== undefined) entry.dealId = parsed.dealId ?? null
|
|
520
1241
|
if (parsed.orderId !== undefined) entry.orderId = parsed.orderId ?? null
|
|
521
|
-
if (parsed.
|
|
1242
|
+
if (parsed.isBillable !== undefined) entry.isBillable = parsed.isBillable
|
|
1243
|
+
if (parsed.rateOverrideAmount !== undefined) {
|
|
1244
|
+
entry.rateOverrideAmount = toStoredTimeEntryRateOverride(parsed.rateOverrideAmount)
|
|
1245
|
+
}
|
|
1246
|
+
if (notes !== undefined) entry.notes = notes
|
|
522
1247
|
entry.updatedAt = new Date()
|
|
523
1248
|
await em.flush()
|
|
524
1249
|
|
|
1250
|
+
if (parsed.tagIds !== undefined) {
|
|
1251
|
+
await syncEntryTags(ctx, {
|
|
1252
|
+
timeEntryId: entry.id,
|
|
1253
|
+
tenantId: entry.tenantId,
|
|
1254
|
+
organizationId: entry.organizationId,
|
|
1255
|
+
tagIds: parsed.tagIds,
|
|
1256
|
+
})
|
|
1257
|
+
}
|
|
1258
|
+
|
|
525
1259
|
await emitCrudSideEffects({
|
|
526
1260
|
dataEngine: ctx.container.resolve('dataEngine'),
|
|
527
1261
|
action: 'updated',
|
|
@@ -542,10 +1276,19 @@ const updateTimeEntryCommand: CommandHandler<StaffTimeEntryUpdateInput, { timeEn
|
|
|
542
1276
|
const changes = buildChanges(before as unknown as Record<string, unknown>, after as unknown as Record<string, unknown>, [
|
|
543
1277
|
'date',
|
|
544
1278
|
'durationMinutes',
|
|
1279
|
+
'roundedMinutes',
|
|
1280
|
+
// A time correction is exactly the edit somebody will later have to justify
|
|
1281
|
+
// on an invoice, so the clocks are named in the trail beside the minutes.
|
|
1282
|
+
'startedAt',
|
|
1283
|
+
'endedAt',
|
|
545
1284
|
'timeProjectId',
|
|
1285
|
+
'taskId',
|
|
546
1286
|
'customerId',
|
|
547
1287
|
'dealId',
|
|
548
1288
|
'orderId',
|
|
1289
|
+
'isBillable',
|
|
1290
|
+
'rateOverrideAmount',
|
|
1291
|
+
'rateCurrencyCode',
|
|
549
1292
|
'notes',
|
|
550
1293
|
'deletedAt',
|
|
551
1294
|
])
|
|
@@ -576,10 +1319,20 @@ const updateTimeEntryCommand: CommandHandler<StaffTimeEntryUpdateInput, { timeEn
|
|
|
576
1319
|
if (!entry) return
|
|
577
1320
|
entry.date = before.date as unknown as Date
|
|
578
1321
|
entry.durationMinutes = before.durationMinutes
|
|
1322
|
+
entry.roundedMinutes = before.roundedMinutes ?? null
|
|
1323
|
+
// Restored beside the minutes they belong to: now that an update can move the
|
|
1324
|
+
// clocks, an undo that left them where the edit put them would hand back an
|
|
1325
|
+
// entry whose duration and span disagree.
|
|
1326
|
+
entry.startedAt = before.startedAt ? new Date(before.startedAt) : null
|
|
1327
|
+
entry.endedAt = before.endedAt ? new Date(before.endedAt) : null
|
|
579
1328
|
entry.timeProjectId = before.timeProjectId ?? null
|
|
1329
|
+
entry.taskId = before.taskId ?? null
|
|
580
1330
|
entry.customerId = before.customerId ?? null
|
|
581
1331
|
entry.dealId = before.dealId ?? null
|
|
582
1332
|
entry.orderId = before.orderId ?? null
|
|
1333
|
+
entry.isBillable = before.isBillable ?? true
|
|
1334
|
+
entry.rateOverrideAmount = before.rateOverrideAmount ?? null
|
|
1335
|
+
entry.rateCurrencyCode = before.rateCurrencyCode ?? null
|
|
583
1336
|
entry.notes = before.notes ?? null
|
|
584
1337
|
entry.deletedAt = before.deletedAt ? new Date(before.deletedAt) : null
|
|
585
1338
|
entry.updatedAt = new Date()
|
|
@@ -622,9 +1375,14 @@ const deleteTimeEntryCommand: CommandHandler<{ id?: string }, { timeEntryId: str
|
|
|
622
1375
|
ensureTenantScope(ctx, entry.tenantId)
|
|
623
1376
|
ensureOrganizationScope(ctx, entry.organizationId)
|
|
624
1377
|
|
|
1378
|
+
// A closed report renders these minutes; deleting the row would leave the
|
|
1379
|
+
// report totalling hours that no longer exist.
|
|
1380
|
+
assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate)
|
|
1381
|
+
|
|
625
1382
|
// Ownership enforcement: callers without `staff.timesheets.manage_all`
|
|
626
1383
|
// can only delete entries they own.
|
|
627
|
-
|
|
1384
|
+
const hasManageAll = await callerHasManageAll(ctx)
|
|
1385
|
+
if (!hasManageAll) {
|
|
628
1386
|
const callerStaffMemberId = await resolveCallerStaffMemberId(em, ctx)
|
|
629
1387
|
if (!callerStaffMemberId || entry.staffMemberId !== callerStaffMemberId) {
|
|
630
1388
|
const { translate } = await resolveTranslations()
|
|
@@ -634,6 +1392,14 @@ const deleteTimeEntryCommand: CommandHandler<{ id?: string }, { timeEntryId: str
|
|
|
634
1392
|
}
|
|
635
1393
|
}
|
|
636
1394
|
|
|
1395
|
+
const settings = await readEntrySettings(ctx, entry.tenantId)
|
|
1396
|
+
await assertEntryProjectAccess({
|
|
1397
|
+
access: await resolveEntryProjectAccess(em, ctx, entry.tenantId, entry.organizationId, settings),
|
|
1398
|
+
hasManageAll,
|
|
1399
|
+
projectId: entry.timeProjectId ?? null,
|
|
1400
|
+
entryStaffMemberId: entry.staffMemberId,
|
|
1401
|
+
})
|
|
1402
|
+
|
|
637
1403
|
entry.deletedAt = new Date()
|
|
638
1404
|
entry.updatedAt = new Date()
|
|
639
1405
|
await em.flush()
|
|
@@ -681,13 +1447,18 @@ const deleteTimeEntryCommand: CommandHandler<{ id?: string }, { timeEntryId: str
|
|
|
681
1447
|
staffMemberId: before.staffMemberId,
|
|
682
1448
|
date: before.date as unknown as Date,
|
|
683
1449
|
durationMinutes: before.durationMinutes,
|
|
1450
|
+
roundedMinutes: before.roundedMinutes ?? null,
|
|
684
1451
|
startedAt: before.startedAt ? new Date(before.startedAt) : null,
|
|
685
1452
|
endedAt: before.endedAt ? new Date(before.endedAt) : null,
|
|
686
1453
|
notes: before.notes ?? null,
|
|
687
1454
|
timeProjectId: before.timeProjectId ?? null,
|
|
1455
|
+
taskId: before.taskId ?? null,
|
|
688
1456
|
customerId: before.customerId ?? null,
|
|
689
1457
|
dealId: before.dealId ?? null,
|
|
690
1458
|
orderId: before.orderId ?? null,
|
|
1459
|
+
isBillable: before.isBillable ?? true,
|
|
1460
|
+
rateOverrideAmount: before.rateOverrideAmount ?? null,
|
|
1461
|
+
rateCurrencyCode: before.rateCurrencyCode ?? null,
|
|
691
1462
|
source: (before.source ?? 'manual') as StaffTimeEntrySource,
|
|
692
1463
|
deletedAt: null,
|
|
693
1464
|
createdAt: new Date(),
|
|
@@ -698,13 +1469,18 @@ const deleteTimeEntryCommand: CommandHandler<{ id?: string }, { timeEntryId: str
|
|
|
698
1469
|
entry.staffMemberId = before.staffMemberId
|
|
699
1470
|
entry.date = before.date as unknown as Date
|
|
700
1471
|
entry.durationMinutes = before.durationMinutes
|
|
1472
|
+
entry.roundedMinutes = before.roundedMinutes ?? null
|
|
701
1473
|
entry.startedAt = before.startedAt ? new Date(before.startedAt) : null
|
|
702
1474
|
entry.endedAt = before.endedAt ? new Date(before.endedAt) : null
|
|
703
1475
|
entry.notes = before.notes ?? null
|
|
704
1476
|
entry.timeProjectId = before.timeProjectId ?? null
|
|
1477
|
+
entry.taskId = before.taskId ?? null
|
|
705
1478
|
entry.customerId = before.customerId ?? null
|
|
706
1479
|
entry.dealId = before.dealId ?? null
|
|
707
1480
|
entry.orderId = before.orderId ?? null
|
|
1481
|
+
entry.isBillable = before.isBillable ?? true
|
|
1482
|
+
entry.rateOverrideAmount = before.rateOverrideAmount ?? null
|
|
1483
|
+
entry.rateCurrencyCode = before.rateCurrencyCode ?? null
|
|
708
1484
|
entry.source = (before.source ?? 'manual') as StaffTimeEntrySource
|
|
709
1485
|
entry.deletedAt = null
|
|
710
1486
|
entry.updatedAt = new Date()
|
|
@@ -722,7 +1498,209 @@ const deleteTimeEntryCommand: CommandHandler<{ id?: string }, { timeEntryId: str
|
|
|
722
1498
|
},
|
|
723
1499
|
}
|
|
724
1500
|
|
|
1501
|
+
export const staffTimeEntryDuplicateSchema = z.object({
|
|
1502
|
+
id: z.string().uuid(),
|
|
1503
|
+
// The copy usually lands on another day (US-D6 "log the same thing again"), so
|
|
1504
|
+
// the caller may move it; omitted, it keeps the source's day.
|
|
1505
|
+
date: z.coerce.date().optional(),
|
|
1506
|
+
// US-D6 ends "…I adjust the duration and the date", so both adjustments travel
|
|
1507
|
+
// with the copy request rather than costing a second round trip that would
|
|
1508
|
+
// leave the unadjusted duration visible in between. Omitted, the copy keeps the
|
|
1509
|
+
// source's minutes. The rounded twin is always recomputed by the create command
|
|
1510
|
+
// from the tenant rule, so an overridden duration can never carry the source's
|
|
1511
|
+
// stale `rounded_minutes` into a new entry (D-7).
|
|
1512
|
+
durationMinutes: z.number().int().min(0).max(1440).optional(),
|
|
1513
|
+
// Tags are NOT read off the source here: this file writes tags only through the
|
|
1514
|
+
// tag commands and never touches `staff_time_entry_tags`, so the caller — which
|
|
1515
|
+
// already has the source's `tags[]` from the list response — passes them on.
|
|
1516
|
+
tagIds: z.array(z.string().uuid()).max(50).optional(),
|
|
1517
|
+
})
|
|
1518
|
+
|
|
1519
|
+
export type StaffTimeEntryDuplicateInput = z.infer<typeof staffTimeEntryDuplicateSchema>
|
|
1520
|
+
|
|
1521
|
+
/**
|
|
1522
|
+
* US-D6. The fourth write path TC-TT-018 exercises. It resolves the SOURCE entry
|
|
1523
|
+
* and runs it past the lock gate before copying: screen 10 note 4 hides every row
|
|
1524
|
+
* action on a locked row, so an API that happily duplicated one would be a side
|
|
1525
|
+
* door back into a frozen report's numbers.
|
|
1526
|
+
*
|
|
1527
|
+
* The copy itself is delegated to the create command rather than re-implemented,
|
|
1528
|
+
* so it inherits ownership enforcement, project access, the billable/currency
|
|
1529
|
+
* defaults, rounding (D-7), tag syncing and the audit entry unchanged.
|
|
1530
|
+
*/
|
|
1531
|
+
const duplicateTimeEntryCommand: CommandHandler<StaffTimeEntryDuplicateInput, { timeEntryId: string }> = {
|
|
1532
|
+
id: 'staff.timesheets.time_entries.duplicate',
|
|
1533
|
+
async execute(rawInput, ctx) {
|
|
1534
|
+
const parsed = staffTimeEntryDuplicateSchema.parse(rawInput)
|
|
1535
|
+
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
1536
|
+
const scope = commandActorScope(ctx)
|
|
1537
|
+
const source = await findOneWithDecryption(
|
|
1538
|
+
em,
|
|
1539
|
+
StaffTimeEntry,
|
|
1540
|
+
applyScopeToWhere<StaffTimeEntry>({ id: parsed.id, deletedAt: null }, scope),
|
|
1541
|
+
undefined,
|
|
1542
|
+
scopeForDecryption(scope),
|
|
1543
|
+
)
|
|
1544
|
+
if (!source) throw new CrudHttpError(404, { error: 'Time entry not found.' })
|
|
1545
|
+
ensureTenantScope(ctx, source.tenantId)
|
|
1546
|
+
ensureOrganizationScope(ctx, source.organizationId)
|
|
1547
|
+
|
|
1548
|
+
assertTimeEntryUnlocked(source, (await resolveTranslations()).translate)
|
|
1549
|
+
|
|
1550
|
+
const bus = ctx.container.resolve('commandBus') as CommandBus
|
|
1551
|
+
const created = await bus.execute<unknown, { timeEntryId: string }>(createTimeEntryCommand.id, {
|
|
1552
|
+
input: {
|
|
1553
|
+
tenantId: source.tenantId,
|
|
1554
|
+
organizationId: source.organizationId,
|
|
1555
|
+
staffMemberId: source.staffMemberId,
|
|
1556
|
+
date: parsed.date ?? source.date,
|
|
1557
|
+
durationMinutes: parsed.durationMinutes ?? source.durationMinutes,
|
|
1558
|
+
timeProjectId: source.timeProjectId ?? null,
|
|
1559
|
+
taskId: source.taskId ?? null,
|
|
1560
|
+
customerId: source.customerId ?? null,
|
|
1561
|
+
dealId: source.dealId ?? null,
|
|
1562
|
+
orderId: source.orderId ?? null,
|
|
1563
|
+
isBillable: source.isBillable,
|
|
1564
|
+
rateOverrideAmount:
|
|
1565
|
+
source.rateOverrideAmount === null || source.rateOverrideAmount === undefined
|
|
1566
|
+
? null
|
|
1567
|
+
: Number(source.rateOverrideAmount),
|
|
1568
|
+
description: source.notes ?? null,
|
|
1569
|
+
// A copy is typed, not timed: carrying `started_at`/`ended_at` over would
|
|
1570
|
+
// manufacture an overlap with the entry it was copied from (US-D7).
|
|
1571
|
+
source: 'manual' as StaffTimeEntrySource,
|
|
1572
|
+
...(parsed.tagIds !== undefined ? { tagIds: parsed.tagIds } : {}),
|
|
1573
|
+
},
|
|
1574
|
+
ctx,
|
|
1575
|
+
})
|
|
1576
|
+
|
|
1577
|
+
const timeEntryId = created?.result?.timeEntryId
|
|
1578
|
+
if (!timeEntryId) throw new CrudHttpError(400, { error: 'Failed to duplicate time entry.' })
|
|
1579
|
+
return { timeEntryId }
|
|
1580
|
+
},
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
export const staffTimeEntryReapplyRoundingSchema = z.object({
|
|
1584
|
+
tenantId: z.string().uuid(),
|
|
1585
|
+
organizationId: z.string().uuid(),
|
|
1586
|
+
entryIds: z.array(z.string().uuid()).min(1).max(500),
|
|
1587
|
+
})
|
|
1588
|
+
|
|
1589
|
+
export type StaffTimeEntryReapplyRoundingInput = z.infer<typeof staffTimeEntryReapplyRoundingSchema>
|
|
1590
|
+
|
|
1591
|
+
export type StaffTimeEntryReapplyRoundingResult = {
|
|
1592
|
+
/** Entries whose `rounded_minutes` actually moved. */
|
|
1593
|
+
updatedCount: number
|
|
1594
|
+
/** Entries already at the current rule — read, compared, left alone. */
|
|
1595
|
+
unchangedCount: number
|
|
1596
|
+
/** Requested ids that were locked, deleted or out of scope, and therefore never loaded. */
|
|
1597
|
+
skippedCount: number
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
/**
|
|
1601
|
+
* Retroactive rounding (T7.3, screen 16 note 3) — restates `rounded_minutes` on a
|
|
1602
|
+
* batch of existing entries under the tenant's current rule.
|
|
1603
|
+
*
|
|
1604
|
+
* Three properties, in the order they matter:
|
|
1605
|
+
*
|
|
1606
|
+
* 1. **A locked entry is unreachable, not merely skipped.** `lockedReportId: null`
|
|
1607
|
+
* is part of the WHERE clause, so an entry frozen into a closed report is never
|
|
1608
|
+
* loaded, never compared and never assigned. The guarantee is structural: there
|
|
1609
|
+
* is no branch in this handler that could write one.
|
|
1610
|
+
* 2. **It is an explicit, batched administrative action**, driven by the
|
|
1611
|
+
* `reapply-rounding` ProgressJob worker, never a side effect of saving the
|
|
1612
|
+
* settings form. Rewriting billing history silently is exactly the failure the
|
|
1613
|
+
* spec's risk R1 is about.
|
|
1614
|
+
* 3. **It writes one derived column and nothing else.** No clock is re-anchored,
|
|
1615
|
+
* no duration is touched — `duration_minutes` remains the raw observation.
|
|
1616
|
+
*
|
|
1617
|
+
* Reachable only by a privileged actor: the queue worker (`systemActor`) or a
|
|
1618
|
+
* super-admin. An interactive caller cannot rewrite a tenant's history through the
|
|
1619
|
+
* command bus by guessing the id; the route that enqueues the job is the gate, and
|
|
1620
|
+
* it is gated on `staff.timesheets.settings.manage`.
|
|
1621
|
+
*/
|
|
1622
|
+
const reapplyRoundingCommand: CommandHandler<
|
|
1623
|
+
StaffTimeEntryReapplyRoundingInput,
|
|
1624
|
+
StaffTimeEntryReapplyRoundingResult
|
|
1625
|
+
> = {
|
|
1626
|
+
id: 'staff.timesheets.time_entries.reapply_rounding',
|
|
1627
|
+
async execute(input, ctx) {
|
|
1628
|
+
const parsed = staffTimeEntryReapplyRoundingSchema.parse(input ?? {})
|
|
1629
|
+
if (ctx.systemActor !== true && ctx.auth?.isSuperAdmin !== true) {
|
|
1630
|
+
const { translate } = await resolveTranslations()
|
|
1631
|
+
throw new CrudHttpError(403, { error: translate('staff.errors.forbidden', 'Forbidden') })
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
const scope = commandInputScope(ctx, parsed.tenantId, parsed.organizationId)
|
|
1635
|
+
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
1636
|
+
const settings = await readEntrySettings(ctx, parsed.tenantId)
|
|
1637
|
+
|
|
1638
|
+
const entries = await em.find(
|
|
1639
|
+
StaffTimeEntry,
|
|
1640
|
+
applyScopeToWhere<StaffTimeEntry>(
|
|
1641
|
+
{
|
|
1642
|
+
id: { $in: parsed.entryIds },
|
|
1643
|
+
deletedAt: null,
|
|
1644
|
+
// Non-negotiable: a frozen entry has already been billed at the value it
|
|
1645
|
+
// carries, so it is excluded at the query, not at an `if`.
|
|
1646
|
+
lockedReportId: null,
|
|
1647
|
+
},
|
|
1648
|
+
scope,
|
|
1649
|
+
),
|
|
1650
|
+
)
|
|
1651
|
+
|
|
1652
|
+
const changed: StaffTimeEntry[] = []
|
|
1653
|
+
let unchangedCount = 0
|
|
1654
|
+
for (const entry of entries) {
|
|
1655
|
+
const next = roundedMinutesFor(entry.durationMinutes, settings, {
|
|
1656
|
+
tenantId: entry.tenantId,
|
|
1657
|
+
organizationId: entry.organizationId,
|
|
1658
|
+
})
|
|
1659
|
+
if (entry.roundedMinutes === next) {
|
|
1660
|
+
unchangedCount += 1
|
|
1661
|
+
continue
|
|
1662
|
+
}
|
|
1663
|
+
entry.roundedMinutes = next
|
|
1664
|
+
entry.updatedAt = new Date()
|
|
1665
|
+
changed.push(entry)
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
if (changed.length > 0) await em.flush()
|
|
1669
|
+
|
|
1670
|
+
const dataEngine = ctx.container.resolve('dataEngine') as Parameters<
|
|
1671
|
+
typeof emitCrudSideEffects
|
|
1672
|
+
>[0]['dataEngine']
|
|
1673
|
+
for (const entry of changed) {
|
|
1674
|
+
await emitCrudSideEffects({
|
|
1675
|
+
dataEngine,
|
|
1676
|
+
action: 'updated',
|
|
1677
|
+
entity: entry,
|
|
1678
|
+
identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },
|
|
1679
|
+
events: staffTimeEntryCrudEvents,
|
|
1680
|
+
indexer: timeEntryCrudIndexer,
|
|
1681
|
+
})
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
return {
|
|
1685
|
+
updatedCount: changed.length,
|
|
1686
|
+
unchangedCount,
|
|
1687
|
+
skippedCount: Math.max(0, parsed.entryIds.length - entries.length),
|
|
1688
|
+
}
|
|
1689
|
+
},
|
|
1690
|
+
}
|
|
1691
|
+
|
|
725
1692
|
registerCommand(createTimeEntryCommand)
|
|
726
1693
|
registerCommand(startTimerCommand)
|
|
727
1694
|
registerCommand(updateTimeEntryCommand)
|
|
728
1695
|
registerCommand(deleteTimeEntryCommand)
|
|
1696
|
+
registerCommand(duplicateTimeEntryCommand)
|
|
1697
|
+
registerCommand(reapplyRoundingCommand)
|
|
1698
|
+
|
|
1699
|
+
export const staffTimeEntryCommandIds = {
|
|
1700
|
+
create: createTimeEntryCommand.id,
|
|
1701
|
+
startTimer: startTimerCommand.id,
|
|
1702
|
+
update: updateTimeEntryCommand.id,
|
|
1703
|
+
delete: deleteTimeEntryCommand.id,
|
|
1704
|
+
duplicate: duplicateTimeEntryCommand.id,
|
|
1705
|
+
reapplyRounding: reapplyRoundingCommand.id,
|
|
1706
|
+
} as const
|