@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
|
@@ -17,8 +17,15 @@ See [`.ai/specs/implemented/2026-05-08-staff-decouple-from-core.md`](../../../..
|
|
|
17
17
|
| Key | Contract |
|
|
18
18
|
|-----|----------|
|
|
19
19
|
| `availabilityAccessResolver` | Resolves an `AvailabilityWriteAccess` shape for the authenticated request, including whether the caller may edit availability for all members vs only themselves. Consumed by `planner/api/access.ts` via `container.resolve(..., { allowUnregistered: true })` — planner gracefully degrades to `403 staff_module_not_loaded` when staff is absent. |
|
|
20
|
+
| `timeRoundingResolver` | Server entry point to the rounding registry (EP-32). `resolveStrategy(ctx?)` returns the strategy a scoped call site should run; `roundMinutes(raw, settings, ctx?)` is the module function. See § Time-tracking strategy registries. |
|
|
21
|
+
| `timeRateResolver` | Server entry point to the rate resolver chain (EP-33). `resolveRate(ctx)`. |
|
|
22
|
+
| `timeBillabilityResolver` | Server entry point to the billability chain (EP-34). `resolveBillability(ctx)`. |
|
|
23
|
+
| `timeCapacityResolver` | Server entry point to the capacity provider (EP-40). `resolveCapacity(staffMemberId, dateRange, ctx)`. |
|
|
24
|
+
| `timeOverlapPolicyResolver` | Server entry point to the overlap policy (EP-38). `evaluate(spans, ctx)`. |
|
|
25
|
+
| `timeProjectCodeResolver` | Server entry point to the project code generator (EP-39). `generate(name, taken, ctx?)`. |
|
|
26
|
+
| `timeTrackingAccessResolver` | The single project-access resolver every time-tracking route consults (board, tasks, entries, timesheets, reports). Returns `{ canManageAll, projectIds, staffMemberId }`. `canManageAll: true` (holder of `staff.timesheets.projects.manage`, wildcard-aware) means **unrestricted** — `projectIds` is then empty and MUST NOT be read as "no projects". Otherwise `projectIds` lists the projects with an active, non-deleted `staff_time_project_members` row for the caller. A caller with no staff profile is a normal outcome (`staffMemberId: null`, empty `projectIds`) that drives the no-access screen, never an error. Every query is scoped by `tenantId` + `organizationId`; a request missing either fails closed. |
|
|
20
27
|
|
|
21
|
-
Resolver
|
|
28
|
+
Resolver shapes (from `lib/availabilityAccess.ts` and `lib/time-tracking/access.ts`):
|
|
22
29
|
|
|
23
30
|
```ts
|
|
24
31
|
type AvailabilityAccessResolver = {
|
|
@@ -26,10 +33,35 @@ type AvailabilityAccessResolver = {
|
|
|
26
33
|
ctx: AvailabilityAccessContext,
|
|
27
34
|
): Promise<AvailabilityWriteAccess>
|
|
28
35
|
}
|
|
36
|
+
|
|
37
|
+
type TimeTrackingAccessResolver = {
|
|
38
|
+
resolveProjectAccess(ctx: {
|
|
39
|
+
em: EntityManager
|
|
40
|
+
userId?: string | null
|
|
41
|
+
tenantId?: string | null
|
|
42
|
+
organizationId?: string | null
|
|
43
|
+
userFeatures?: readonly string[]
|
|
44
|
+
/** The manage-all decision, resolved by the caller through `resolveFeatureAccess`. Preferred over `userFeatures`. */
|
|
45
|
+
canManageAll?: boolean
|
|
46
|
+
/** Resolved `access.assignmentGraceDays`; omitted/null falls back to the documented default (14). */
|
|
47
|
+
assignmentGraceDays?: number | null
|
|
48
|
+
/** Injectable clock for the assignment window; defaults to now. */
|
|
49
|
+
now?: Date
|
|
50
|
+
}): Promise<{ canManageAll: boolean; projectIds: string[]; staffMemberId: string | null }>
|
|
51
|
+
}
|
|
29
52
|
```
|
|
30
53
|
|
|
54
|
+
**Assignment window (spec D-12).** Membership alone is not access: a non-manager reaches a project only while `assigned_start_date <= today <= assigned_end_date + assignmentGraceDays` (a null end date is open-ended). Pass `assignmentGraceDays` from `readTimeTrackingSettings(...).access.assignmentGraceDays` — the resolver deliberately does not read `ModuleConfigService` itself, so it stays testable and a caller that already loaded settings avoids a second lookup. Both extra fields are optional and default safely; existing callers are unaffected. An unparseable date bound or an unusable clock fails **closed**.
|
|
55
|
+
|
|
31
56
|
`AvailabilityWriteAccess.unregistered?: boolean` is an additive sentinel field (BC surface #2 — STABLE) set to `true` only when staff DI is missing. Existing required fields MUST NOT be removed.
|
|
32
57
|
|
|
58
|
+
Route-side, gate a project id with `assertProjectAccess(access, projectId)` from `lib/time-tracking/access.ts` instead of re-implementing the `canManageAll` / `projectIds` check. `userFeatures` is matched with the shared `authorizeFeatures` policy, so `staff.*` and `staff.timesheets.*` grants are honoured — MUST NOT compare the raw feature array with exact string equality.
|
|
59
|
+
|
|
60
|
+
**One RBAC authority (time tracking).** Every time-tracking feature question MUST go through `resolveFeatureAccess(container, userId, features, scope)` in [`lib/time-tracking/featureAccess.ts`](./lib/time-tracking/featureAccess.ts) — the module MUST NOT hand-roll a `rbacService.getGrantedFeatures` / `userHasAllFeatures` read at a call site. It asks `userHasAllFeatures` (the call that carries `isSuperAdmin`), fails closed on every path, and logs the failure. It returns `{ allowed, grantedFeatures }`:
|
|
61
|
+
|
|
62
|
+
- `allowed` is the decision. Authorize on this, and pass it to `resolveProjectAccess` as `canManageAll` rather than letting the resolver re-derive it from an array.
|
|
63
|
+
- `grantedFeatures` is plumbing for the surfaces that take a list — approval policies, the interceptor context, `runRouteMutationGuards`. It is **always an array, never `null`**: a nullable grant list cannot say whether an empty answer means "no grants" or "could not ask", and every consumer treats an empty one the same fail-closed way. MUST NOT gate money, or anything else, on it.
|
|
64
|
+
|
|
33
65
|
### API routes (BC surface #7 — STABLE)
|
|
34
66
|
|
|
35
67
|
| Route | Owner | Notes |
|
|
@@ -38,14 +70,750 @@ type AvailabilityAccessResolver = {
|
|
|
38
70
|
|
|
39
71
|
Replaces the deprecated `GET /api/customers/assignable-staff`, which now returns `308 Permanent Redirect` and will be removed no earlier than the next major release.
|
|
40
72
|
|
|
73
|
+
#### Time tracking (`/api/staff/timesheets/*`)
|
|
74
|
+
|
|
75
|
+
The consulting suite added the routes below. The URL namespace stays `staff/timesheets` even though the pages moved to `/backend/staff/time-tracking/*` — the routes are a STABLE surface and were not renamed.
|
|
76
|
+
|
|
77
|
+
| Route | Features | Notes |
|
|
78
|
+
|-------|----------|-------|
|
|
79
|
+
| `GET/POST/PUT/DELETE /tasks`, `PATCH /tasks/[id]/status`, `GET/POST/PUT/DELETE /tasks/[id]/comments` | `tasks.view` / `tasks.manage` | Tasks are depth-1 (a subtask is a child task, D-2). Every query is intersected with `resolveProjectAccess`. `?tagIds=` narrows to tasks carrying **every** listed tag. |
|
|
80
|
+
| `GET/POST/PUT/DELETE /task-statuses` | `tasks.view` / `tasks.manage` | Per-project Kanban columns (D-1), seeded when a project is created. |
|
|
81
|
+
| `GET/POST/PUT/DELETE /tags`, `POST/DELETE /tags/{task,entry}-assignments` | `tasks.view` / `tasks.manage` | Refuses to tag an entry locked in a closed report. |
|
|
82
|
+
| `POST /time-entries/[id]/duplicate`, `POST /time-entries/copy-day`, `GET /time-entries/overlaps` | `manage_own` / `view` | Overlaps is advisory and midnight-aware (D-8); copy-day refuses a non-empty target unless `allowDuplicates`. |
|
|
83
|
+
| `GET/POST/PUT/DELETE /reports`, `POST /reports/preview`, `GET /reports/[id]/sheet`, `POST /reports/[id]/{close,unlock}`, `GET /reports/[id]/export` | `reports.view` / `reports.manage` / `reports.unlock` | Close freezes per-entry values and locks the entries; unlock requires a reason. Export never locks. |
|
|
84
|
+
| `GET /my-work` | `view` | Screen 1 aggregate; always the caller's own, no `staffMemberId` parameter exists. |
|
|
85
|
+
| `POST /access-requests` | `view` | Raises a notification instead of disclosing that a project exists. |
|
|
86
|
+
| `GET/PUT /settings` | `view` / `settings.manage` | Tenant-global (§10). Response groups: `rounding`, `defaults`, `targets`, `warnings`, `access`. |
|
|
87
|
+
| `GET /settings/rounding-impact` | `settings.manage` | Projects a candidate rounding rule over a recent window (default 90 days). Locked entries are excluded from the projection and counted separately. |
|
|
88
|
+
| `POST /settings/reapply-rounding` | `settings.manage` | Enqueues the retro-rounding `ProgressJob`; `202` with `progressJobId`, or `200` with a null id when nothing is eligible. **Never touches a locked entry.** |
|
|
89
|
+
|
|
90
|
+
Retro-rounding runs as worker `staff:timesheets-reapply-rounding` on queue `staff-time-reapply-rounding` (`workers/timesheets-reapply-rounding.ts`), driving the `staff.timesheets.time_entries.reapply_rounding` command in batches with `bulkImport.skipEvents`/`skipNotifications` so a tenant-wide restatement refreshes the query index without emitting an event per entry. That command is reachable only by a system actor or a super-admin.
|
|
91
|
+
|
|
41
92
|
### ACL feature IDs (BC surface #10 — FROZEN)
|
|
42
93
|
|
|
43
94
|
The following feature IDs are stored in role configurations and MUST NOT be renamed or removed:
|
|
44
95
|
|
|
45
96
|
- `staff.my_availability.manage`
|
|
46
97
|
- `staff.my_availability.unavailability`
|
|
98
|
+
- Time tracking: `staff.timesheets.view`, `staff.timesheets.manage_own`, `staff.timesheets.manage_all`, `staff.timesheets.projects.view`, `staff.timesheets.projects.manage`, `staff.timesheets.approve`, `staff.timesheets.lock`, `staff.timesheets.tasks.view`, `staff.timesheets.tasks.manage`, `staff.timesheets.reports.view`, `staff.timesheets.reports.manage`, `staff.timesheets.reports.unlock`, `staff.timesheets.settings.manage`, `staff.timesheets.rates.view`
|
|
47
99
|
- Other `staff.*` features declared in [`acl.ts`](./acl.ts)
|
|
48
100
|
|
|
101
|
+
Money is gated on `staff.timesheets.rates.view` and is **absent** from payloads for anyone else, not blanked — never re-add a rate or cost field to a response for a caller without it.
|
|
102
|
+
|
|
103
|
+
### Time-tracking mutation-guard `resourceKind` taxonomy (BC surface #2 — STABLE)
|
|
104
|
+
|
|
105
|
+
Every hand-rolled `/api/staff/timesheets/*` write route runs the same guard set as
|
|
106
|
+
`makeCrudRoute` (`api/guards.ts` → `runStaffMutationGuards`, which collects
|
|
107
|
+
`getAllMutationGuardInstances()` plus the bridged legacy DI service). It resolves the
|
|
108
|
+
caller's granted features itself, through `resolveFeatureAccess` — **do not pass a
|
|
109
|
+
grant list**. `runMutationGuards` drops any guard whose declared `features` the caller
|
|
110
|
+
does not hold, so supplying an empty list silently disables every feature-gated guard;
|
|
111
|
+
the deprecated `resolveUserFeatures(auth)` did exactly that, because `AuthContext` has
|
|
112
|
+
no `features` field. The
|
|
113
|
+
`resourceKind` a guard matches on is published as
|
|
114
|
+
`STAFF_TIME_TRACKING_RESOURCE_KINDS` in [`api/guards.ts`](./api/guards.ts) — import it
|
|
115
|
+
rather than re-typing a string:
|
|
116
|
+
|
|
117
|
+
| Key | `resourceKind` | Routes |
|
|
118
|
+
|-----|----------------|--------|
|
|
119
|
+
| `timeEntry` | `staff.timesheets.time_entry` | `time-entries/{bulk,copy-day,start-timer}`, `time-entries/[id]/{duplicate,timer-start,timer-stop}` |
|
|
120
|
+
| `timeEntrySegment` | `staff.timesheets.time_entry_segment` | `time-entries/[id]/segments`, `time-entries/[id]/segments/[segmentId]` |
|
|
121
|
+
| `timeTask` | `staff.timesheets.time_task` | `tasks/[id]/status` |
|
|
122
|
+
| `timeTaskComment` | `staff.timesheets.task_comment` | `tasks/[id]/comments` |
|
|
123
|
+
| `timeProject` | `staff.timesheets.time_project` | `time-projects/[id]/change-currency` |
|
|
124
|
+
| `timeProjectMember` | `staff.timesheets.time_project_member` | `my-projects/[projectId]` |
|
|
125
|
+
| `timeReport` | `staff.timesheets.time_report` | `reports/[id]/close`, `reports/[id]/unlock` |
|
|
126
|
+
| `entryTag` | `staff.timesheets.entry_tag` | `tags/entry-assignments` |
|
|
127
|
+
| `taskTag` | `staff.timesheets.task_tag` | `tags/task-assignments` |
|
|
128
|
+
| `settings` | `staff.timesheets.settings` | `settings`, `settings/reapply-rounding` |
|
|
129
|
+
| `accessRequest` | `staff.timesheets.access_request` | `access-requests` |
|
|
130
|
+
|
|
131
|
+
These are the **guard** resource kinds. The eight `makeCrudRoute` time-tracking
|
|
132
|
+
resources derive their own tag from the `events` config the factory reads
|
|
133
|
+
(`resolveResourceAliasesList`), so a guard targeting a factory route matches that
|
|
134
|
+
derived tag instead — for example `staff.timesheets.time.entry` for
|
|
135
|
+
`/api/staff/timesheets/time-entries`. `packages/core/src/modules/staff/__tests__/time-tracking-write-path-contracts.test.ts`
|
|
136
|
+
fails if a route re-types a literal or an entry above stops being used.
|
|
137
|
+
|
|
138
|
+
### Time-tracking API interceptors (BC surface #7 — STABLE)
|
|
139
|
+
|
|
140
|
+
Every hand-rolled `/api/staff/timesheets/*` route runs the same `before` and `after`
|
|
141
|
+
API interceptor passes the CRUD factory runs, through
|
|
142
|
+
[`api/timesheets/_shared/withTimesheetInterceptors.ts`](./api/timesheets/_shared/withTimesheetInterceptors.ts).
|
|
143
|
+
|
|
144
|
+
- `targetRoute` is the pathname **without** the `/api/` prefix — `staff/timesheets/time-entries/bulk`,
|
|
145
|
+
`staff/timesheets/my-work` — matching `normalizeInterceptorRoutePath` in the factory.
|
|
146
|
+
- A route with a dynamic segment carries the **concrete id** in that path, so target it
|
|
147
|
+
with the registry's prefix wildcard: `staff/timesheets/time-entries/*`, never a literal
|
|
148
|
+
containing `[id]`.
|
|
149
|
+
- The before pass can rewrite the body (mutations) or the query (read aggregates) and can
|
|
150
|
+
deny with its own status; the after pass shapes the JSON body. `reports/[id]/export`
|
|
151
|
+
answers with bytes, so its after pass shapes a **descriptor** instead —
|
|
152
|
+
`filename` and `contentType` are read back and applied to the download headers.
|
|
153
|
+
- The interceptor context always carries `tenantId` + `organizationId` and fails closed
|
|
154
|
+
with `400` when either is missing. `/settings` and `/settings/reapply-rounding` are
|
|
155
|
+
tenant-global and opt in with `tenantGlobal: true`.
|
|
156
|
+
|
|
157
|
+
`__tests__/time-tracking-write-path-contracts.test.ts` fails if one of the routes drops
|
|
158
|
+
the wiring.
|
|
159
|
+
|
|
160
|
+
### Time-tracking response enricher hosts (BC surface #2 — STABLE)
|
|
161
|
+
|
|
162
|
+
Declared in [`data/enrichers.ts`](./data/enrichers.ts). Register your own enricher against
|
|
163
|
+
the same `targetEntity` and it composes with these rather than replacing them.
|
|
164
|
+
|
|
165
|
+
| `targetEntity` | Enricher id | Adds |
|
|
166
|
+
|-----|-----|-----|
|
|
167
|
+
| `staff:staff_time_project` | `staff.timesheets-projects-portfolio` | `_staff`: hours trend, financials, member preview, customer name |
|
|
168
|
+
| `staff:staff_time_task` | `staff.timesheets-tasks-rollup` | `ownMinutes`, `loggedMinutes`, `childCount`, `doneChildCount` (top level, by spec) |
|
|
169
|
+
| `staff:staff_time_task` | `staff.timesheets-tasks-tags` | `tagIds`, `tags` |
|
|
170
|
+
| `staff:staff_time_task` | `staff.timesheets-tasks-context` | `_staff`: project name/code/colour, status, assignee |
|
|
171
|
+
| `staff:staff_time_entry` | `staff.timesheets-time-entries` | `description` alias, `roundedMinutes`, `isLocked`, `lockedReportId`, `tags`, plus `cost`/`currencyCode` |
|
|
172
|
+
| `staff:staff_time_report` | `staff.timesheets-reports` | `_staff`: freeze state, frozen entry count, export history, totals |
|
|
173
|
+
|
|
174
|
+
Money (`hourlyRate`, `cost`, `currencyCode`, `totalAmount`) is **added** for a holder of
|
|
175
|
+
`staff.timesheets.rates.view` and absent for everyone else — never blanked.
|
|
176
|
+
|
|
177
|
+
Each of the six is also published under a `<id>.query-engine` alias whose `targetEntity`
|
|
178
|
+
is the **dot** form (`staff.staff_time_entry`) with `queryEngine: { enabled: true }`. The
|
|
179
|
+
CRUD factory looks an enricher up by the colon form a route declares; the query engine
|
|
180
|
+
looks it up by `entityIdToEventEntity(entity)`, which is the dot form — so an enricher
|
|
181
|
+
published only under the colon form never runs in a query pipeline. The two lookups never
|
|
182
|
+
both match, so nothing runs twice.
|
|
183
|
+
|
|
184
|
+
### Time-tracking events: browser and webhooks (BC surface #5 — FROZEN ids)
|
|
185
|
+
|
|
186
|
+
- **Browser.** `clientBroadcast: true` is set on `time_entry.{created,updated,deleted,timer_started,timer_stopped}`,
|
|
187
|
+
`time_report.{closed,unlocked}`, `time_project.budget_threshold_reached` and
|
|
188
|
+
`time_task.status_changed`. The DOM Event Bridge filters only by tenant + organization —
|
|
189
|
+
**no feature check** — so a broadcast payload MUST NOT carry a rate, a cost or an amount.
|
|
190
|
+
`time_report.closed` therefore carries minute totals but no `totalAmount`, and
|
|
191
|
+
`time_project.budget_threshold_reached` carries `budgetValue`/`usedValue` only for an
|
|
192
|
+
`hours` budget. **Money is not the only thing that audience rules out.** A broadcast
|
|
193
|
+
payload also carries no operator free text and no customer identity:
|
|
194
|
+
`time_report.unlocked` omits its mandatory `reason` (2000 characters of prose about a
|
|
195
|
+
client's billing, kept on the `StaffTimeReportEvent` audit row instead) and
|
|
196
|
+
`time_report.closed` omits `customerId`. `reference` and the minute totals stay —
|
|
197
|
+
stable identifiers and non-money aggregates the published webhook contract and the
|
|
198
|
+
live reports screen both need, and the DOM bridge can narrow an audience only by user
|
|
199
|
+
or role id, which this module deliberately does not gate on.
|
|
200
|
+
`__tests__/timeTrackingEventPayloads.test.ts` fails if a forbidden field reappears.
|
|
201
|
+
- **Webhooks.** There is no per-event registration: `packages/webhooks` subscribes with
|
|
202
|
+
`event: '*'` and matches each id against a webhook's subscribed patterns, so declaring
|
|
203
|
+
the event in `events.ts` IS its registration. Delivery needs `tenantId` in the payload —
|
|
204
|
+
the dispatcher drops anything without one. The payload contract every subscriber codes
|
|
205
|
+
against is [`events.payloads.ts`](./events.payloads.ts); `__tests__/timeTrackingEventPayloads.test.ts`
|
|
206
|
+
fails if a declared `staff.timesheets.*` id has no schema or a schema without `tenantId`.
|
|
207
|
+
|
|
208
|
+
### Customer-portal time reports (EP-50 — BC surface #7, STABLE)
|
|
209
|
+
|
|
210
|
+
| Route / page | Guard | Notes |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| `GET /api/staff/portal/time-reports` | portal session + `portal.time_reports.view` | Closed reports of the signed-in customer only |
|
|
213
|
+
| `GET /api/staff/portal/time-reports/{id}` | same | A report of another customer answers `404`, never `403` |
|
|
214
|
+
| `/{orgSlug}/portal/time-reports` | `requireCustomerAuth` + `requireCustomerFeatures: ['portal.time_reports.view']` | Listed in the portal sidebar (`nav.group: 'main'`) |
|
|
215
|
+
| `/{orgSlug}/portal/time-reports/{id}` | same | Hosts `portal:staff.time_report:{before,after}` |
|
|
216
|
+
|
|
217
|
+
**The ownership check is four predicates in one WHERE clause, and all four come
|
|
218
|
+
from the session:**
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
tenant_id = auth.tenantId
|
|
222
|
+
organization_id = auth.orgId
|
|
223
|
+
customer_id = auth.customerEntityId -- customer_users.customer_entity_id
|
|
224
|
+
status = 'closed' AND deleted_at IS NULL
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`customerEntityId` is the FK into `customers:customer_entity`, the same id
|
|
228
|
+
`staff_time_reports.customer_id` holds (the link EP-44 declares). A portal session
|
|
229
|
+
without one is refused with `403` rather than shown an unscoped list. The detail
|
|
230
|
+
route loads the row **with** the predicates rather than loading it and checking
|
|
231
|
+
afterwards, so a foreign id is never read even momentarily. The clause is written
|
|
232
|
+
once, in [`lib/time-tracking/portalReports.ts`](./lib/time-tracking/portalReports.ts),
|
|
233
|
+
and pinned by `api/portal/time-reports/__tests__/route.test.ts`.
|
|
234
|
+
|
|
235
|
+
**Money is structurally absent on this surface, not conditionally hidden.**
|
|
236
|
+
`staff.timesheets.rates.view` is a *staff* feature graded by `rbacService`; a
|
|
237
|
+
portal identity is graded by `CustomerRbacService` against the disjoint `portal.*`
|
|
238
|
+
namespace and can never hold it. So the portal response schemas carry no rate,
|
|
239
|
+
cost, amount or currency field at all — the SQL does not select
|
|
240
|
+
`frozen_rate_amount` or `frozen_amount`, and a test fails if a money-shaped key
|
|
241
|
+
appears in a response body.
|
|
242
|
+
|
|
243
|
+
`portal.time_reports.view` is granted to the `buyer` and `viewer` customer roles
|
|
244
|
+
through `setup.defaultCustomerRoleFeatures`. It is deliberately **not** in
|
|
245
|
+
[`acl.ts`](./acl.ts) — that file is the staff feature catalog.
|
|
246
|
+
|
|
247
|
+
**Staff takes no static dependency on `customer_accounts`.** The routes resolve
|
|
248
|
+
the portal identity with a dynamic `import()` and answer `401` if it fails, and
|
|
249
|
+
`lib/time-tracking/portalRecipients.ts` reads `customer_users` by table name
|
|
250
|
+
through Kysely rather than importing an entity. `requires` in
|
|
251
|
+
[`index.ts`](./index.ts) is unchanged.
|
|
252
|
+
|
|
253
|
+
### The portal event mirror (EP-06 — BC surface #5, FROZEN id)
|
|
254
|
+
|
|
255
|
+
`staff.timesheets.time_report.portal_published` is `portalBroadcast: true` and
|
|
256
|
+
`excludeFromTriggers: true`. `time_report.closed` and `.exported` are **not**
|
|
257
|
+
portal-broadcast, and that is a security decision rather than an omission: the
|
|
258
|
+
portal SSE stream (`customer_accounts/api/portal/events/stream.ts`) filters a
|
|
259
|
+
broadcast by tenant + organization and narrows to named people only when the
|
|
260
|
+
payload carries `recipientUserIds`. One organization serves many customers, so
|
|
261
|
+
flagging `time_report.closed` — which carries `reference` and the minute totals —
|
|
262
|
+
would give every client a feed of every other client's reports.
|
|
263
|
+
That event is also `clientBroadcast: true` with a published webhook payload, so it
|
|
264
|
+
cannot be narrowed without breaking its backoffice consumers.
|
|
265
|
+
|
|
266
|
+
[`subscribers/time-report-portal-broadcast.ts`](./subscribers/time-report-portal-broadcast.ts)
|
|
267
|
+
is the only emitter. It resolves the portal users of the report's own customer and
|
|
268
|
+
**does not emit at all** when that list is empty — the same rule
|
|
269
|
+
`warranty_claims/AGENTS.md` states for its portal event. The payload carries the
|
|
270
|
+
reference and the period; no money, because SSE applies no feature check.
|
|
271
|
+
Subscribe on the portal with `usePortalAppEvent`.
|
|
272
|
+
|
|
273
|
+
### Time-tracking sync lifecycle subscribers (BC surface #5 — STABLE)
|
|
274
|
+
|
|
275
|
+
The seven `makeCrudRoute` time-tracking resources declare an `events:` config, which is
|
|
276
|
+
the only thing `deriveLifecycleEventIds` (`shared/lib/crud/factory.ts`) needs to resolve —
|
|
277
|
+
so `runSyncBeforeEvent` / `runSyncAfterEvent` dispatch on every create, update and delete
|
|
278
|
+
they serve. A sync subscriber runs **inside** the write pipeline: it can veto the write
|
|
279
|
+
before the command executes, and rewrite the payload the command receives.
|
|
280
|
+
|
|
281
|
+
The ids are `<events.module>.<events.entity>.<phase>`, with phases `creating|created`,
|
|
282
|
+
`updating|updated`, `deleting|deleted` (`LIFECYCLE_ACTION_MAP`, `factory.ts:631`):
|
|
283
|
+
|
|
284
|
+
| Route | Entity | `before` phases | `after` phases |
|
|
285
|
+
|-------|--------|-----------------|----------------|
|
|
286
|
+
| `/time-entries` | `staff.timesheets.time_entry` | `.creating` `.updating` `.deleting` | `.created` `.updated` `.deleted` |
|
|
287
|
+
| `/time-projects` | `staff.timesheets.time_project` | same three | same three |
|
|
288
|
+
| `/time-projects/[id]/employees` | `staff.timesheets.time_project_member` | same three | same three |
|
|
289
|
+
| `/tasks` | `staff.timesheets.time_task` | same three | same three |
|
|
290
|
+
| `/task-statuses` | `staff.timesheets.time_task_status` | same three | same three |
|
|
291
|
+
| `/tags` | `staff.timesheets.time_tag` | same three | same three |
|
|
292
|
+
| `/reports` | `staff.timesheets.time_report` | same three | same three |
|
|
293
|
+
|
|
294
|
+
`/tasks/[id]/comments` declares no `events:` config and therefore dispatches no sync
|
|
295
|
+
lifecycle event. `__tests__/time-tracking-write-path-contracts.test.ts` derives this table
|
|
296
|
+
from the `events` configs and fails if the two disagree.
|
|
297
|
+
|
|
298
|
+
**Declaring one.** A subscriber file under `subscribers/` exporting
|
|
299
|
+
`metadata = { event, sync: true, priority?, id? }` plus a default handler. `event` accepts
|
|
300
|
+
the wildcards `matchWildcardPattern` supports, so `staff.timesheets.*.creating` covers the
|
|
301
|
+
whole family. `priority` sorts **ascending** and defaults to `50` — a lower number runs
|
|
302
|
+
earlier, and an earlier subscriber's `modifiedPayload` is visible to the next one.
|
|
303
|
+
|
|
304
|
+
**What a handler may return** (`SyncCrudEventResult`):
|
|
305
|
+
|
|
306
|
+
- `undefined` — allow, change nothing.
|
|
307
|
+
- `{ ok: false, status?, message?, body? }` — **veto**. The route answers with that status
|
|
308
|
+
(default `422`, body defaults to `{ error: message, subscriberId }`) and the command
|
|
309
|
+
never runs. Before-phase only.
|
|
310
|
+
- `{ modifiedPayload }` — shallow-merged into the mutation payload.
|
|
311
|
+
|
|
312
|
+
**Four things the types do not tell you.**
|
|
313
|
+
|
|
314
|
+
1. On these routes the payload is the **mapped command input**, not the request body:
|
|
315
|
+
`mapInput` (`parseScopedCommandInput`) has already run, so field names are the
|
|
316
|
+
validator's camelCase, dates are coerced, and `tenantId` / `organizationId` are present.
|
|
317
|
+
2. `modifiedPayload` is merged into that input and handed straight to the command — it is
|
|
318
|
+
**not** re-validated against the create/update schema. A subscriber can therefore write
|
|
319
|
+
a field the schema would have rejected; validate your own contribution.
|
|
320
|
+
3. The **delete** phase is narrower: its payload carries no mutation data at all
|
|
321
|
+
(`payload` is `undefined`) and a `modifiedPayload` is ignored. Delete subscribers can
|
|
322
|
+
veto, not rewrite.
|
|
323
|
+
4. The `after` ids are the same strings as the async CRUD events in `events.ts`, and
|
|
324
|
+
bootstrap registers a `sync: true` subscriber in **both** registries
|
|
325
|
+
(`bootstrap.ts` → `registerModuleSubscribers` and `registerSyncSubscribers`), so an
|
|
326
|
+
after-phase handler is invoked twice: once in-pipeline, once from the event bus. Put
|
|
327
|
+
in-pipeline work on the `before` phase and plain reactions on an ordinary async
|
|
328
|
+
subscriber. The `before` ids are never emitted on the bus and need no `events.ts` entry.
|
|
329
|
+
|
|
330
|
+
Proved end to end in `__tests__/time-tracking-sync-subscribers.test.ts`.
|
|
331
|
+
|
|
332
|
+
## Host extension points
|
|
333
|
+
|
|
334
|
+
Declared in [`extension-points.ts`](./extension-points.ts) and emitted into the generated
|
|
335
|
+
extension-point catalog and module-facts by `yarn generate` + the CLI build. Every id below
|
|
336
|
+
is a **FROZEN** contract surface (BC surface #6): a third-party widget names it verbatim, so
|
|
337
|
+
renaming one silently unbinds every contribution aimed at it. `__tests__/timeTrackingExtensionHosts.test.ts`
|
|
338
|
+
pins the whole list, verifies each declaration's `source` file exists and references
|
|
339
|
+
`extensionPoints.hosts.<key>` (an unreferenced declaration is emitted as an
|
|
340
|
+
`unbound-declaration` diagnostic, not a usable host).
|
|
341
|
+
|
|
342
|
+
**Two id conventions meet here and neither is negotiable.** `crud-form:` hosts carry the
|
|
343
|
+
**dot** form of the entity id (`staff.staff_time_project`) because `CrudForm` derives its
|
|
344
|
+
own spot id from `entityIds` by replacing every colon with a dot — the colon form would
|
|
345
|
+
name a spot no widget is ever loaded for. `detail:` hosts carry the **colon** form
|
|
346
|
+
(`detail:staff:staff_time_task:header`); nothing derives those, the host picks them.
|
|
347
|
+
|
|
348
|
+
### DataTable hosts
|
|
349
|
+
|
|
350
|
+
| Table id | Screen |
|
|
351
|
+
|---|---|
|
|
352
|
+
| `staff.time_entries.list` | `backend/staff/time-tracking/entries/page.tsx` |
|
|
353
|
+
| `staff.time_projects.list` | `backend/staff/time-tracking/projects/page.tsx` |
|
|
354
|
+
| `staff.time_reports.list` | `backend/staff/time-tracking/reports/page.tsx` |
|
|
355
|
+
|
|
356
|
+
Each derives the nine standard surfaces — `data-table:<id>:{columns,row-actions,bulk-actions,filters,toolbar,search-trailing,header,footer,empty-state}`.
|
|
357
|
+
|
|
358
|
+
### CrudForm hosts
|
|
359
|
+
|
|
360
|
+
| Spot id | Host |
|
|
361
|
+
|---|---|
|
|
362
|
+
| `crud-form:staff.staff_time_project` | `backend/staff/time-tracking/projects/{create,[id]/edit}/page.tsx` (real `CrudForm`) |
|
|
363
|
+
| `crud-form:staff.staff_time_entry` | `lib/time-tracking-ui/TimeEntryDialog.tsx` (hand-rolled host) |
|
|
364
|
+
| `crud-form:staff.staff_time_task` | `lib/time-tracking-ui/NewTaskDialog.tsx` (hand-rolled host) |
|
|
365
|
+
| `crud-form:staff.staff_time_report` | `backend/staff/time-tracking/reports/create/page.tsx` (hand-rolled host) |
|
|
366
|
+
|
|
367
|
+
Each carries the standard child spots (`:before-fields`, `:fields`, `:after-fields`,
|
|
368
|
+
`:footer`, `:group:<groupId>`, `:field:<fieldId>:{before,after}`, …).
|
|
369
|
+
|
|
370
|
+
The project form's group ids are a published contract — `PROJECT_FORM_GROUP_IDS` in
|
|
371
|
+
[`backend/staff/time-tracking/projects/projectFormConfig.ts`](./backend/staff/time-tracking/projects/projectFormConfig.ts):
|
|
372
|
+
`basics`, `billing`, `budget`, `status`, `team`, `rounding`, `details` (a `compact` host
|
|
373
|
+
renders `basics` + `billing` only). Address one with `CrudFormInjectionSpots.group(...)`.
|
|
374
|
+
|
|
375
|
+
The three hand-rolled hosts are not `CrudForm`s; they replay its contract by hand. What
|
|
376
|
+
they support and what they do not:
|
|
377
|
+
|
|
378
|
+
| Lifecycle | TimeEntryDialog | NewTaskDialog | Report create |
|
|
379
|
+
|---|---|---|---|
|
|
380
|
+
| `onFieldChange` (value + `sideEffects` + messages written back) | yes | no | no |
|
|
381
|
+
| `onBeforeSave` (`ok: false` blocks the write, `message` is flashed, `fieldErrors` map onto the task/duration fields, `requestHeaders` are merged into the request **under** the optimistic-lock header) | yes | `ok`/`message` only | no |
|
|
382
|
+
| `onAfterSave` | yes | yes | no |
|
|
383
|
+
| `transformFormData` / delete lifecycle | no | no | no |
|
|
384
|
+
|
|
385
|
+
Pinned by `lib/time-tracking-ui/__tests__/TimeEntryDialog.test.tsx` →
|
|
386
|
+
"crud-form host lifecycle".
|
|
387
|
+
|
|
388
|
+
### Injection spots
|
|
389
|
+
|
|
390
|
+
| Spot id | Host | Context |
|
|
391
|
+
|---|---|---|
|
|
392
|
+
| `portal:staff.time_report:{before,after}` | `frontend/[orgSlug]/portal/time-reports/[id]/page.tsx` | `{ orgSlug, reportId, reference, periodFrom, periodTo, resolvedFeatures }` — a **customer** session, never a staff one, and no money |
|
|
393
|
+
| `detail:staff:staff_time_project:{header,status-badges,tabs,sidebar,footer}` | `backend/staff/time-tracking/projects/[id]/page.tsx` | `{ entityId, recordId, projectId, path, retryLastMutation }` |
|
|
394
|
+
| `detail:staff:staff_time_task:{header,status-badges,tabs,sidebar,footer}` | `lib/time-tracking-ui/TaskDrawer.tsx` | `{ entityId, recordId, taskId, timeProjectId, retryLastMutation }` |
|
|
395
|
+
| `detail:staff:staff_time_report:{header,status-badges,footer}` | `backend/staff/time-tracking/reports/[id]/page.tsx` | `{ entityId, recordId, reportId, reference, isClosed, periodFrom, periodTo, retryLastMutation }` |
|
|
396
|
+
| `staff.time_report.sheet:{before-lines,after-totals}` | `lib/time-tracking-ui/ReportSheet.tsx` | `{ entityId, recordId, reportId, reference, periodLabel, currencyCode }` |
|
|
397
|
+
| `staff.timesheet:toolbar` | `backend/staff/time-tracking/timesheet/page.tsx` | `{ staffMemberId, periodKind, periodFrom, periodTo, view, readOnly, retryLastMutation }` |
|
|
398
|
+
| `staff.timesheet:period-footer` | `lib/time-tracking-ui/TimesheetPeriodFooter.tsx` | `{ workingDays, dailyHours }` |
|
|
399
|
+
| `staff.timesheet:day-cell-actions` | `lib/time-tracking-ui/TimesheetCalendar.tsx` | `{ date, isToday, isWeekend, totalMinutes }` |
|
|
400
|
+
| `staff.time_task.board:toolbar` | `lib/time-tracking-ui/KanbanBoard.tsx` | `{ timeProjectId, projectName, statusIds }` |
|
|
401
|
+
| `staff.time_task.board:column-header` | `lib/time-tracking-ui/KanbanColumn.tsx` | `{ statusId, statusName, isDone, total, loggedMinutes }` |
|
|
402
|
+
| `staff.time_task.board:{card-badges,card-footer}` | `lib/time-tracking-ui/KanbanCard.tsx` | `{ entityId, recordId, taskId, timeProjectId, taskStatusId, timerRunning }` |
|
|
403
|
+
| `staff.my_work:{before-sections,after-sections}` | `backend/staff/time-tracking/page.tsx` | `{ staffMemberId, today, projectIds, retryLastMutation }` |
|
|
404
|
+
| `staff.time_tracking.settings:sections` | `backend/staff/time-tracking/settings/page.tsx` | `{ moduleId, canManage }` |
|
|
405
|
+
| `staff.timesheets.timer-bar:actions` | `lib/timesheets-ui/TimerBar.tsx` | `{ staffMemberId, retryLastMutation }` |
|
|
406
|
+
|
|
407
|
+
No context carries a rate, cost or amount; contributions that need money must ask for it
|
|
408
|
+
behind `staff.timesheets.rates.view` themselves. An empty spot renders nothing and changes
|
|
409
|
+
no DOM — pinned by `__tests__/timeTrackingInjectionSpots.test.ts`.
|
|
410
|
+
|
|
411
|
+
**Tabs.** `detail:staff:staff_time_project:tabs` is a real tab host: a widget mapped to it
|
|
412
|
+
with `placement: { kind: 'tab', groupId, groupLabel, priority }` gets its own
|
|
413
|
+
`TabsTrigger` and its own `TabsContent` in the page's `<Tabs>` (higher `priority` first,
|
|
414
|
+
`metadata.title` as the label fallback). `detail:staff:staff_time_task:tabs` is **not** —
|
|
415
|
+
the task drawer is a single-column stack with no tab strip, so the spot renders a
|
|
416
|
+
contributed widget as an additional panel at the end of the drawer body.
|
|
417
|
+
|
|
418
|
+
**Server-side "my work" sections (deferred).** `staff.my_work:{before,after}-sections` are
|
|
419
|
+
client render spots only. The spec's server-side section-contribution contract — a module
|
|
420
|
+
adding its own section to `api/timesheets/my-work/myWorkAggregate.ts` — is **not**
|
|
421
|
+
implemented: the aggregate returns one closed, zod-validated shape that the page's KPI
|
|
422
|
+
strip, quick-entry targets and totals all read positionally, so a contributed section
|
|
423
|
+
needs its own registry, its own per-section scoping and its own response slot before it can
|
|
424
|
+
be added without loosening that contract. A client spot fetching its own data covers the
|
|
425
|
+
same use case today.
|
|
426
|
+
|
|
427
|
+
### Replaceable components
|
|
428
|
+
|
|
429
|
+
`replace` / `wrapper` / `props` targets, registered by each component's own module through
|
|
430
|
+
`registerComponent` and resolved with `useRegisteredComponent`. Every one publishes a zod
|
|
431
|
+
`propsSchema`, which `useRegisteredComponent` parses in development — a replacement that
|
|
432
|
+
does not satisfy it falls back to the original component. Catalogued in
|
|
433
|
+
[`widgets/components.ts`](./widgets/components.ts).
|
|
434
|
+
|
|
435
|
+
| Handle | Component |
|
|
436
|
+
|---|---|
|
|
437
|
+
| `staff.time_entry_dialog` | `lib/time-tracking-ui/TimeEntryDialog.tsx` |
|
|
438
|
+
| `staff.timer_bar` | `lib/timesheets-ui/TimerBar.tsx` |
|
|
439
|
+
| `staff.kanban_card` | `lib/time-tracking-ui/KanbanCard.tsx` |
|
|
440
|
+
| `staff.kanban_column` | `lib/time-tracking-ui/KanbanColumn.tsx` |
|
|
441
|
+
| `staff.timesheet_grid` | `backend/staff/time-tracking/timesheet/GridView.tsx` |
|
|
442
|
+
| `staff.timesheet_list` | `lib/timesheets-ui/ListView.tsx` |
|
|
443
|
+
| `staff.timesheet_calendar` | `lib/time-tracking-ui/TimesheetCalendar.tsx` |
|
|
444
|
+
| `staff.report_sheet` | `lib/time-tracking-ui/ReportSheet.tsx` |
|
|
445
|
+
| `staff.project_card` | `lib/timesheets-projects-ui/ProjectCard.tsx` |
|
|
446
|
+
| `staff.entries_summary_footer` | `lib/time-tracking-ui/TimeEntriesSummaryFooter.tsx` |
|
|
447
|
+
|
|
448
|
+
## Time-tracking strategy registries
|
|
449
|
+
|
|
450
|
+
EP-32…EP-41. Ten closed pure functions and DB enums are now registries. **Every one
|
|
451
|
+
registers the module's existing implementation as its built-in default at module load,
|
|
452
|
+
so with no contribution the observable behaviour is the one the module shipped.** The
|
|
453
|
+
built-in ids below are the `runtimeContract` of the matching `specialized-registry` host
|
|
454
|
+
in [`extension-points.ts`](./extension-points.ts).
|
|
455
|
+
|
|
456
|
+
| EP | Registry id / host | Register with | Built-in default | Resolution |
|
|
457
|
+
|---|---|---|---|---|
|
|
458
|
+
| 32 | `staff.time_tracking.rounding` | `registerTimeRoundingStrategy({ id, labelKey, priority?, round(raw, ctx) })` — [`lib/time-tracking/rounding.ts`](./lib/time-tracking/rounding.ts) | `staff.time_tracking.rounding.unit` (`up`/`nearest` × `0\|5\|10\|15`) | single winner |
|
|
459
|
+
| 33 | `staff.time_tracking.rate` | `registerTimeRateResolver({ id, priority?, resolve(ctx): number \| null })` — [`lib/time-tracking/cost.ts`](./lib/time-tracking/cost.ts) | `staff.time_tracking.rate.entry_override_then_project` | chain, first non-null |
|
|
460
|
+
| 34 | `staff.time_tracking.billability` | `registerBillabilityResolver({ id, priority?, resolve(ctx): boolean \| null })` — [`lib/time-tracking/billability.ts`](./lib/time-tracking/billability.ts) | `staff.time_tracking.billability.project_then_tenant` | chain, first non-null |
|
|
461
|
+
| 35 | `staff.time_tracking.report_export_format` | `registerReportExportFormat({ id, labelKey, mimeType, extension, serialize(input) })` — [`lib/timesheets-reports/reportExportFormats.ts`](./lib/timesheets-reports/reportExportFormats.ts) | `pdf`, `csv`, `xlsx` | keyed by id |
|
|
462
|
+
| 36 | `staff.time_tracking.report_grouping` | `registerReportGrouping({ id, labelKey, groupOf(entry), labelOf(key, ctx), sort })` — [`lib/timesheets-reports/reportGroupings.ts`](./lib/timesheets-reports/reportGroupings.ts) | `project_task`, `project_person`, `project_day` | keyed by id |
|
|
463
|
+
| 37 | `staff.time_tracking.time_entry_source` | `registerTimeEntrySource({ id, labelKey, icon, editable })` — [`lib/time-tracking/timeEntrySources.ts`](./lib/time-tracking/timeEntrySources.ts) | `manual`, `timer`, `kiosk`, `mobile` | keyed by id |
|
|
464
|
+
| 38 | `staff.time_tracking.overlap_policy` | `registerOverlapPolicy({ id, priority?, evaluate(spans, ctx) })` — [`lib/time-tracking/overlap.ts`](./lib/time-tracking/overlap.ts) | `staff.time_tracking.overlap.warn_when_enabled` | max severity |
|
|
465
|
+
| 39 | `staff.time_tracking.project_code_generator` | `registerProjectCodeGenerator({ id, priority?, generate(name, taken, ctx) })` — [`lib/time-tracking/projectCode.ts`](./lib/time-tracking/projectCode.ts) | `staff.time_tracking.project_code.initials` | single winner |
|
|
466
|
+
| 40 | `staff.time_tracking.capacity_provider` | `registerCapacityProvider({ id, priority?, resolve(staffMemberId, dateRange, ctx) })` — [`lib/time-tracking/capacity.ts`](./lib/time-tracking/capacity.ts) | `staff.time_tracking.capacity.flat_daily_hours` | single winner |
|
|
467
|
+
| 41 | `staff.time_tracking.report_approval_policy` | `registerReportApprovalPolicy({ id, priority?, canClose?, canUnlock?, onClosed? })` — [`lib/timesheets-reports/reportApprovalPolicies.ts`](./lib/timesheets-reports/reportApprovalPolicies.ts) | `staff.time_tracking.report_approval.acl_only` | conjunction, first refusal |
|
|
468
|
+
| 42 | `staff.time_tracking.setting_key` | `registerTimeTrackingSettingKey({ group, key, schema, default, labelKey, priority? })` — [`lib/time-tracking/settingKeys.ts`](./lib/time-tracking/settingKeys.ts) | the eight frozen keys | keyed by `<group>.<key>` |
|
|
469
|
+
| 51 | `staff.time_tracking.recalculation` | `registerTimeTrackingRecalculation({ id, labelKey, priority?, run(ctx) })` — [`lib/time-tracking/recalculations.ts`](./lib/time-tracking/recalculations.ts) | `staff.time_tracking.recalculation.rounding` | keyed by id, run in order |
|
|
470
|
+
|
|
471
|
+
### The four resolution orders
|
|
472
|
+
|
|
473
|
+
All four order candidates by **descending `priority` (default `0`), ties by registration
|
|
474
|
+
order**, and every built-in registers at `BUILT_IN_STRATEGY_PRIORITY` (`-1000`) so it is
|
|
475
|
+
always the last candidate. They differ only in what they do with that order:
|
|
476
|
+
|
|
477
|
+
- **single winner** — the first candidate runs; nothing else is consulted.
|
|
478
|
+
- **chain, first non-null** — candidates are asked in order and the first non-`null`
|
|
479
|
+
answer wins. Returning `null` is an abstention, not a "no", so an unopinionated
|
|
480
|
+
contribution cannot change an entry.
|
|
481
|
+
- **max severity** (`block` > `warn` > `allow`) — a contributed overlap policy can only
|
|
482
|
+
ESCALATE. Nothing can suppress a warning the tenant asked for.
|
|
483
|
+
- **conjunction, first refusal** — every approval policy must agree. `canClose`/`canUnlock`
|
|
484
|
+
return a refusal or nothing, **never a grant**, so no policy can open a door the ACL
|
|
485
|
+
closed. The ACL check in the close/unlock routes runs first and unconditionally.
|
|
486
|
+
|
|
487
|
+
### A built-in is a separate slot, and a contribution's answer is not trusted
|
|
488
|
+
|
|
489
|
+
Two rules the registry itself enforces, so a call site cannot get them wrong:
|
|
490
|
+
|
|
491
|
+
- **`register()` refuses a built-in id and no disposer can remove a built-in.** The
|
|
492
|
+
eleven built-in ids are published above, and the map used to be last-writer-wins:
|
|
493
|
+
registering `{ id: 'staff.time_tracking.rounding.unit', … }` made you the built-in,
|
|
494
|
+
put you on the unscoped path the fail-closed gate exists to keep byte-identical,
|
|
495
|
+
and let your disposer delete the real built-in for the life of the process. Built-ins
|
|
496
|
+
now live in a map `register()` cannot reach (`registerBuiltIn`, module-load only),
|
|
497
|
+
and `selectScopedStrategy` takes the built-in **by reference** rather than looking
|
|
498
|
+
its id up among the candidates.
|
|
499
|
+
- **Every strategy invocation is wrapped, and every numeric answer is clamped.**
|
|
500
|
+
`registries/invoke.ts` publishes `runStrategy` (single-winner: fall back to the
|
|
501
|
+
built-in), `tryStrategy` (chain: skip this candidate, ask the next) and
|
|
502
|
+
`clampToStoredMinutes`. `roundMinutes` clamps to `Math.max(0, Math.round(v))` or
|
|
503
|
+
falls back — `rounded_minutes` is an `integer` column and the sole input to cost, and
|
|
504
|
+
the obvious contributed strategy (`Math.floor(raw / ctx.settings.unitMinutes) * …`)
|
|
505
|
+
is `NaN` under the shipped default `unitMinutes: 0`, which stores garbage and then
|
|
506
|
+
bills 0.00 in silence. `CapacityProvider.resolve` and `ProjectCodeGenerator.generate`
|
|
507
|
+
validate their answers the same way. Two deliberate exceptions: a failing export
|
|
508
|
+
`serialize` re-raises as an internal error naming the format (falling back would
|
|
509
|
+
answer with another format's bytes under the requested MIME type), and a throwing
|
|
510
|
+
`canClose`/`canUnlock` becomes a **refusal** — those are gates, and the built-in
|
|
511
|
+
grants, so falling back to it would turn a broken policy into permission.
|
|
512
|
+
|
|
513
|
+
### Scoping — fail closed to the built-in
|
|
514
|
+
|
|
515
|
+
EP-42 is the one exception to the paragraph below: settings are tenant-global by spec
|
|
516
|
+
§10, so its host declares `scopeContract: 'tenant'` and a contributed key is stored with
|
|
517
|
+
`{ tenantId }` and nothing else.
|
|
518
|
+
|
|
519
|
+
Every resolver context carries `tenantId` + `organizationId`
|
|
520
|
+
([`lib/time-tracking/registries/scope.ts`](./lib/time-tracking/registries/scope.ts)). A
|
|
521
|
+
**contributed** strategy is consulted only when both are present; with either missing the
|
|
522
|
+
built-in is the only candidate. Failing closed lands on the built-in rather than on an
|
|
523
|
+
error, because the built-in is the same pure code that ran before the registries existed.
|
|
524
|
+
|
|
525
|
+
### Client-side call sites
|
|
526
|
+
|
|
527
|
+
The registries are plain module-scope `Map`s (the `registerPaymentProvider` idiom), not DI
|
|
528
|
+
singletons, so the browser bundle reaches them by importing the `register*` function
|
|
529
|
+
directly — no container. The DI keys in the table above are the *server* entry point and
|
|
530
|
+
exist so an app can replace a resolver through `entry.overrides` DI; resolving one and
|
|
531
|
+
calling the module function are equivalent.
|
|
532
|
+
|
|
533
|
+
Four call sites run in the browser and none of them has a tenant id to hand, so all four
|
|
534
|
+
resolve the built-in today, byte-identically to before:
|
|
535
|
+
`lib/time-tracking-ui/TimeEntryDialog.tsx` (rate + cost preview),
|
|
536
|
+
`lib/time-tracking-ui/timeTrackingSettingsForm.ts` (`buildRoundingExamples`),
|
|
537
|
+
`lib/time-tracking-ui/ProjectCodeField.tsx` (`deriveProjectCode`), and
|
|
538
|
+
`lib/time-tracking/roundingImpact.ts`. A client host that wants a contribution to apply
|
|
539
|
+
must pass the scope explicitly.
|
|
540
|
+
|
|
541
|
+
### Schema
|
|
542
|
+
|
|
543
|
+
EP-37 and EP-36 replaced two database enums with registry-backed validation:
|
|
544
|
+
`staff_time_entries.source` and `staff_time_reports.grouping` are plain `text` columns
|
|
545
|
+
(`Migration20260824143357_staff.ts` drops `staff_time_entries_source_check` and
|
|
546
|
+
`staff_time_reports_grouping_check`). The write-side guard moved into
|
|
547
|
+
`data/validators.ts`, which asks the registries rather than a literal `z.enum`, and
|
|
548
|
+
`normalizeTimeEntrySource` / `normalizeReportGrouping` coerce a stored value whose
|
|
549
|
+
contributing module has since been removed back to `manual` / `project_task`.
|
|
550
|
+
|
|
551
|
+
Pinned by [`lib/time-tracking/__tests__/strategyRegistries.test.ts`](./lib/time-tracking/__tests__/strategyRegistries.test.ts).
|
|
552
|
+
|
|
553
|
+
## Time-tracking data model and settings
|
|
554
|
+
|
|
555
|
+
EP-42…EP-45. Four declaration surfaces. Read the "does not" column of each before
|
|
556
|
+
building on one — three of the four are contracts that a later phase still has to make
|
|
557
|
+
load-bearing, and pretending otherwise is how a third-party module ships a broken screen.
|
|
558
|
+
|
|
559
|
+
### Contributed settings keys (EP-42 — BC surface #2, STABLE)
|
|
560
|
+
|
|
561
|
+
`TIME_TRACKING_SETTING_KEYS`, `normalizeTimeTrackingSettings` and
|
|
562
|
+
`staffTimeTrackingSettingsSchema` all read
|
|
563
|
+
[`lib/time-tracking/settingKeys.ts`](./lib/time-tracking/settingKeys.ts) now. The eight
|
|
564
|
+
keys the module shipped are registered there as built-ins, so with no contribution the
|
|
565
|
+
defaults, the validating schema, the read and the eight `ModuleConfigService` rows are
|
|
566
|
+
what they always were.
|
|
567
|
+
|
|
568
|
+
```ts
|
|
569
|
+
registerTimeTrackingSettingKey({
|
|
570
|
+
group: 'jira', // top-level group in the settings object
|
|
571
|
+
key: 'projectKey', // leaf inside it; the config row is named `jira.projectKey`
|
|
572
|
+
schema: z.string().min(1).max(20),
|
|
573
|
+
default: 'OPS', // must satisfy `schema`, or registration throws
|
|
574
|
+
labelKey: 'jira.settings.projectKey',
|
|
575
|
+
priority: 0,
|
|
576
|
+
})
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
- **Storage and scope.** One `ModuleConfigService` row per key under module id
|
|
580
|
+
`staff.time_tracking`, named `<group>.<key>`, written with `{ tenantId }` only.
|
|
581
|
+
A contributed value is therefore **tenant-global**, with `organization_id` null —
|
|
582
|
+
there is no per-organization, per-project or per-customer override, and a contribution
|
|
583
|
+
cannot opt into one. `GET`/`PUT /api/staff/timesheets/settings` carry it in the group
|
|
584
|
+
it declared.
|
|
585
|
+
- **Validation.** `buildTimeTrackingSettingsSchema()` composes
|
|
586
|
+
`schema.optional().default(default)` per key and one `.optional().default({...})` per
|
|
587
|
+
group. The settings route builds it **per request**, and publishes it through OpenAPI
|
|
588
|
+
getters, so a key registered after the route module first loaded still validates and
|
|
589
|
+
still appears in the published schema. The exported `staffTimeTrackingSettingsSchema`
|
|
590
|
+
const is the load-time snapshot and stays for backward compatibility.
|
|
591
|
+
- **A stored value that no longer validates falls back to the registered default**, the
|
|
592
|
+
same way a stored rounding unit the schema stopped accepting always did. Registering a
|
|
593
|
+
key whose id collides with one of the eight built-ins throws.
|
|
594
|
+
- **Rendering it.** Pair the key with a widget on `staff.time_tracking.settings:sections`
|
|
595
|
+
(EP-26). The spot context carries `{ moduleId, canManage, keys, values, setValue }`:
|
|
596
|
+
`keys` is `contributedTimeTrackingSettingKeys()`, `values` is keyed by `<group>.<key>`
|
|
597
|
+
and holds contributed keys only, and `setValue(id, value)` writes into the page draft
|
|
598
|
+
so the page's own Save round-trips a key it knows nothing about. The eight built-ins
|
|
599
|
+
are absent from `values` — the page renders those itself.
|
|
600
|
+
|
|
601
|
+
### Time-tracking custom fields (EP-43)
|
|
602
|
+
|
|
603
|
+
[`ce.ts`](./ce.ts) declares `staff_time_entry`, `staff_time_project`, `staff_time_task`,
|
|
604
|
+
`staff_time_report` and `staff_time_tag` next to `staff_team_member`, each with
|
|
605
|
+
`showInSidebar: false` and no default fields.
|
|
606
|
+
|
|
607
|
+
**What the declaration does.** All five are *system* entities, so
|
|
608
|
+
`entities/lib/install-from-ce.ts` seeds only their `fields` and never writes a
|
|
609
|
+
`custom_entities` row — `label`, `description` and `showInSidebar` are metadata for code,
|
|
610
|
+
not for the installer. The one runtime behaviour it changes today is `labelField`, read by
|
|
611
|
+
`attachments/lib/assignmentDetails.ts` to label an attached record. It also marks the
|
|
612
|
+
entity `customFields: true` in the generated module facts.
|
|
613
|
+
|
|
614
|
+
**What it does NOT do, and what each surface still needs:**
|
|
615
|
+
|
|
616
|
+
| Surface | State |
|
|
617
|
+
|---|---|
|
|
618
|
+
| Defining a custom field on these entities | Already worked before EP-43. `entities/api/entities.ts` lists every generated entity id, so the Data designer always offered them. |
|
|
619
|
+
| Storing a value through the CRUD routes | **Does not work.** All five `makeCrudRoute` resources are command-backed, and `factory.ts` honours `actions.create.customFields` / `update.customFields` only on its ORM write path (`factory.ts:2403`, `:2741`). A `cf_*` key posted to `/api/staff/timesheets/*` is dropped in silence. Closing it means threading `splitCustomFieldPayload` through each route's `mapInput` and calling `setCustomFieldsIfAny` in the command, the way `commands/team-members.ts` already does. |
|
|
620
|
+
| Reading values back on a list | **Does not work.** None of the five declares `list.decorateCustomFields`, so no `customFields` / `customValues` reaches a list item. |
|
|
621
|
+
| `CrudForm` rendering the inputs | Works for the project form only — it is the one real `CrudForm` and it passes `entityIds={[E.staff.staff_time_project]}`. Its `onSubmit` builds the body with `buildProjectPayload`, which copies named fields, so the rendered values are dropped before the request. `TimeEntryDialog`, `NewTaskDialog` and the report create page are hand-rolled hosts and render no custom fields at all. |
|
|
622
|
+
| Filtering/sorting by a custom field | Not reachable: each list route validates its query with a closed zod schema and builds filters itself, so a `cf:` filter never survives parsing. |
|
|
623
|
+
|
|
624
|
+
Treat EP-43 as the *declaration* half of custom-field support. Do not tell a customer the
|
|
625
|
+
fields are usable end to end until the write path lands.
|
|
626
|
+
|
|
627
|
+
### Cross-module links (EP-44 — BC surface #2, STABLE)
|
|
628
|
+
|
|
629
|
+
[`data/extensions.ts`](./data/extensions.ts) declares four links over the foreign-key
|
|
630
|
+
columns time tracking already carried:
|
|
631
|
+
|
|
632
|
+
| Base | Extension | Join |
|
|
633
|
+
|---|---|---|
|
|
634
|
+
| `customers:customer_entity` | `staff:staff_time_entry` | `id` → `customer_id` |
|
|
635
|
+
| `customers:customer_deal` | `staff:staff_time_entry` | `id` → `deal_id` |
|
|
636
|
+
| `sales:sales_order` | `staff:staff_time_entry` | `id` → `order_id` |
|
|
637
|
+
| `customers:customer_entity` | `staff:staff_time_project` | `id` → `customer_id` |
|
|
638
|
+
|
|
639
|
+
`base` is the other module's entity, `extension` is the staff entity holding the FK —
|
|
640
|
+
the query engine only ever looks a link up by `base`. **These are strings, never
|
|
641
|
+
imports.** Staff is slated for extraction into a standalone package, so
|
|
642
|
+
`data/extensions.ts` may not import a `customers` or `sales` symbol and
|
|
643
|
+
`requires: ['planner', 'resources']` in [`index.ts`](./index.ts) must not grow;
|
|
644
|
+
`__tests__/entityExtensions.test.ts` fails if either happens, and pins every table and
|
|
645
|
+
column against the migration snapshots.
|
|
646
|
+
|
|
647
|
+
**Declaration-only, exactly as in `apps/mercato/src/modules/example/data/extensions.ts`.**
|
|
648
|
+
Nothing in the platform passes `includeExtensions` to a query, the basic engine's
|
|
649
|
+
extension join adds no projection and exposes no filterable or sortable alias, and the
|
|
650
|
+
hybrid engine DI registers ignores the flag outside its basic-engine fallback. What the
|
|
651
|
+
declaration *is* today is the traversal contract `yarn generate` emits as
|
|
652
|
+
`entityExtensions` in the module registry.
|
|
653
|
+
|
|
654
|
+
The matching catalog entry is not in [`extension-points.ts`](./extension-points.ts) on
|
|
655
|
+
purpose: `module-extension-facts.ts` already emits an `entity`-family fact host with the
|
|
656
|
+
`entity-extension` capability for every entity a module owns, so a hand-written
|
|
657
|
+
declaration would duplicate it and, having no source that references
|
|
658
|
+
`extensionPoints.hosts.<key>`, would be emitted as an `unbound-declaration` diagnostic
|
|
659
|
+
rather than a usable host.
|
|
660
|
+
|
|
661
|
+
### Translatable fields (EP-45)
|
|
662
|
+
|
|
663
|
+
[`translations.ts`](./translations.ts) adds `staff_time_project` (`name`, `description`),
|
|
664
|
+
`staff_time_task_status` (`name`) and `staff_time_tag` (`label`). Values are **database
|
|
665
|
+
column** names. Only columns that exist are listed — task statuses have no description and
|
|
666
|
+
a tag's human-readable column is `label`.
|
|
667
|
+
|
|
668
|
+
Registration is via the generator: `translations-fields.generated.ts` imports every
|
|
669
|
+
module's `translations.ts` and calls `registerTranslatableFields`. The hard-coded list in
|
|
670
|
+
`translations/di.ts` covers four core modules and deliberately does not include staff — a
|
|
671
|
+
core module importing staff would violate MUST rule 1.
|
|
672
|
+
|
|
673
|
+
One rendered change: `translations/widgets/injection-table.ts` derives
|
|
674
|
+
`crud-form:<module>.<entity>:header` spots from the registry, so the project **edit** form
|
|
675
|
+
now shows the translation-manager affordance. It is gated on `translations.view` and on a
|
|
676
|
+
record id, so create forms and callers without the feature see nothing. No list or detail
|
|
677
|
+
output changes until a tenant actually writes a translation — the overlay substitutes a
|
|
678
|
+
value only when a row exists for the request locale.
|
|
679
|
+
|
|
680
|
+
## Time-tracking search, analytics, notifications and AI
|
|
681
|
+
|
|
682
|
+
EP-46…EP-49 and EP-51. Four contribution surfaces plus the recalculation hook.
|
|
683
|
+
|
|
684
|
+
### Search (EP-46) — and the one thing it cannot do
|
|
685
|
+
|
|
686
|
+
[`search.ts`](./search.ts) indexes all five time-tracking entities. **Read this
|
|
687
|
+
before adding a sixth.** An entity's `aclFeatures` is the *only* authorization the
|
|
688
|
+
search pipeline applies. It is enforced twice per query — `resolveReadableEntityTypes`
|
|
689
|
+
narrows the entity list before the query, `filterSearchResultsByEntityAccess`
|
|
690
|
+
filters the results after it — but both decide **per entity type**.
|
|
691
|
+
`SearchEntityConfig` has no row-level hook and `SearchOptions` has no record-id
|
|
692
|
+
allowlist, so a search gate cannot reproduce the per-project membership
|
|
693
|
+
intersection `resolveProjectAccess` applies on every time-tracking route.
|
|
694
|
+
|
|
695
|
+
So each gate names the feature set whose holder already sees **every** record of
|
|
696
|
+
that entity through the API:
|
|
697
|
+
|
|
698
|
+
| Entity | `aclFeatures` | Why |
|
|
699
|
+
|---|---|---|
|
|
700
|
+
| `staff:staff_time_project` | `staff.timesheets.projects.view` | Unchanged; the projects list is not membership-narrowed |
|
|
701
|
+
| `staff:staff_time_task` | `tasks.view` **+** `projects.manage` | `projects.manage` is what makes `resolveProjectAccess` answer `canManageAll` |
|
|
702
|
+
| `staff:staff_time_report` | `reports.view` **+** `projects.manage` | Same; the reports list intersects with project membership otherwise |
|
|
703
|
+
| `staff:staff_time_tag` | `staff.timesheets.view` | Tags are organization-global on their own route; exact match |
|
|
704
|
+
| `staff:staff_time_entry` | `staff.timesheets.view` **+** `projects.manage` | Entry notes are the most sensitive text the module holds |
|
|
705
|
+
|
|
706
|
+
A project member without `projects.manage` therefore gets **no** task, report or
|
|
707
|
+
entry hits — including for their own records. That is deliberate: the gate cannot
|
|
708
|
+
express "own", and half a gate would disclose other clients' work. Money columns
|
|
709
|
+
are `fieldPolicy.excluded`, not merely left out of `searchable`. The tag entity
|
|
710
|
+
resolves no URL — there is no tag detail screen and the entries list deep-links
|
|
711
|
+
only `taskId` and `ids`, so an unlinked hit is more honest than a wrong link.
|
|
712
|
+
|
|
713
|
+
### Analytics (EP-47)
|
|
714
|
+
|
|
715
|
+
[`analytics.ts`](./analytics.ts) declares `staff:staff_time_entries`,
|
|
716
|
+
`staff:staff_time_tasks`, `staff:staff_time_projects` and `staff:staff_time_reports`.
|
|
717
|
+
|
|
718
|
+
- **No money column is mapped.** An `AnalyticsEntityConfig` carries one
|
|
719
|
+
`requiredFeatures` list for the whole entity, so there is no shape in which
|
|
720
|
+
`total_amount` or `hourly_rate` is gated on `staff.timesheets.rates.view` while
|
|
721
|
+
the rest stays readable. Minutes carry the same analysis without the disclosure.
|
|
722
|
+
- **Custom fields are not dimensions and cannot be made into dimensions.**
|
|
723
|
+
`AnalyticsFieldMapping` is `{ dbColumn, type }` and the aggregation builder emits
|
|
724
|
+
`dbColumn` as a bare identifier against the one `tableName`, so a mapping can only
|
|
725
|
+
ever name a real column on that table. The EP-43 custom fields are EAV rows in the
|
|
726
|
+
`entities` module's tables — no join, and the jsonb-path form needs the values on
|
|
727
|
+
the row itself. The point is moot anyway while the write path drops `cf_*`.
|
|
728
|
+
- `is_billable` / `billable_by_default` are typed `text` because
|
|
729
|
+
`AnalyticsFieldType` has no boolean; the type is read only to decide whether a
|
|
730
|
+
group-by column gets a `date_trunc`.
|
|
731
|
+
|
|
732
|
+
### Notification types and reactive handlers (EP-48)
|
|
733
|
+
|
|
734
|
+
[`notifications.ts`](./notifications.ts) adds four ids;
|
|
735
|
+
[`notifications.handlers.ts`](./notifications.handlers.ts) is the module's browser
|
|
736
|
+
reaction to them, one handler per type with a stable override key
|
|
737
|
+
(`notifications.handlers.<id>`).
|
|
738
|
+
|
|
739
|
+
| Type | Raised by |
|
|
740
|
+
|---|---|
|
|
741
|
+
| `staff.timesheets.time_report.approved` | **Core.** [`subscribers/time-report-approved-notification.ts`](./subscribers/time-report-approved-notification.ts) on `time_report.closed` — closing IS the approval; the recipient is the report's author, never the closer |
|
|
742
|
+
| `staff.timesheets.time_report.ready_for_approval` | Contributable only — pairs with a `registerReportApprovalPolicy` (EP-41) that adds a review step. The module has one transition, draft → closed, so core has nothing to announce |
|
|
743
|
+
| `staff.timesheets.time_entry.timer_running_long` | Contributable only — needs a scheduled sweep over open timers, which the module does not ship |
|
|
744
|
+
| `staff.timesheets.timesheet.period_incomplete` | Contributable only — "incomplete" is the capacity provider's question (EP-40), and the built-in provider has no schedule and no opinion about when to complain |
|
|
745
|
+
|
|
746
|
+
A contributable-only type is not a dormant id: the renderer, the delivery
|
|
747
|
+
preferences and the browser handler are all in place, so a contributed raiser gets
|
|
748
|
+
a working notification on day one without patching core.
|
|
749
|
+
|
|
750
|
+
### AI tool pack and agent (EP-49)
|
|
751
|
+
|
|
752
|
+
[`ai-tools.ts`](./ai-tools.ts) publishes six tools and
|
|
753
|
+
[`ai-agents.ts`](./ai-agents.ts) one agent, `staff.time_tracking_assistant`
|
|
754
|
+
(`mutationPolicy: 'confirm-required'`).
|
|
755
|
+
|
|
756
|
+
| Tool | Kind | Gate | Backing route |
|
|
757
|
+
|---|---|---|---|
|
|
758
|
+
| `staff.log_time` | mutation | `manage_own` | `POST /staff/timesheets/time-entries` |
|
|
759
|
+
| `staff.start_timer` | mutation | `manage_own` | `POST /staff/timesheets/time-entries/start-timer` |
|
|
760
|
+
| `staff.stop_timer` | mutation | `manage_own` | `POST /staff/timesheets/time-entries/{id}/timer-stop` |
|
|
761
|
+
| `staff.summarize_week` | read | `view` | `GET /staff/timesheets/time-entries` |
|
|
762
|
+
| `staff.find_missing_days` | read | `view` | same |
|
|
763
|
+
| `staff.draft_client_report` | read | `reports.view` | `POST /staff/timesheets/reports/preview` |
|
|
764
|
+
|
|
765
|
+
**A module tool never calls `prepareMutation`.** That function is framework code:
|
|
766
|
+
its only call site is `agent-tools.ts`, which intercepts the model's call,
|
|
767
|
+
short-circuits the handler, persists an `AiPendingAction` and returns the preview
|
|
768
|
+
card. A tool opts in by declaring `isMutation: true` plus a `loadBeforeRecord`
|
|
769
|
+
resolver that renders the diff; the handler runs later, once, from the confirm
|
|
770
|
+
route, with the stored input. Calling `prepareMutation` from a tool would be
|
|
771
|
+
calling the interceptor from inside the thing it intercepts.
|
|
772
|
+
|
|
773
|
+
Every tool is API-backed through `createAiApiOperationRunner`, which refuses a
|
|
774
|
+
route whose `requiredFeatures` the tool does not itself declare. The gate is
|
|
775
|
+
therefore checked twice, and the route still applies the project-access
|
|
776
|
+
intersection, the mutation guards, the interceptors and the commands. Nothing in
|
|
777
|
+
the pack writes a time entry itself. The tools act as the caller's own staff member
|
|
778
|
+
and none accepts a `staffMemberId`. `staff.draft_client_report` asks for a preview
|
|
779
|
+
with `showRates: false` and projects only minute columns, so an agent transcript
|
|
780
|
+
never becomes a second, ungated copy of a customer's rate card.
|
|
781
|
+
|
|
782
|
+
### Recalculation hooks and the CLI (EP-51)
|
|
783
|
+
|
|
784
|
+
```ts
|
|
785
|
+
registerTimeTrackingRecalculation({
|
|
786
|
+
id: 'billing.recalculate_rates',
|
|
787
|
+
labelKey: 'billing.recalculations.rates',
|
|
788
|
+
async run({ container, scope, report }) {
|
|
789
|
+
await report.setTotal(total)
|
|
790
|
+
// …batch, then `await report.advance(n)` and check `report.isCancellationRequested()`
|
|
791
|
+
return { totalCount, processedCount, updatedCount, unchangedCount, skippedCount, cancelled }
|
|
792
|
+
},
|
|
793
|
+
})
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
- A hook does **not** own the `ProgressJob`. `runTimeTrackingRecalculations`
|
|
797
|
+
([`lib/time-tracking/recalculationRunner.ts`](./lib/time-tracking/recalculationRunner.ts))
|
|
798
|
+
owns start / totals / cancel / complete, so several hooks share one progress bar;
|
|
799
|
+
marking a job failed is the worker's job. Hooks run **sequentially** in registry
|
|
800
|
+
order — they write the same rows.
|
|
801
|
+
- `run` must carry `scope.tenantId` into every query, honour `organizationIds` when
|
|
802
|
+
it is not `null`, and never touch an entry locked into a closed report.
|
|
803
|
+
- **A job with no `hookIds` runs the built-in rounding pass alone.** That is what
|
|
804
|
+
`POST /api/staff/timesheets/settings/reapply-rounding` enqueues, so a contributed
|
|
805
|
+
hook cannot attach itself to the retro-rounding button a tenant pressed. An
|
|
806
|
+
explicit list comes only from the CLI.
|
|
807
|
+
|
|
808
|
+
```bash
|
|
809
|
+
yarn mercato staff timesheets:recalculate --list
|
|
810
|
+
yarn mercato staff timesheets:recalculate --tenant <id> [--org <id>] [--hook <id>[,<id>]]
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
The CLI enqueues onto the same queue with the same `ProgressJob` the settings
|
|
814
|
+
screen uses; it does not do the work in process. An unknown `--hook` id refuses to
|
|
815
|
+
start rather than silently doing nothing.
|
|
816
|
+
|
|
49
817
|
## Internal-Only Surfaces (NOT public contract)
|
|
50
818
|
|
|
51
819
|
These are subject to change without deprecation; do not import them from non-staff code:
|