@open-mercato/core 0.10.1-develop.7716.1.cefe7975df → 0.10.1-develop.7721.1.b5c4a00a68
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_support_routing_rule/index.js +21 -0
- package/dist/generated/entities/staff_support_routing_rule/index.js.map +7 -0
- package/dist/generated/entities/staff_support_ticket/index.js +37 -0
- package/dist/generated/entities/staff_support_ticket/index.js.map +7 -0
- package/dist/generated/entities/staff_time_project/index.js +2 -0
- package/dist/generated/entities/staff_time_project/index.js.map +2 -2
- package/dist/generated/entities/staff_time_project_member/index.js +2 -0
- package/dist/generated/entities/staff_time_project_member/index.js.map +2 -2
- package/dist/generated/entities/staff_time_task/index.js +4 -0
- package/dist/generated/entities/staff_time_task/index.js.map +2 -2
- package/dist/generated/entities/staff_time_task_comment/index.js +9 -1
- package/dist/generated/entities/staff_time_task_comment/index.js.map +2 -2
- package/dist/generated/entities.ids.generated.js +2 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +38 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/auth.js +1 -0
- package/dist/helpers/integration/auth.js.map +2 -2
- package/dist/helpers/integration/communicationChannelsFixtures.js +20 -0
- package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
- package/dist/modules/catalog/api/bulk-delete/route.js +12 -2
- package/dist/modules/catalog/api/bulk-delete/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/test-seed/route.js +17 -3
- package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
- package/dist/modules/communication_channels/lib/test-seed.js +38 -5
- package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
- package/dist/modules/messages/commands/messages.js +2 -3
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
- package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js +299 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js +91 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/route.js +146 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/options/route.js +60 -0
- package/dist/modules/staff/api/portal/support-tickets/options/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/route.js +220 -0
- package/dist/modules/staff/api/portal/support-tickets/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/settings/route.js +7 -1
- package/dist/modules/staff/api/timesheets/settings/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js +122 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js +180 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/route.js +147 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/support-tickets/options/route.js +133 -0
- package/dist/modules/staff/api/timesheets/support-tickets/options/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js +33 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js +25 -7
- package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js +32 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/route.js +94 -47
- package/dist/modules/staff/api/timesheets/tasks/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +3 -0
- 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 +3 -0
- package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +1 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +54 -4
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js +21 -5
- package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/settings/page.js +319 -254
- package/dist/modules/staff/backend/staff/time-tracking/settings/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/support/page.js +363 -0
- package/dist/modules/staff/backend/staff/time-tracking/support/page.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js +18 -0
- package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js.map +7 -0
- package/dist/modules/staff/cli.js +27 -0
- package/dist/modules/staff/cli.js.map +2 -2
- package/dist/modules/staff/commands/index.js +3 -0
- package/dist/modules/staff/commands/index.js.map +2 -2
- package/dist/modules/staff/commands/shared.js +10 -1
- package/dist/modules/staff/commands/shared.js.map +2 -2
- package/dist/modules/staff/commands/support-routing-rules.js +505 -0
- package/dist/modules/staff/commands/support-routing-rules.js.map +7 -0
- package/dist/modules/staff/commands/support-tickets.js +513 -0
- package/dist/modules/staff/commands/support-tickets.js.map +7 -0
- package/dist/modules/staff/commands/timesheets-projects.js +72 -14
- package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-task-assignment.js +270 -0
- package/dist/modules/staff/commands/timesheets-task-assignment.js.map +7 -0
- package/dist/modules/staff/commands/timesheets-task-comments.js +58 -12
- package/dist/modules/staff/commands/timesheets-task-comments.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-tasks.js +91 -9
- package/dist/modules/staff/commands/timesheets-tasks.js.map +3 -3
- package/dist/modules/staff/data/enrichers.js +141 -0
- package/dist/modules/staff/data/enrichers.js.map +2 -2
- package/dist/modules/staff/data/entities.js +143 -2
- package/dist/modules/staff/data/entities.js.map +2 -2
- package/dist/modules/staff/data/extensions.js +8 -0
- package/dist/modules/staff/data/extensions.js.map +2 -2
- package/dist/modules/staff/data/validators.js +34 -5
- package/dist/modules/staff/data/validators.js.map +2 -2
- package/dist/modules/staff/encryption.js +6 -0
- package/dist/modules/staff/encryption.js.map +2 -2
- package/dist/modules/staff/events.js +25 -0
- package/dist/modules/staff/events.js.map +2 -2
- package/dist/modules/staff/events.payloads.js +54 -0
- package/dist/modules/staff/events.payloads.js.map +2 -2
- package/dist/modules/staff/extension-points.js +1 -0
- package/dist/modules/staff/extension-points.js.map +2 -2
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js +468 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js +12 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js +316 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js +12 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js +190 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js +19 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js.map +7 -0
- package/dist/modules/staff/index.js +1 -1
- package/dist/modules/staff/index.js.map +2 -2
- package/dist/modules/staff/lib/crud.js +2 -0
- package/dist/modules/staff/lib/crud.js.map +2 -2
- package/dist/modules/staff/lib/support/attachments.js +36 -0
- package/dist/modules/staff/lib/support/attachments.js.map +7 -0
- package/dist/modules/staff/lib/support/dictionaries.js +87 -0
- package/dist/modules/staff/lib/support/dictionaries.js.map +7 -0
- package/dist/modules/staff/lib/support/notifications.js +111 -0
- package/dist/modules/staff/lib/support/notifications.js.map +7 -0
- package/dist/modules/staff/lib/support/portalAttachments.js +100 -0
- package/dist/modules/staff/lib/support/portalAttachments.js.map +7 -0
- package/dist/modules/staff/lib/support/portalBroadcast.js +117 -0
- package/dist/modules/staff/lib/support/portalBroadcast.js.map +7 -0
- package/dist/modules/staff/lib/support/portalRoute.js +99 -0
- package/dist/modules/staff/lib/support/portalRoute.js.map +7 -0
- package/dist/modules/staff/lib/support/portalStatus.js +12 -0
- package/dist/modules/staff/lib/support/portalStatus.js.map +7 -0
- package/dist/modules/staff/lib/support/portalTickets.js +148 -0
- package/dist/modules/staff/lib/support/portalTickets.js.map +7 -0
- package/dist/modules/staff/lib/support/routing.js +81 -0
- package/dist/modules/staff/lib/support/routing.js.map +7 -0
- package/dist/modules/staff/lib/support/routingRuleDraft.js +25 -0
- package/dist/modules/staff/lib/support/routingRuleDraft.js.map +7 -0
- package/dist/modules/staff/lib/support/seedDefaults.js +114 -0
- package/dist/modules/staff/lib/support/seedDefaults.js.map +7 -0
- package/dist/modules/staff/lib/support/supportActivity.js +26 -0
- package/dist/modules/staff/lib/support/supportActivity.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js +23 -0
- package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js +112 -0
- package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/portalPendingUploads.js +45 -0
- package/dist/modules/staff/lib/support-ui/portalPendingUploads.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/portalSupport.js +51 -0
- package/dist/modules/staff/lib/support-ui/portalSupport.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/access.js +3 -39
- package/dist/modules/staff/lib/time-tracking/access.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/listProjectAccess.js +54 -0
- package/dist/modules/staff/lib/time-tracking/listProjectAccess.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/portalRecipients.js +92 -2
- package/dist/modules/staff/lib/time-tracking/portalRecipients.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/projectAssignment.js +112 -0
- package/dist/modules/staff/lib/time-tracking/projectAssignment.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/settingKeys.js +14 -0
- package/dist/modules/staff/lib/time-tracking/settingKeys.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/settings.js +28 -1
- package/dist/modules/staff/lib/time-tracking/settings.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js +143 -0
- package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/workRecipients.js +92 -0
- package/dist/modules/staff/lib/time-tracking/workRecipients.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js +32 -3
- package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js +29 -0
- package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js +4 -0
- package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js +141 -1
- package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js +153 -0
- package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js +67 -0
- package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js +3 -1
- package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js +80 -17
- package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js +230 -41
- package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js +21 -1
- package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js +16 -4
- package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js +3 -1
- package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js +68 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js +50 -0
- package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js +13 -1
- package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js +36 -2
- package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js.map +2 -2
- package/dist/modules/staff/migrations/Migration20261009180543_staff.js +42 -0
- package/dist/modules/staff/migrations/Migration20261009180543_staff.js.map +7 -0
- package/dist/modules/staff/notifications.js +92 -0
- package/dist/modules/staff/notifications.js.map +2 -2
- package/dist/modules/staff/setup.js +6 -2
- package/dist/modules/staff/setup.js.map +2 -2
- package/dist/modules/staff/subscribers/support-portal-comment-created.js +26 -0
- package/dist/modules/staff/subscribers/support-portal-comment-created.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-comment-deleted.js +26 -0
- package/dist/modules/staff/subscribers/support-portal-comment-deleted.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-comment-updated.js +26 -0
- package/dist/modules/staff/subscribers/support-portal-comment-updated.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-status-column-updated.js +24 -0
- package/dist/modules/staff/subscribers/support-portal-status-column-updated.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-task-status-changed.js +25 -0
- package/dist/modules/staff/subscribers/support-portal-task-status-changed.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-task-updated.js +25 -0
- package/dist/modules/staff/subscribers/support-portal-task-updated.js.map +7 -0
- package/dist/modules/staff/subscribers/support-ticket-created-notification.js +25 -0
- package/dist/modules/staff/subscribers/support-ticket-created-notification.js.map +7 -0
- package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js +25 -0
- package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js.map +7 -0
- package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js +26 -0
- package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js.map +7 -0
- package/dist/modules/staff/subscribers/time-task-assignee-notification.js +83 -0
- package/dist/modules/staff/subscribers/time-task-assignee-notification.js.map +7 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js +112 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js.map +7 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js +17 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js.map +7 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js +870 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js.map +7 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js +17 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js.map +7 -0
- package/dist/modules/staff/widgets/injection-table.js +16 -1
- package/dist/modules/staff/widgets/injection-table.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +4 -18
- package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLotDetailPage.js +4 -18
- package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +4 -18
- package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +2 -2
- package/dist/modules/wms/lib/inventoryCsvExport.js +19 -0
- package/dist/modules/wms/lib/inventoryCsvExport.js.map +7 -0
- package/generated/entities/staff_support_routing_rule/index.ts +9 -0
- package/generated/entities/staff_support_ticket/index.ts +17 -0
- package/generated/entities/staff_time_project/index.ts +1 -0
- package/generated/entities/staff_time_project_member/index.ts +1 -0
- package/generated/entities/staff_time_task/index.ts +2 -0
- package/generated/entities/staff_time_task_comment/index.ts +4 -0
- package/generated/entities.ids.generated.ts +2 -0
- package/generated/entity-fields-registry.ts +38 -0
- package/package.json +8 -8
- package/src/helpers/integration/auth.ts +1 -1
- package/src/helpers/integration/communicationChannelsFixtures.ts +60 -2
- package/src/modules/catalog/api/bulk-delete/route.ts +12 -2
- package/src/modules/communication_channels/api/post/test-seed/route.ts +29 -12
- package/src/modules/communication_channels/lib/test-seed.ts +49 -10
- package/src/modules/messages/commands/messages.ts +8 -10
- package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +2 -2
- package/src/modules/staff/AGENTS.md +22 -6
- package/src/modules/staff/api/portal/support-tickets/[id]/attachments/route.ts +327 -0
- package/src/modules/staff/api/portal/support-tickets/[id]/messages/route.ts +94 -0
- package/src/modules/staff/api/portal/support-tickets/[id]/route.ts +160 -0
- package/src/modules/staff/api/portal/support-tickets/options/route.ts +60 -0
- package/src/modules/staff/api/portal/support-tickets/route.ts +253 -0
- package/src/modules/staff/api/timesheets/settings/route.ts +7 -1
- package/src/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.ts +131 -0
- package/src/modules/staff/api/timesheets/support-routing-rules/preview/route.ts +248 -0
- package/src/modules/staff/api/timesheets/support-routing-rules/route.ts +150 -0
- package/src/modules/staff/api/timesheets/support-tickets/options/route.ts +181 -0
- package/src/modules/staff/api/timesheets/tasks/[id]/claim/route.ts +31 -0
- package/src/modules/staff/api/timesheets/tasks/[id]/comments/route.ts +25 -7
- package/src/modules/staff/api/timesheets/tasks/[id]/release/route.ts +30 -0
- package/src/modules/staff/api/timesheets/tasks/route.ts +119 -67
- package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +3 -0
- package/src/modules/staff/api/timesheets/time-projects/route.ts +3 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +1 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +67 -2
- package/src/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.ts +22 -3
- package/src/modules/staff/backend/staff/time-tracking/settings/page.tsx +405 -308
- package/src/modules/staff/backend/staff/time-tracking/support/page.meta.ts +14 -0
- package/src/modules/staff/backend/staff/time-tracking/support/page.tsx +428 -0
- package/src/modules/staff/cli.ts +28 -0
- package/src/modules/staff/commands/index.ts +3 -0
- package/src/modules/staff/commands/shared.ts +22 -1
- package/src/modules/staff/commands/support-routing-rules.ts +595 -0
- package/src/modules/staff/commands/support-tickets.ts +580 -0
- package/src/modules/staff/commands/timesheets-projects.ts +84 -13
- package/src/modules/staff/commands/timesheets-task-assignment.ts +319 -0
- package/src/modules/staff/commands/timesheets-task-comments.ts +70 -15
- package/src/modules/staff/commands/timesheets-tasks.ts +106 -15
- package/src/modules/staff/data/enrichers.ts +203 -0
- package/src/modules/staff/data/entities.ts +133 -2
- package/src/modules/staff/data/extensions.ts +9 -1
- package/src/modules/staff/data/validators.ts +36 -0
- package/src/modules/staff/encryption.ts +6 -0
- package/src/modules/staff/events.payloads.ts +87 -1
- package/src/modules/staff/events.ts +26 -0
- package/src/modules/staff/extension-points.ts +1 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.ts +10 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.tsx +553 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.ts +10 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.tsx +346 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/page.meta.ts +17 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/page.tsx +253 -0
- package/src/modules/staff/i18n/de.json +291 -0
- package/src/modules/staff/i18n/en.json +291 -0
- package/src/modules/staff/i18n/es.json +291 -0
- package/src/modules/staff/i18n/ko.json +291 -0
- package/src/modules/staff/i18n/pl.json +291 -0
- package/src/modules/staff/index.ts +1 -1
- package/src/modules/staff/lib/crud.ts +2 -1
- package/src/modules/staff/lib/support/attachments.ts +43 -0
- package/src/modules/staff/lib/support/dictionaries.ts +120 -0
- package/src/modules/staff/lib/support/notifications.ts +138 -0
- package/src/modules/staff/lib/support/portalAttachments.ts +140 -0
- package/src/modules/staff/lib/support/portalBroadcast.ts +161 -0
- package/src/modules/staff/lib/support/portalRoute.ts +108 -0
- package/src/modules/staff/lib/support/portalStatus.ts +15 -0
- package/src/modules/staff/lib/support/portalTickets.ts +233 -0
- package/src/modules/staff/lib/support/routing.ts +117 -0
- package/src/modules/staff/lib/support/routingRuleDraft.ts +45 -0
- package/src/modules/staff/lib/support/seedDefaults.ts +151 -0
- package/src/modules/staff/lib/support/supportActivity.ts +39 -0
- package/src/modules/staff/lib/support-ui/PortalSupportNotLinked.tsx +20 -0
- package/src/modules/staff/lib/support-ui/portalAttachmentValidation.ts +125 -0
- package/src/modules/staff/lib/support-ui/portalPendingUploads.ts +49 -0
- package/src/modules/staff/lib/support-ui/portalSupport.ts +116 -0
- package/src/modules/staff/lib/time-tracking/access.ts +2 -62
- package/src/modules/staff/lib/time-tracking/listProjectAccess.ts +78 -0
- package/src/modules/staff/lib/time-tracking/portalRecipients.ts +173 -5
- package/src/modules/staff/lib/time-tracking/projectAssignment.ts +172 -0
- package/src/modules/staff/lib/time-tracking/settingKeys.ts +11 -0
- package/src/modules/staff/lib/time-tracking/settings.ts +46 -1
- package/src/modules/staff/lib/time-tracking/taskAssignmentRoute.ts +165 -0
- package/src/modules/staff/lib/time-tracking/workRecipients.ts +183 -0
- package/src/modules/staff/lib/time-tracking-ui/BoardFilterChips.tsx +36 -1
- package/src/modules/staff/lib/time-tracking-ui/KanbanBoard.tsx +30 -0
- package/src/modules/staff/lib/time-tracking-ui/KanbanCard.tsx +9 -0
- package/src/modules/staff/lib/time-tracking-ui/ProjectFormSections.tsx +185 -6
- package/src/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.tsx +188 -0
- package/src/modules/staff/lib/time-tracking-ui/SupportRequestPanel.tsx +102 -0
- package/src/modules/staff/lib/time-tracking-ui/TaskBoardScreen.tsx +2 -0
- package/src/modules/staff/lib/time-tracking-ui/TaskCommentThread.tsx +105 -22
- package/src/modules/staff/lib/time-tracking-ui/TaskDrawer.tsx +278 -43
- package/src/modules/staff/lib/time-tracking-ui/boardDirectory.ts +21 -0
- package/src/modules/staff/lib/time-tracking-ui/boardFilters.ts +16 -2
- package/src/modules/staff/lib/time-tracking-ui/kanbanBoardData.ts +4 -0
- package/src/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.ts +117 -0
- package/src/modules/staff/lib/time-tracking-ui/settingsTabs.ts +17 -0
- package/src/modules/staff/lib/time-tracking-ui/supportTicketData.ts +71 -0
- package/src/modules/staff/lib/time-tracking-ui/taskDrawerData.ts +15 -0
- package/src/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.ts +48 -1
- package/src/modules/staff/migrations/.snapshot-open-mercato.json +714 -0
- package/src/modules/staff/migrations/Migration20261009180543_staff.ts +50 -0
- package/src/modules/staff/notifications.ts +92 -0
- package/src/modules/staff/setup.ts +6 -2
- package/src/modules/staff/subscribers/support-portal-comment-created.ts +35 -0
- package/src/modules/staff/subscribers/support-portal-comment-deleted.ts +35 -0
- package/src/modules/staff/subscribers/support-portal-comment-updated.ts +35 -0
- package/src/modules/staff/subscribers/support-portal-status-column-updated.ts +36 -0
- package/src/modules/staff/subscribers/support-portal-task-status-changed.ts +34 -0
- package/src/modules/staff/subscribers/support-portal-task-updated.ts +34 -0
- package/src/modules/staff/subscribers/support-ticket-created-notification.ts +27 -0
- package/src/modules/staff/subscribers/support-ticket-customer-replied-notification.ts +27 -0
- package/src/modules/staff/subscribers/support-ticket-unnotified-clear.ts +29 -0
- package/src/modules/staff/subscribers/time-task-assignee-notification.ts +102 -0
- package/src/modules/staff/widgets/injection/portal-support-tickets/widget.client.tsx +151 -0
- package/src/modules/staff/widgets/injection/portal-support-tickets/widget.ts +16 -0
- package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.tsx +1058 -0
- package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.ts +17 -0
- package/src/modules/staff/widgets/injection-table.ts +15 -0
- package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +4 -20
- package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +4 -20
- package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +4 -19
- package/src/modules/wms/lib/inventoryCsvExport.ts +18 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/staff/backend/staff/time-tracking/settings/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\n/**\n * Module settings \u2014 screen 16 (T7.1).\n *\n * One page, four groups, and one thing that is not decoration: the worked examples\n * under the rounding control recompute live from `roundMinutes`, the same helper the\n * write path uses (mockup note 1). Rounding is the rule people misread, and its\n * effect is otherwise invisible until an invoice goes out.\n *\n * Two deliberate departures from the mockup:\n *\n * * **\"Apply retroactively\" is an action, not a switch** (note 3 read strictly). A\n * switch that restates `rounded_minutes` across a tenant's history as a side\n * effect of saving a form is a silent rewrite of billed amounts. It is a button\n * that names the number of affected entries, asks for confirmation, and queues a\n * `ProgressJob` that can never touch a locked entry.\n * * **The access grace window is on this page.** It is live behaviour \u2014 a member\n * loses project access this many days after their assignment ends (D-12) \u2014 and it\n * had no UI at all before this screen.\n *\n * Every rule here is tenant-global by decision \u00A710; the card on the right says so,\n * because \"why can't I set this per project\" is otherwise a support ticket.\n */\n\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'\nimport * as React from 'react'\nimport { AlertTriangle, History, Info, Save } from 'lucide-react'\nimport { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'\nimport { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Card, CardContent, CardHeader, CardTitle } from '@open-mercato/ui/primitives/card'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { SegmentedControl, SegmentedControlItem } from '@open-mercato/ui/primitives/segmented-control'\nimport { SwitchField } from '@open-mercato/ui/primitives/switch-field'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { OrganizationScopeChangedDetail } from '@open-mercato/shared/lib/frontend/organizationEvents'\nimport { useOrganizationScopeDetail, useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { formatDuration } from '../../../../lib/time-tracking/duration'\nimport type { RoundingUnitMinutes } from '../../../../lib/time-tracking/rounding'\nimport type { TimeTrackingSettings } from '../../../../lib/time-tracking/settings'\nimport { contributedTimeTrackingSettingKeys } from '../../../../lib/time-tracking/settingKeys'\nimport {\n ROUNDING_UNIT_OPTIONS,\n buildRoundingExamples,\n draftRounding,\n formatSignedMinutes,\n isSettingsDraftDirty,\n settingsDraftErrors,\n toSettingsDraft,\n toSettingsPayload,\n type TimeTrackingSettingsDraft,\n} from '../../../../lib/time-tracking-ui/timeTrackingSettingsForm'\n\nconst logger = createLogger('staff').child({ component: 'time-tracking/settings' })\n\nconst SETTINGS_API = '/api/staff/timesheets/settings'\nconst IMPACT_API = '/api/staff/timesheets/settings/rounding-impact'\nconst REAPPLY_API = '/api/staff/timesheets/settings/reapply-rounding'\nconst MANAGE_FEATURE = 'staff.timesheets.settings.manage'\nconst MUTATION_CONTEXT_ID = 'staff.time_tracking.settings'\nconst RESOURCE_KIND = 'staff.timesheets.settings'\nconst RESOURCE_ID = 'time_tracking'\nconst IMPACT_DEBOUNCE_MS = 350\nconst TENANT_SCOPE_CHANGED_CODE = 'tenant_scope_changed'\n\nclass TenantScopeChangedError extends Error {\n constructor() {\n super('[internal] tenant scope changed before the settings save was applied')\n this.name = 'TenantScopeChangedError'\n }\n}\n\nfunction isTenantScopeChangedResponse(status: number, result: unknown): boolean {\n return status === 409\n && typeof result === 'object'\n && result !== null\n && (result as { code?: unknown }).code === TENANT_SCOPE_CHANGED_CODE\n}\n\ntype MutationContext = {\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n}\n\ntype RoundingImpactTotals = {\n entryCount: number\n rawMinutes: number\n roundedMinutes: number\n deltaMinutes: number\n}\n\ntype RoundingImpactResponse = {\n windowDays: number\n projected: RoundingImpactTotals\n lockedEntryCount: number\n}\n\nconst TIME_TRACKING_SETTINGS_MODULE_ID = 'staff.time_tracking'\nconst EMPTY_CONTRIBUTED_SETTINGS: Record<string, unknown> = {}\n\nexport default function TimeTrackingSettingsPage() {\n const t = useT()\n const { payload } = useBackendChrome()\n const scopeVersion = useOrganizationScopeVersion()\n const scope = useOrganizationScopeDetail()\n const scopeRef = React.useRef(scope)\n scopeRef.current = scope\n const loadedScopeRef = React.useRef<OrganizationScopeChangedDetail | null>(null)\n const latestLoadRef = React.useRef(0)\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const canManage = hasFeature(payload?.grantedFeatures, MANAGE_FEATURE)\n\n const [baseline, setBaseline] = React.useState<TimeTrackingSettingsDraft | null>(null)\n const [draft, setDraft] = React.useState<TimeTrackingSettingsDraft | null>(null)\n const draftRef = React.useRef(draft)\n draftRef.current = draft\n const baselineRef = React.useRef(baseline)\n baselineRef.current = baseline\n const [isLoading, setIsLoading] = React.useState(true)\n const [loadError, setLoadError] = React.useState<string | null>(null)\n const [isSaving, setIsSaving] = React.useState(false)\n const [isReapplying, setIsReapplying] = React.useState(false)\n\n const [impact, setImpact] = React.useState<RoundingImpactResponse | null>(null)\n const [impactState, setImpactState] = React.useState<'idle' | 'loading' | 'error'>('idle')\n\n const { runMutation, retryLastMutation } = useGuardedMutation<MutationContext>({\n contextId: MUTATION_CONTEXT_ID,\n blockedMessage: t('staff.time_tracking.settings.saveError', 'Failed to save the settings.'),\n })\n\n const loadSettings = React.useCallback(async (options?: { discardDraft?: boolean }) => {\n const requestId = latestLoadRef.current + 1\n latestLoadRef.current = requestId\n const requestScope = { tenantId: scopeRef.current.tenantId, organizationId: scopeRef.current.organizationId }\n setIsLoading(true)\n setLoadError(null)\n try {\n const call = await apiCall<TimeTrackingSettings>(SETTINGS_API)\n if (requestId !== latestLoadRef.current) return\n if (!call.ok || !call.result) {\n setLoadError(t('staff.time_tracking.settings.loadError', 'Failed to load the settings.'))\n return\n }\n const next = toSettingsDraft(call.result)\n const pending = draftRef.current\n const previous = baselineRef.current\n const loadedScope = loadedScopeRef.current\n const scopeUnresolved = loadedScope === null || (loadedScope.tenantId === null && loadedScope.organizationId === null)\n const scopeUnchanged = loadedScope !== null\n && loadedScope.tenantId === requestScope.tenantId\n && loadedScope.organizationId === requestScope.organizationId\n const hasUnsavedEdit = pending !== null && previous !== null && isSettingsDraftDirty(pending, previous)\n const keepDraft = options?.discardDraft !== true && hasUnsavedEdit && (scopeUnresolved || scopeUnchanged)\n loadedScopeRef.current = requestScope\n setBaseline(next)\n if (!keepDraft) setDraft(next)\n } catch (err) {\n if (requestId !== latestLoadRef.current) return\n logger.error('time tracking settings load failed', { err })\n setLoadError(t('staff.time_tracking.settings.loadError', 'Failed to load the settings.'))\n } finally {\n if (requestId === latestLoadRef.current) setIsLoading(false)\n }\n }, [t])\n\n React.useEffect(() => {\n void loadSettings()\n }, [loadSettings, scopeVersion])\n\n const rounding = draft ? draftRounding(draft) : null\n const roundingUnit = rounding?.unitMinutes ?? null\n const roundingDirection = rounding?.direction ?? null\n\n // The impact preview follows the CANDIDATE rule, not the stored one, so the\n // warning card answers \"what would this do\" before Save is pressed.\n React.useEffect(() => {\n if (roundingUnit === null || roundingDirection === null) return\n let cancelled = false\n setImpactState('loading')\n const timer = setTimeout(() => {\n void (async () => {\n try {\n const call = await apiCall<RoundingImpactResponse>(\n `${IMPACT_API}?unitMinutes=${roundingUnit}&direction=${roundingDirection}`,\n )\n if (cancelled) return\n if (!call.ok || !call.result) {\n setImpactState('error')\n return\n }\n setImpact(call.result)\n setImpactState('idle')\n } catch (err) {\n if (cancelled) return\n logger.error('rounding impact preview failed', { err })\n setImpactState('error')\n }\n })()\n }, IMPACT_DEBOUNCE_MS)\n return () => {\n cancelled = true\n clearTimeout(timer)\n }\n }, [roundingUnit, roundingDirection, scopeVersion])\n\n const patchDraft = React.useCallback((patch: Partial<TimeTrackingSettingsDraft>) => {\n setDraft((current) => (current ? { ...current, ...patch } : current))\n }, [])\n\n const fieldErrors = draft ? settingsDraftErrors(draft) : []\n const isDirty = draft && baseline ? isSettingsDraftDirty(draft, baseline) : false\n /**\n * EP-42/EP-26 \u2014 a contributed setting key is rendered by a widget on this spot, and\n * the widget edits it through `setValue`: the value lands in the page draft, the\n * page's own Save sends it with everything else, and the tenant-scoped PUT stores it.\n * `values` is keyed by the key's `<group>.<key>` id and holds contributed keys only.\n */\n const setContributedSetting = React.useCallback((id: string, value: unknown) => {\n setDraft((current) =>\n current ? { ...current, contributed: { ...current.contributed, [id]: value } } : current,\n )\n }, [])\n const contributedValues = draft?.contributed ?? EMPTY_CONTRIBUTED_SETTINGS\n const settingsInjectionContext = React.useMemo(\n () => ({\n moduleId: TIME_TRACKING_SETTINGS_MODULE_ID,\n canManage,\n keys: contributedTimeTrackingSettingKeys(),\n values: contributedValues,\n setValue: setContributedSetting,\n }),\n [canManage, contributedValues, setContributedSetting],\n )\n const canSave = canManage && isDirty && fieldErrors.length === 0 && !isSaving && !isLoading\n\n /**\n * optimistic-lock-exempt: tenant time-tracking settings are a single config\n * blob written through `configService` (`api/timesheets/settings/route.ts`),\n * not a row with an `updated_at` column \u2014 there is no version to send and no\n * per-record conflict to report. The write is still guarded (mutation guards\n * + `staff.timesheets.settings.manage`), and the one destructive consequence\n * of a settings change \u2014 retroactive rounding \u2014 is a separate, explicitly\n * confirmed action that skips entries locked into closed reports.\n */\n const handleSave = React.useCallback(async () => {\n if (!draft) return\n const body = toSettingsPayload(draft)\n if (!body) return\n const saveLoadId = latestLoadRef.current\n const saveScope = scopeRef.current\n const saveTenantId = saveScope.tenantId\n const saveUrl = saveTenantId\n ? `${SETTINGS_API}?expectedTenantId=${encodeURIComponent(saveTenantId)}`\n : SETTINGS_API\n const reportTenantScopeChanged = () => {\n flash(\n t(\n 'staff.time_tracking.settings.tenantChanged',\n 'The tenant changed before the settings were saved. Nothing was saved.',\n ),\n 'warning',\n )\n void loadSettings({ discardDraft: true })\n }\n setIsSaving(true)\n try {\n const call = await runMutation({\n operation: async () => {\n if (saveTenantId !== null && scopeRef.current.tenantId !== saveTenantId) {\n throw new TenantScopeChangedError()\n }\n const response = await apiCall<TimeTrackingSettings>(saveUrl, {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (isTenantScopeChangedResponse(response.status, response.result)) {\n throw new TenantScopeChangedError()\n }\n return response\n },\n context: {\n formId: MUTATION_CONTEXT_ID,\n resourceKind: RESOURCE_KIND,\n resourceId: RESOURCE_ID,\n retryLastMutation,\n },\n mutationPayload: body as unknown as Record<string, unknown>,\n })\n if (!call.ok || !call.result) {\n flash(t('staff.time_tracking.settings.saveError', 'Failed to save the settings.'), 'error')\n return\n }\n const scopeMoved =\n saveLoadId !== latestLoadRef.current\n || saveScope.tenantId !== scopeRef.current.tenantId\n || saveScope.organizationId !== scopeRef.current.organizationId\n if (scopeMoved) {\n void loadSettings()\n return\n }\n const saved = toSettingsDraft(call.result)\n setBaseline(saved)\n setDraft(saved)\n flash(t('staff.time_tracking.settings.saved', 'Settings saved.'), 'success')\n } catch (err) {\n if (err instanceof TenantScopeChangedError) {\n reportTenantScopeChanged()\n return\n }\n logger.error('time tracking settings save failed', { err })\n flash(t('staff.time_tracking.settings.saveError', 'Failed to save the settings.'), 'error')\n } finally {\n setIsSaving(false)\n }\n }, [draft, loadSettings, retryLastMutation, runMutation, t])\n\n const handleReapply = React.useCallback(async () => {\n const candidateCount = impact?.projected.entryCount ?? 0\n const confirmed = await confirm({\n title: t('staff.time_tracking.settings.retro.confirmTitle', 'Reapply rounding to existing entries?'),\n text: t(\n 'staff.time_tracking.settings.retro.confirmText',\n 'Rounded time will be recomputed on existing entries using the saved rule. Entries locked in closed reports are never changed.',\n ),\n confirmText: t('staff.time_tracking.settings.retro.cta', 'Reapply to existing entries'),\n })\n if (!confirmed) return\n setIsReapplying(true)\n try {\n const call = await runMutation({\n operation: () =>\n apiCall<{ progressJobId: string | null; candidateCount: number }>(REAPPLY_API, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({}),\n }),\n context: {\n formId: MUTATION_CONTEXT_ID,\n resourceKind: RESOURCE_KIND,\n resourceId: RESOURCE_ID,\n retryLastMutation,\n },\n mutationPayload: { action: 'reapply-rounding', candidateCount },\n })\n if (!call.ok) {\n flash(t('staff.time_tracking.settings.retro.error', 'Failed to start the recalculation.'), 'error')\n return\n }\n if (!call.result?.progressJobId) {\n flash(t('staff.time_tracking.settings.retro.nothing', 'There is nothing to recalculate.'), 'info')\n return\n }\n flash(\n t(\n 'staff.time_tracking.settings.retro.queued',\n 'Recalculating rounded time on {count} entries. Progress is shown in the top bar.',\n { count: call.result.candidateCount },\n ),\n 'success',\n )\n } catch (err) {\n logger.error('reapply rounding request failed', { err })\n flash(t('staff.time_tracking.settings.retro.error', 'Failed to start the recalculation.'), 'error')\n } finally {\n setIsReapplying(false)\n }\n }, [confirm, impact, retryLastMutation, runMutation, t])\n\n const examples = rounding ? buildRoundingExamples(rounding) : []\n\n const ruleLabel = React.useMemo(() => {\n if (!rounding) return ''\n if (rounding.unitMinutes === 0) {\n return t('staff.time_tracking.settings.impact.rule.none', 'no rounding')\n }\n return rounding.direction === 'up'\n ? t('staff.time_tracking.settings.impact.rule.up', '{unit} min up', { unit: rounding.unitMinutes })\n : t('staff.time_tracking.settings.impact.rule.nearest', '{unit} min to nearest', {\n unit: rounding.unitMinutes,\n })\n }, [rounding, t])\n\n if (isLoading && !draft) {\n return (\n <Page>\n <PageHeader title={t('staff.time_tracking.settings.title', 'Module settings')} />\n <PageBody>\n <LoadingMessage label={t('staff.time_tracking.settings.loading', 'Loading settings\u2026')} />\n </PageBody>\n </Page>\n )\n }\n\n if (loadError || !draft) {\n return (\n <Page>\n <PageHeader title={t('staff.time_tracking.settings.title', 'Module settings')} />\n <PageBody>\n <ErrorMessage\n label={loadError ?? t('staff.time_tracking.settings.loadError', 'Failed to load the settings.')}\n action={\n <Button type=\"button\" variant=\"outline\" onClick={() => void loadSettings()}>\n {t('staff.time_tracking.settings.retry', 'Try again')}\n </Button>\n }\n />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageHeader\n title={t('staff.time_tracking.settings.title', 'Module settings')}\n description={t(\n 'staff.time_tracking.settings.subtitle',\n 'Rules shared by all customers and projects',\n )}\n />\n <PageBody>\n <div className=\"grid gap-6 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]\">\n <div className=\"flex flex-col gap-6\">\n <Card>\n <CardHeader>\n <CardTitle>{t('staff.time_tracking.settings.rounding.title', 'Time rounding')}</CardTitle>\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'staff.time_tracking.settings.rounding.description',\n 'Applies to all projects. The rounded value is the one cost is calculated from.',\n )}\n </p>\n </CardHeader>\n <CardContent className=\"flex flex-col gap-4\">\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"flex flex-col gap-2\">\n <Label>{t('staff.time_tracking.settings.rounding.unit', 'Unit')}</Label>\n <SegmentedControl\n value={String(draft.roundingUnitMinutes)}\n onValueChange={(value) => {\n const parsed = Number(value)\n if (!ROUNDING_UNIT_OPTIONS.includes(parsed as RoundingUnitMinutes)) return\n patchDraft({ roundingUnitMinutes: parsed as RoundingUnitMinutes })\n }}\n disabled={!canManage}\n aria-label={t('staff.time_tracking.settings.rounding.unit', 'Unit')}\n >\n {ROUNDING_UNIT_OPTIONS.map((unit) => (\n <SegmentedControlItem key={unit} value={String(unit)}>\n {unit === 0\n ? t('staff.time_tracking.settings.rounding.unit.none', 'None')\n : t('staff.time_tracking.settings.rounding.unit.minutes', '{count} min', {\n count: unit,\n })}\n </SegmentedControlItem>\n ))}\n </SegmentedControl>\n </div>\n <div className=\"flex flex-col gap-2\">\n <Label>{t('staff.time_tracking.settings.rounding.direction', 'Direction')}</Label>\n <SegmentedControl\n value={draft.roundingDirection}\n onValueChange={(value) => {\n if (value !== 'up' && value !== 'nearest') return\n patchDraft({ roundingDirection: value })\n }}\n disabled={!canManage || draft.roundingUnitMinutes === 0}\n aria-label={t('staff.time_tracking.settings.rounding.direction', 'Direction')}\n >\n <SegmentedControlItem value=\"up\">\n {t('staff.time_tracking.settings.rounding.direction.up', 'Up')}\n </SegmentedControlItem>\n <SegmentedControlItem value=\"nearest\">\n {t('staff.time_tracking.settings.rounding.direction.nearest', 'To nearest')}\n </SegmentedControlItem>\n </SegmentedControl>\n </div>\n </div>\n\n <Alert status=\"information\" icon={<Info aria-hidden=\"true\" />}>\n <AlertTitle>\n {t('staff.time_tracking.settings.rounding.examples.title', 'Examples at the current setting')}\n </AlertTitle>\n <AlertDescription>\n <span className=\"font-mono text-sm\" data-testid=\"rounding-examples\">\n {examples\n .map((example) => `${example.rawLabel} \u2192 ${example.roundedLabel}`)\n .join(' \u00B7 ')}\n </span>\n </AlertDescription>\n </Alert>\n\n <div className=\"flex flex-col gap-2 rounded-md border border-border p-4\">\n <span className=\"text-sm font-medium\">\n {t(\n 'staff.time_tracking.settings.retro.title',\n 'Apply retroactively to existing entries',\n )}\n </span>\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'staff.time_tracking.settings.retro.hint',\n 'Saving changes new entries only. Recalculating existing ones is a separate, explicit action \u2014 and it never changes entries locked in closed reports.',\n )}\n </span>\n <div>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => void handleReapply()}\n disabled={!canManage || isReapplying || isDirty}\n data-testid=\"reapply-rounding\"\n >\n {isReapplying ? <Spinner className=\"size-4\" /> : <History className=\"size-4\" aria-hidden=\"true\" />}\n {t('staff.time_tracking.settings.retro.cta', 'Reapply to existing entries')}\n </Button>\n </div>\n {isDirty ? (\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'staff.time_tracking.settings.retro.saveFirst',\n 'Save the rule first \u2014 the recalculation uses the saved rule, not the one on screen.',\n )}\n </span>\n ) : null}\n </div>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('staff.time_tracking.settings.defaults.title', 'Entry defaults')}</CardTitle>\n </CardHeader>\n <CardContent className=\"flex flex-col gap-4\">\n <SwitchField\n label={t('staff.time_tracking.settings.defaults.billable', 'New entries are billable')}\n description={t(\n 'staff.time_tracking.settings.defaults.billableHint',\n 'A project can override this.',\n )}\n checked={draft.defaultsBillable}\n onCheckedChange={(value) => patchDraft({ defaultsBillable: value === true })}\n disabled={!canManage}\n />\n <SwitchField\n label={t(\n 'staff.time_tracking.settings.defaults.chainStart',\n 'Start = end of the previous entry',\n )}\n description={t(\n 'staff.time_tracking.settings.defaults.chainStartHint',\n 'A suggestion when adding another entry on the same day.',\n )}\n checked={draft.defaultsChainStartFromPreviousEnd}\n onCheckedChange={(value) =>\n patchDraft({ defaultsChainStartFromPreviousEnd: value === true })\n }\n disabled={!canManage}\n />\n <div className=\"flex flex-col gap-2\">\n <Label htmlFor=\"time-tracking-daily-hours\">\n {t('staff.time_tracking.settings.targets.dailyHours', 'Daily hours target')}\n </Label>\n <div className=\"flex items-center gap-2\">\n <Input\n id=\"time-tracking-daily-hours\"\n className=\"max-w-28\"\n inputMode=\"decimal\"\n value={draft.dailyHoursText}\n onChange={(event) => patchDraft({ dailyHoursText: event.target.value })}\n disabled={!canManage}\n aria-invalid={fieldErrors.includes('dailyHours')}\n />\n <span className=\"text-sm text-muted-foreground\">\n {t('staff.time_tracking.settings.targets.hoursUnit', 'h')}\n </span>\n </div>\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'staff.time_tracking.settings.targets.dailyHoursHint',\n 'Scales the load bars in the timesheet. Leave empty to turn targets off.',\n )}\n </span>\n {fieldErrors.includes('dailyHours') ? (\n <span className=\"text-xs text-status-error-text\">\n {t(\n 'staff.time_tracking.settings.targets.dailyHoursError',\n 'Enter a number of hours between 0 and 24, or leave the field empty.',\n )}\n </span>\n ) : null}\n </div>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('staff.time_tracking.settings.warnings.title', 'Warnings')}</CardTitle>\n </CardHeader>\n <CardContent className=\"flex flex-col gap-4\">\n <SwitchField\n label={t('staff.time_tracking.settings.warnings.overlap', 'Warn about overlapping entries')}\n description={t('staff.time_tracking.settings.warnings.overlapHint', 'A warning, not a block.')}\n checked={draft.warningsOverlap}\n onCheckedChange={(value) => patchDraft({ warningsOverlap: value === true })}\n disabled={!canManage}\n />\n <SwitchField\n label={t('staff.time_tracking.settings.warnings.runningTimer', 'Remind about a running timer')}\n description={t(\n 'staff.time_tracking.settings.warnings.runningTimerHint',\n 'An unobtrusive reminder when leaving the module and after 8 h of continuous running.',\n )}\n checked={draft.warningsRunningTimer}\n onCheckedChange={(value) => patchDraft({ warningsRunningTimer: value === true })}\n disabled={!canManage}\n />\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('staff.time_tracking.settings.access.title', 'Project access')}</CardTitle>\n </CardHeader>\n <CardContent className=\"flex flex-col gap-2\">\n <Label htmlFor=\"time-tracking-grace-days\">\n {t(\n 'staff.time_tracking.settings.access.graceDays',\n 'Access grace period after an assignment ends',\n )}\n </Label>\n <div className=\"flex items-center gap-2\">\n <Input\n id=\"time-tracking-grace-days\"\n className=\"max-w-28\"\n inputMode=\"numeric\"\n value={draft.assignmentGraceDaysText}\n onChange={(event) => patchDraft({ assignmentGraceDaysText: event.target.value })}\n disabled={!canManage}\n aria-invalid={fieldErrors.includes('assignmentGraceDays')}\n />\n <span className=\"text-sm text-muted-foreground\">\n {t('staff.time_tracking.settings.access.daysUnit', 'days')}\n </span>\n </div>\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'staff.time_tracking.settings.access.graceDaysHint',\n 'A member keeps access to a project for this many days after their assignment end date, so the last week of work can still be logged and corrected. 0 ends access on the end date itself.',\n )}\n </span>\n {fieldErrors.includes('assignmentGraceDays') ? (\n <span className=\"text-xs text-status-error-text\">\n {t(\n 'staff.time_tracking.settings.access.graceDaysError',\n 'Enter a whole number of days between 0 and 365.',\n )}\n </span>\n ) : null}\n </CardContent>\n </Card>\n\n <InjectionSpot\n spotId={extensionPoints.hosts.timeTrackingSettingsSections.spotId}\n context={settingsInjectionContext}\n data={draft}\n />\n </div>\n\n <div className=\"flex flex-col gap-6\">\n <Alert status=\"warning\" icon={<AlertTriangle aria-hidden=\"true\" />}>\n <AlertTitle>\n {t('staff.time_tracking.settings.impact.title', 'Changing rounding affects amounts')}\n </AlertTitle>\n <AlertDescription>\n {impactState === 'loading' && !impact ? (\n <span className=\"text-sm\">\n {t('staff.time_tracking.settings.impact.loading', 'Calculating the impact\u2026')}\n </span>\n ) : impactState === 'error' ? (\n <span className=\"text-sm\">\n {t('staff.time_tracking.settings.impact.error', 'The impact preview is unavailable.')}\n </span>\n ) : impact && impact.projected.entryCount > 0 ? (\n <div className=\"flex flex-col gap-1\" data-testid=\"rounding-impact\">\n <span className=\"text-sm\">\n {t(\n 'staff.time_tracking.settings.impact.body',\n 'With the current {count} entries from the last {days} days, {rule} gives {rounded} ({delta} against raw time). Closed reports stay unchanged.',\n {\n count: impact.projected.entryCount,\n days: impact.windowDays,\n rule: ruleLabel,\n rounded: formatDuration(impact.projected.roundedMinutes, 'clock'),\n delta: formatSignedMinutes(impact.projected.deltaMinutes),\n },\n )}\n </span>\n {impact.lockedEntryCount > 0 ? (\n <span className=\"text-xs\">\n {t(\n 'staff.time_tracking.settings.impact.locked',\n '{count} entries in the window are locked in closed reports and are excluded.',\n { count: impact.lockedEntryCount },\n )}\n </span>\n ) : null}\n </div>\n ) : (\n <span className=\"text-sm\">\n {t(\n 'staff.time_tracking.settings.impact.empty',\n 'There are no unlocked entries in the last {days} days to project against.',\n { days: impact?.windowDays ?? 90 },\n )}\n </span>\n )}\n </AlertDescription>\n </Alert>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('staff.time_tracking.settings.scope.title', 'Settings scope')}</CardTitle>\n </CardHeader>\n <CardContent className=\"flex flex-col gap-2\">\n <span className=\"text-sm\">\n {t(\n 'staff.time_tracking.settings.scope.body',\n 'These rules are global \u2014 they cannot be set per customer or per project.',\n )}\n </span>\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'staff.time_tracking.settings.scope.hint',\n 'The rate and currency stay at project level, and a rate override stays at entry level.',\n )}\n </span>\n </CardContent>\n </Card>\n </div>\n </div>\n\n <div className=\"mt-6 flex items-center justify-end gap-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={() => setDraft(baseline)}\n disabled={!isDirty || isSaving || isLoading}\n >\n {t('staff.time_tracking.settings.actions.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" onClick={() => void handleSave()} disabled={!canSave} data-testid=\"save-settings\">\n {isSaving ? <Spinner className=\"size-4\" /> : <Save className=\"size-4\" aria-hidden=\"true\" />}\n {t('staff.time_tracking.settings.actions.save', 'Save')}\n </Button>\n </div>\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\n/**\n * Module settings \u2014 screen 16 (T7.1).\n *\n * One page, four groups, and one thing that is not decoration: the worked examples\n * under the rounding control recompute live from `roundMinutes`, the same helper the\n * write path uses (mockup note 1). Rounding is the rule people misread, and its\n * effect is otherwise invisible until an invoice goes out.\n *\n * Two deliberate departures from the mockup:\n *\n * * **\"Apply retroactively\" is an action, not a switch** (note 3 read strictly). A\n * switch that restates `rounded_minutes` across a tenant's history as a side\n * effect of saving a form is a silent rewrite of billed amounts. It is a button\n * that names the number of affected entries, asks for confirmation, and queues a\n * `ProgressJob` that can never touch a locked entry.\n * * **The access grace window is on this page.** It is live behaviour \u2014 a member\n * loses project access this many days after their assignment ends (D-12) \u2014 and it\n * had no UI at all before this screen.\n *\n * Every rule here is tenant-global by decision \u00A710; the card on the right says so,\n * because \"why can't I set this per project\" is otherwise a support ticket.\n *\n * The form above is the \"Time tracking\" tab. Modules add further tabs through the\n * `staff.time_tracking.settings:tabs` spot (staff's own Support routing is one); a tab\n * owns its saves, so the Save bar lives inside this tab only, and `?tab=` deep-links.\n */\n\nimport { InjectionSpot, useInjectionWidgets } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'\nimport * as React from 'react'\nimport { usePathname, useRouter, useSearchParams } from 'next/navigation'\nimport { AlertTriangle, History, Info, Save } from 'lucide-react'\nimport { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'\nimport { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Card, CardContent, CardHeader, CardTitle } from '@open-mercato/ui/primitives/card'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { SegmentedControl, SegmentedControlItem } from '@open-mercato/ui/primitives/segmented-control'\nimport { SwitchField } from '@open-mercato/ui/primitives/switch-field'\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { OrganizationScopeChangedDetail } from '@open-mercato/shared/lib/frontend/organizationEvents'\nimport { useOrganizationScopeDetail, useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { formatDuration } from '../../../../lib/time-tracking/duration'\nimport type { RoundingUnitMinutes } from '../../../../lib/time-tracking/rounding'\nimport type { TimeTrackingSettings } from '../../../../lib/time-tracking/settings'\nimport { contributedTimeTrackingSettingKeys } from '../../../../lib/time-tracking/settingKeys'\nimport {\n ROUNDING_UNIT_OPTIONS,\n buildRoundingExamples,\n draftRounding,\n formatSignedMinutes,\n isSettingsDraftDirty,\n rebaseSettingsDraft,\n settingsDraftErrors,\n toSettingsDraft,\n toSettingsPayload,\n type TimeTrackingSettingsDraft,\n} from '../../../../lib/time-tracking-ui/timeTrackingSettingsForm'\nimport {\n TIME_TRACKING_SETTINGS_GENERAL_TAB,\n TIME_TRACKING_SETTINGS_TAB_PARAM,\n type TimeTrackingSettingsTabContext,\n} from '../../../../lib/time-tracking-ui/settingsTabs'\n\nconst logger = createLogger('staff').child({ component: 'time-tracking/settings' })\n\nconst SETTINGS_API = '/api/staff/timesheets/settings'\nconst IMPACT_API = '/api/staff/timesheets/settings/rounding-impact'\nconst REAPPLY_API = '/api/staff/timesheets/settings/reapply-rounding'\nconst MANAGE_FEATURE = 'staff.timesheets.settings.manage'\nconst MUTATION_CONTEXT_ID = 'staff.time_tracking.settings'\nconst RESOURCE_KIND = 'staff.timesheets.settings'\nconst RESOURCE_ID = 'time_tracking'\nconst IMPACT_DEBOUNCE_MS = 350\nconst TENANT_SCOPE_CHANGED_CODE = 'tenant_scope_changed'\n\nclass TenantScopeChangedError extends Error {\n constructor() {\n super('[internal] tenant scope changed before the settings save was applied')\n this.name = 'TenantScopeChangedError'\n }\n}\n\nfunction isTenantScopeChangedResponse(status: number, result: unknown): boolean {\n return status === 409\n && typeof result === 'object'\n && result !== null\n && (result as { code?: unknown }).code === TENANT_SCOPE_CHANGED_CODE\n}\n\ntype MutationContext = {\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n}\n\ntype RoundingImpactTotals = {\n entryCount: number\n rawMinutes: number\n roundedMinutes: number\n deltaMinutes: number\n}\n\ntype RoundingImpactResponse = {\n windowDays: number\n projected: RoundingImpactTotals\n lockedEntryCount: number\n}\n\nconst TIME_TRACKING_SETTINGS_MODULE_ID = 'staff.time_tracking'\nconst EMPTY_CONTRIBUTED_SETTINGS: Record<string, unknown> = {}\n\nexport default function TimeTrackingSettingsPage() {\n const t = useT()\n const { payload } = useBackendChrome()\n const scopeVersion = useOrganizationScopeVersion()\n const scope = useOrganizationScopeDetail()\n const scopeRef = React.useRef(scope)\n scopeRef.current = scope\n const loadedScopeRef = React.useRef<OrganizationScopeChangedDetail | null>(null)\n const latestLoadRef = React.useRef(0)\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const canManage = hasFeature(payload?.grantedFeatures, MANAGE_FEATURE)\n const router = useRouter()\n const pathname = usePathname()\n const searchParams = useSearchParams()\n const activeTab = searchParams?.get(TIME_TRACKING_SETTINGS_TAB_PARAM) ?? TIME_TRACKING_SETTINGS_GENERAL_TAB\n\n const [savedSettings, setSavedSettings] = React.useState<TimeTrackingSettings | null>(null)\n const [baseline, setBaseline] = React.useState<TimeTrackingSettingsDraft | null>(null)\n const [draft, setDraft] = React.useState<TimeTrackingSettingsDraft | null>(null)\n const draftRef = React.useRef(draft)\n draftRef.current = draft\n const baselineRef = React.useRef(baseline)\n baselineRef.current = baseline\n const [isLoading, setIsLoading] = React.useState(true)\n const [loadError, setLoadError] = React.useState<string | null>(null)\n const [isSaving, setIsSaving] = React.useState(false)\n const [isReapplying, setIsReapplying] = React.useState(false)\n\n const [impact, setImpact] = React.useState<RoundingImpactResponse | null>(null)\n const [impactState, setImpactState] = React.useState<'idle' | 'loading' | 'error'>('idle')\n\n const { runMutation, retryLastMutation } = useGuardedMutation<MutationContext>({\n contextId: MUTATION_CONTEXT_ID,\n blockedMessage: t('staff.time_tracking.settings.saveError', 'Failed to save the settings.'),\n })\n\n const loadSettings = React.useCallback(async (options?: { discardDraft?: boolean }) => {\n const requestId = latestLoadRef.current + 1\n latestLoadRef.current = requestId\n const requestScope = { tenantId: scopeRef.current.tenantId, organizationId: scopeRef.current.organizationId }\n setIsLoading(true)\n setLoadError(null)\n try {\n const call = await apiCall<TimeTrackingSettings>(SETTINGS_API)\n if (requestId !== latestLoadRef.current) return\n if (!call.ok || !call.result) {\n setLoadError(t('staff.time_tracking.settings.loadError', 'Failed to load the settings.'))\n return\n }\n const next = toSettingsDraft(call.result)\n setSavedSettings(call.result)\n const pending = draftRef.current\n const previous = baselineRef.current\n const loadedScope = loadedScopeRef.current\n const scopeUnresolved = loadedScope === null || (loadedScope.tenantId === null && loadedScope.organizationId === null)\n const scopeUnchanged = loadedScope !== null\n && loadedScope.tenantId === requestScope.tenantId\n && loadedScope.organizationId === requestScope.organizationId\n const hasUnsavedEdit = pending !== null && previous !== null && isSettingsDraftDirty(pending, previous)\n const keepDraft = options?.discardDraft !== true && hasUnsavedEdit && (scopeUnresolved || scopeUnchanged)\n loadedScopeRef.current = requestScope\n setBaseline(next)\n if (!keepDraft) setDraft(next)\n } catch (err) {\n if (requestId !== latestLoadRef.current) return\n logger.error('time tracking settings load failed', { err })\n setLoadError(t('staff.time_tracking.settings.loadError', 'Failed to load the settings.'))\n } finally {\n if (requestId === latestLoadRef.current) setIsLoading(false)\n }\n }, [t])\n\n React.useEffect(() => {\n void loadSettings()\n }, [loadSettings, scopeVersion])\n\n const rounding = draft ? draftRounding(draft) : null\n const roundingUnit = rounding?.unitMinutes ?? null\n const roundingDirection = rounding?.direction ?? null\n\n // The impact preview follows the CANDIDATE rule, not the stored one, so the\n // warning card answers \"what would this do\" before Save is pressed.\n React.useEffect(() => {\n if (roundingUnit === null || roundingDirection === null) return\n let cancelled = false\n setImpactState('loading')\n const timer = setTimeout(() => {\n void (async () => {\n try {\n const call = await apiCall<RoundingImpactResponse>(\n `${IMPACT_API}?unitMinutes=${roundingUnit}&direction=${roundingDirection}`,\n )\n if (cancelled) return\n if (!call.ok || !call.result) {\n setImpactState('error')\n return\n }\n setImpact(call.result)\n setImpactState('idle')\n } catch (err) {\n if (cancelled) return\n logger.error('rounding impact preview failed', { err })\n setImpactState('error')\n }\n })()\n }, IMPACT_DEBOUNCE_MS)\n return () => {\n cancelled = true\n clearTimeout(timer)\n }\n }, [roundingUnit, roundingDirection, scopeVersion])\n\n const patchDraft = React.useCallback((patch: Partial<TimeTrackingSettingsDraft>) => {\n setDraft((current) => (current ? { ...current, ...patch } : current))\n }, [])\n\n const fieldErrors = draft ? settingsDraftErrors(draft) : []\n const isDirty = draft && baseline ? isSettingsDraftDirty(draft, baseline) : false\n /**\n * EP-42/EP-26 \u2014 a contributed setting key is rendered by a widget on this spot, and\n * the widget edits it through `setValue`: the value lands in the page draft, the\n * page's own Save sends it with everything else, and the tenant-scoped PUT stores it.\n * `values` is keyed by the key's `<group>.<key>` id and holds contributed keys only.\n */\n const setContributedSetting = React.useCallback((id: string, value: unknown) => {\n setDraft((current) =>\n current ? { ...current, contributed: { ...current.contributed, [id]: value } } : current,\n )\n }, [])\n const contributedValues = draft?.contributed ?? EMPTY_CONTRIBUTED_SETTINGS\n const settingsInjectionContext = React.useMemo(\n () => ({\n moduleId: TIME_TRACKING_SETTINGS_MODULE_ID,\n canManage,\n keys: contributedTimeTrackingSettingKeys(),\n values: contributedValues,\n setValue: setContributedSetting,\n }),\n [canManage, contributedValues, setContributedSetting],\n )\n\n const handleTabSettingsSaved = React.useCallback(\n (saved: TimeTrackingSettings) => {\n const nextBaseline = toSettingsDraft(saved)\n setSavedSettings(saved)\n setDraft((current) => (current && baseline ? rebaseSettingsDraft(current, baseline, nextBaseline) : nextBaseline))\n setBaseline(nextBaseline)\n },\n [baseline],\n )\n const tabInjectionContext = React.useMemo<TimeTrackingSettingsTabContext | null>(\n () =>\n savedSettings\n ? {\n moduleId: TIME_TRACKING_SETTINGS_MODULE_ID,\n canManage,\n settings: savedSettings,\n onSettingsSaved: handleTabSettingsSaved,\n }\n : null,\n [canManage, handleTabSettingsSaved, savedSettings],\n )\n const { widgets: tabWidgets } = useInjectionWidgets<TimeTrackingSettingsTabContext | null>(\n extensionPoints.hosts.timeTrackingSettingsTabs.spotId,\n { context: tabInjectionContext, triggerOnLoad: true },\n )\n const injectedTabs = React.useMemo(\n () =>\n tabWidgets\n .filter((widget) => (widget.placement?.kind ?? 'tab') === 'tab')\n .map((widget) => {\n const tabId = widget.placement?.groupId ?? widget.widgetId\n const groupLabel = widget.placement?.groupLabel\n const label = groupLabel ? t(groupLabel, widget.module.metadata.title ?? tabId) : widget.module.metadata.title ?? tabId\n const priority = typeof widget.placement?.priority === 'number' ? widget.placement.priority : 0\n return { tabId, label, priority, Widget: widget.module.Widget }\n })\n .sort((left, right) => right.priority - left.priority),\n [t, tabWidgets],\n )\n const selectedTab = injectedTabs.some((tab) => tab.tabId === activeTab) ? activeTab : TIME_TRACKING_SETTINGS_GENERAL_TAB\n const selectTab = React.useCallback(\n (tabId: string) => {\n const params = new URLSearchParams(searchParams?.toString() ?? '')\n if (tabId === TIME_TRACKING_SETTINGS_GENERAL_TAB) params.delete(TIME_TRACKING_SETTINGS_TAB_PARAM)\n else params.set(TIME_TRACKING_SETTINGS_TAB_PARAM, tabId)\n const query = params.toString()\n router.replace(query ? `${pathname}?${query}` : pathname ?? '')\n },\n [pathname, router, searchParams],\n )\n const canSave = canManage && isDirty && fieldErrors.length === 0 && !isSaving && !isLoading\n\n /**\n * optimistic-lock-exempt: tenant time-tracking settings are a single config\n * blob written through `configService` (`api/timesheets/settings/route.ts`),\n * not a row with an `updated_at` column \u2014 there is no version to send and no\n * per-record conflict to report. The write is still guarded (mutation guards\n * + `staff.timesheets.settings.manage`), and the one destructive consequence\n * of a settings change \u2014 retroactive rounding \u2014 is a separate, explicitly\n * confirmed action that skips entries locked into closed reports.\n */\n const handleSave = React.useCallback(async () => {\n if (!draft) return\n const body = toSettingsPayload(draft)\n if (!body) return\n const saveLoadId = latestLoadRef.current\n const saveScope = scopeRef.current\n const saveTenantId = saveScope.tenantId\n const saveUrl = saveTenantId\n ? `${SETTINGS_API}?expectedTenantId=${encodeURIComponent(saveTenantId)}`\n : SETTINGS_API\n const reportTenantScopeChanged = () => {\n flash(\n t(\n 'staff.time_tracking.settings.tenantChanged',\n 'The tenant changed before the settings were saved. Nothing was saved.',\n ),\n 'warning',\n )\n void loadSettings({ discardDraft: true })\n }\n setIsSaving(true)\n try {\n const call = await runMutation({\n operation: async () => {\n if (saveTenantId !== null && scopeRef.current.tenantId !== saveTenantId) {\n throw new TenantScopeChangedError()\n }\n const response = await apiCall<TimeTrackingSettings>(saveUrl, {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (isTenantScopeChangedResponse(response.status, response.result)) {\n throw new TenantScopeChangedError()\n }\n return response\n },\n context: {\n formId: MUTATION_CONTEXT_ID,\n resourceKind: RESOURCE_KIND,\n resourceId: RESOURCE_ID,\n retryLastMutation,\n },\n mutationPayload: body as unknown as Record<string, unknown>,\n })\n if (!call.ok || !call.result) {\n flash(t('staff.time_tracking.settings.saveError', 'Failed to save the settings.'), 'error')\n return\n }\n const scopeMoved =\n saveLoadId !== latestLoadRef.current\n || saveScope.tenantId !== scopeRef.current.tenantId\n || saveScope.organizationId !== scopeRef.current.organizationId\n if (scopeMoved) {\n void loadSettings()\n return\n }\n const saved = toSettingsDraft(call.result)\n setSavedSettings(call.result)\n setBaseline(saved)\n setDraft(saved)\n flash(t('staff.time_tracking.settings.saved', 'Settings saved.'), 'success')\n } catch (err) {\n if (err instanceof TenantScopeChangedError) {\n reportTenantScopeChanged()\n return\n }\n logger.error('time tracking settings save failed', { err })\n flash(t('staff.time_tracking.settings.saveError', 'Failed to save the settings.'), 'error')\n } finally {\n setIsSaving(false)\n }\n }, [draft, loadSettings, retryLastMutation, runMutation, t])\n\n const handleReapply = React.useCallback(async () => {\n const candidateCount = impact?.projected.entryCount ?? 0\n const confirmed = await confirm({\n title: t('staff.time_tracking.settings.retro.confirmTitle', 'Reapply rounding to existing entries?'),\n text: t(\n 'staff.time_tracking.settings.retro.confirmText',\n 'Rounded time will be recomputed on existing entries using the saved rule. Entries locked in closed reports are never changed.',\n ),\n confirmText: t('staff.time_tracking.settings.retro.cta', 'Reapply to existing entries'),\n })\n if (!confirmed) return\n setIsReapplying(true)\n try {\n const call = await runMutation({\n operation: () =>\n apiCall<{ progressJobId: string | null; candidateCount: number }>(REAPPLY_API, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({}),\n }),\n context: {\n formId: MUTATION_CONTEXT_ID,\n resourceKind: RESOURCE_KIND,\n resourceId: RESOURCE_ID,\n retryLastMutation,\n },\n mutationPayload: { action: 'reapply-rounding', candidateCount },\n })\n if (!call.ok) {\n flash(t('staff.time_tracking.settings.retro.error', 'Failed to start the recalculation.'), 'error')\n return\n }\n if (!call.result?.progressJobId) {\n flash(t('staff.time_tracking.settings.retro.nothing', 'There is nothing to recalculate.'), 'info')\n return\n }\n flash(\n t(\n 'staff.time_tracking.settings.retro.queued',\n 'Recalculating rounded time on {count} entries. Progress is shown in the top bar.',\n { count: call.result.candidateCount },\n ),\n 'success',\n )\n } catch (err) {\n logger.error('reapply rounding request failed', { err })\n flash(t('staff.time_tracking.settings.retro.error', 'Failed to start the recalculation.'), 'error')\n } finally {\n setIsReapplying(false)\n }\n }, [confirm, impact, retryLastMutation, runMutation, t])\n\n const examples = rounding ? buildRoundingExamples(rounding) : []\n\n const ruleLabel = React.useMemo(() => {\n if (!rounding) return ''\n if (rounding.unitMinutes === 0) {\n return t('staff.time_tracking.settings.impact.rule.none', 'no rounding')\n }\n return rounding.direction === 'up'\n ? t('staff.time_tracking.settings.impact.rule.up', '{unit} min up', { unit: rounding.unitMinutes })\n : t('staff.time_tracking.settings.impact.rule.nearest', '{unit} min to nearest', {\n unit: rounding.unitMinutes,\n })\n }, [rounding, t])\n\n if (isLoading && !draft) {\n return (\n <Page>\n <PageHeader title={t('staff.time_tracking.settings.title', 'Module settings')} />\n <PageBody>\n <LoadingMessage label={t('staff.time_tracking.settings.loading', 'Loading settings\u2026')} />\n </PageBody>\n </Page>\n )\n }\n\n if (loadError || !draft) {\n return (\n <Page>\n <PageHeader title={t('staff.time_tracking.settings.title', 'Module settings')} />\n <PageBody>\n <ErrorMessage\n label={loadError ?? t('staff.time_tracking.settings.loadError', 'Failed to load the settings.')}\n action={\n <Button type=\"button\" variant=\"outline\" onClick={() => void loadSettings()}>\n {t('staff.time_tracking.settings.retry', 'Try again')}\n </Button>\n }\n />\n </PageBody>\n </Page>\n )\n }\n\n const generalSettings = (\n <>\n <div className=\"grid gap-6 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]\">\n <div className=\"flex flex-col gap-6\">\n <Card>\n <CardHeader>\n <CardTitle>{t('staff.time_tracking.settings.rounding.title', 'Time rounding')}</CardTitle>\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'staff.time_tracking.settings.rounding.description',\n 'Applies to all projects. The rounded value is the one cost is calculated from.',\n )}\n </p>\n </CardHeader>\n <CardContent className=\"flex flex-col gap-4\">\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"flex flex-col gap-2\">\n <Label>{t('staff.time_tracking.settings.rounding.unit', 'Unit')}</Label>\n <SegmentedControl\n value={String(draft.roundingUnitMinutes)}\n onValueChange={(value) => {\n const parsed = Number(value)\n if (!ROUNDING_UNIT_OPTIONS.includes(parsed as RoundingUnitMinutes)) return\n patchDraft({ roundingUnitMinutes: parsed as RoundingUnitMinutes })\n }}\n disabled={!canManage}\n aria-label={t('staff.time_tracking.settings.rounding.unit', 'Unit')}\n >\n {ROUNDING_UNIT_OPTIONS.map((unit) => (\n <SegmentedControlItem key={unit} value={String(unit)}>\n {unit === 0\n ? t('staff.time_tracking.settings.rounding.unit.none', 'None')\n : t('staff.time_tracking.settings.rounding.unit.minutes', '{count} min', {\n count: unit,\n })}\n </SegmentedControlItem>\n ))}\n </SegmentedControl>\n </div>\n <div className=\"flex flex-col gap-2\">\n <Label>{t('staff.time_tracking.settings.rounding.direction', 'Direction')}</Label>\n <SegmentedControl\n value={draft.roundingDirection}\n onValueChange={(value) => {\n if (value !== 'up' && value !== 'nearest') return\n patchDraft({ roundingDirection: value })\n }}\n disabled={!canManage || draft.roundingUnitMinutes === 0}\n aria-label={t('staff.time_tracking.settings.rounding.direction', 'Direction')}\n >\n <SegmentedControlItem value=\"up\">\n {t('staff.time_tracking.settings.rounding.direction.up', 'Up')}\n </SegmentedControlItem>\n <SegmentedControlItem value=\"nearest\">\n {t('staff.time_tracking.settings.rounding.direction.nearest', 'To nearest')}\n </SegmentedControlItem>\n </SegmentedControl>\n </div>\n </div>\n\n <Alert status=\"information\" icon={<Info aria-hidden=\"true\" />}>\n <AlertTitle>\n {t('staff.time_tracking.settings.rounding.examples.title', 'Examples at the current setting')}\n </AlertTitle>\n <AlertDescription>\n <span className=\"font-mono text-sm\" data-testid=\"rounding-examples\">\n {examples\n .map((example) => `${example.rawLabel} \u2192 ${example.roundedLabel}`)\n .join(' \u00B7 ')}\n </span>\n </AlertDescription>\n </Alert>\n\n <div className=\"flex flex-col gap-2 rounded-md border border-border p-4\">\n <span className=\"text-sm font-medium\">\n {t(\n 'staff.time_tracking.settings.retro.title',\n 'Apply retroactively to existing entries',\n )}\n </span>\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'staff.time_tracking.settings.retro.hint',\n 'Saving changes new entries only. Recalculating existing ones is a separate, explicit action \u2014 and it never changes entries locked in closed reports.',\n )}\n </span>\n <div>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => void handleReapply()}\n disabled={!canManage || isReapplying || isDirty}\n data-testid=\"reapply-rounding\"\n >\n {isReapplying ? <Spinner className=\"size-4\" /> : <History className=\"size-4\" aria-hidden=\"true\" />}\n {t('staff.time_tracking.settings.retro.cta', 'Reapply to existing entries')}\n </Button>\n </div>\n {isDirty ? (\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'staff.time_tracking.settings.retro.saveFirst',\n 'Save the rule first \u2014 the recalculation uses the saved rule, not the one on screen.',\n )}\n </span>\n ) : null}\n </div>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('staff.time_tracking.settings.defaults.title', 'Entry defaults')}</CardTitle>\n </CardHeader>\n <CardContent className=\"flex flex-col gap-4\">\n <SwitchField\n label={t('staff.time_tracking.settings.defaults.billable', 'New entries are billable')}\n description={t(\n 'staff.time_tracking.settings.defaults.billableHint',\n 'A project can override this.',\n )}\n checked={draft.defaultsBillable}\n onCheckedChange={(value) => patchDraft({ defaultsBillable: value === true })}\n disabled={!canManage}\n />\n <SwitchField\n label={t(\n 'staff.time_tracking.settings.defaults.chainStart',\n 'Start = end of the previous entry',\n )}\n description={t(\n 'staff.time_tracking.settings.defaults.chainStartHint',\n 'A suggestion when adding another entry on the same day.',\n )}\n checked={draft.defaultsChainStartFromPreviousEnd}\n onCheckedChange={(value) =>\n patchDraft({ defaultsChainStartFromPreviousEnd: value === true })\n }\n disabled={!canManage}\n />\n <div className=\"flex flex-col gap-2\">\n <Label htmlFor=\"time-tracking-daily-hours\">\n {t('staff.time_tracking.settings.targets.dailyHours', 'Daily hours target')}\n </Label>\n <div className=\"flex items-center gap-2\">\n <Input\n id=\"time-tracking-daily-hours\"\n className=\"max-w-28\"\n inputMode=\"decimal\"\n value={draft.dailyHoursText}\n onChange={(event) => patchDraft({ dailyHoursText: event.target.value })}\n disabled={!canManage}\n aria-invalid={fieldErrors.includes('dailyHours')}\n />\n <span className=\"text-sm text-muted-foreground\">\n {t('staff.time_tracking.settings.targets.hoursUnit', 'h')}\n </span>\n </div>\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'staff.time_tracking.settings.targets.dailyHoursHint',\n 'Scales the load bars in the timesheet. Leave empty to turn targets off.',\n )}\n </span>\n {fieldErrors.includes('dailyHours') ? (\n <span className=\"text-xs text-status-error-text\">\n {t(\n 'staff.time_tracking.settings.targets.dailyHoursError',\n 'Enter a number of hours between 0 and 24, or leave the field empty.',\n )}\n </span>\n ) : null}\n </div>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('staff.time_tracking.settings.warnings.title', 'Warnings')}</CardTitle>\n </CardHeader>\n <CardContent className=\"flex flex-col gap-4\">\n <SwitchField\n label={t('staff.time_tracking.settings.warnings.overlap', 'Warn about overlapping entries')}\n description={t('staff.time_tracking.settings.warnings.overlapHint', 'A warning, not a block.')}\n checked={draft.warningsOverlap}\n onCheckedChange={(value) => patchDraft({ warningsOverlap: value === true })}\n disabled={!canManage}\n />\n <SwitchField\n label={t('staff.time_tracking.settings.warnings.runningTimer', 'Remind about a running timer')}\n description={t(\n 'staff.time_tracking.settings.warnings.runningTimerHint',\n 'An unobtrusive reminder when leaving the module and after 8 h of continuous running.',\n )}\n checked={draft.warningsRunningTimer}\n onCheckedChange={(value) => patchDraft({ warningsRunningTimer: value === true })}\n disabled={!canManage}\n />\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('staff.time_tracking.settings.access.title', 'Project access')}</CardTitle>\n </CardHeader>\n <CardContent className=\"flex flex-col gap-2\">\n <Label htmlFor=\"time-tracking-grace-days\">\n {t(\n 'staff.time_tracking.settings.access.graceDays',\n 'Access grace period after an assignment ends',\n )}\n </Label>\n <div className=\"flex items-center gap-2\">\n <Input\n id=\"time-tracking-grace-days\"\n className=\"max-w-28\"\n inputMode=\"numeric\"\n value={draft.assignmentGraceDaysText}\n onChange={(event) => patchDraft({ assignmentGraceDaysText: event.target.value })}\n disabled={!canManage}\n aria-invalid={fieldErrors.includes('assignmentGraceDays')}\n />\n <span className=\"text-sm text-muted-foreground\">\n {t('staff.time_tracking.settings.access.daysUnit', 'days')}\n </span>\n </div>\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'staff.time_tracking.settings.access.graceDaysHint',\n 'A member keeps access to a project for this many days after their assignment end date, so the last week of work can still be logged and corrected. 0 ends access on the end date itself.',\n )}\n </span>\n {fieldErrors.includes('assignmentGraceDays') ? (\n <span className=\"text-xs text-status-error-text\">\n {t(\n 'staff.time_tracking.settings.access.graceDaysError',\n 'Enter a whole number of days between 0 and 365.',\n )}\n </span>\n ) : null}\n </CardContent>\n </Card>\n\n <InjectionSpot\n spotId={extensionPoints.hosts.timeTrackingSettingsSections.spotId}\n context={settingsInjectionContext}\n data={draft}\n />\n </div>\n\n <div className=\"flex flex-col gap-6\">\n <Alert status=\"warning\" icon={<AlertTriangle aria-hidden=\"true\" />}>\n <AlertTitle>\n {t('staff.time_tracking.settings.impact.title', 'Changing rounding affects amounts')}\n </AlertTitle>\n <AlertDescription>\n {impactState === 'loading' && !impact ? (\n <span className=\"text-sm\">\n {t('staff.time_tracking.settings.impact.loading', 'Calculating the impact\u2026')}\n </span>\n ) : impactState === 'error' ? (\n <span className=\"text-sm\">\n {t('staff.time_tracking.settings.impact.error', 'The impact preview is unavailable.')}\n </span>\n ) : impact && impact.projected.entryCount > 0 ? (\n <div className=\"flex flex-col gap-1\" data-testid=\"rounding-impact\">\n <span className=\"text-sm\">\n {t(\n 'staff.time_tracking.settings.impact.body',\n 'With the current {count} entries from the last {days} days, {rule} gives {rounded} ({delta} against raw time). Closed reports stay unchanged.',\n {\n count: impact.projected.entryCount,\n days: impact.windowDays,\n rule: ruleLabel,\n rounded: formatDuration(impact.projected.roundedMinutes, 'clock'),\n delta: formatSignedMinutes(impact.projected.deltaMinutes),\n },\n )}\n </span>\n {impact.lockedEntryCount > 0 ? (\n <span className=\"text-xs\">\n {t(\n 'staff.time_tracking.settings.impact.locked',\n '{count} entries in the window are locked in closed reports and are excluded.',\n { count: impact.lockedEntryCount },\n )}\n </span>\n ) : null}\n </div>\n ) : (\n <span className=\"text-sm\">\n {t(\n 'staff.time_tracking.settings.impact.empty',\n 'There are no unlocked entries in the last {days} days to project against.',\n { days: impact?.windowDays ?? 90 },\n )}\n </span>\n )}\n </AlertDescription>\n </Alert>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('staff.time_tracking.settings.scope.title', 'Settings scope')}</CardTitle>\n </CardHeader>\n <CardContent className=\"flex flex-col gap-2\">\n <span className=\"text-sm\">\n {t(\n 'staff.time_tracking.settings.scope.body',\n 'These rules are global \u2014 they cannot be set per customer or per project.',\n )}\n </span>\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'staff.time_tracking.settings.scope.hint',\n 'The rate and currency stay at project level, and a rate override stays at entry level.',\n )}\n </span>\n </CardContent>\n </Card>\n </div>\n </div>\n\n <div className=\"mt-6 flex items-center justify-end gap-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={() => setDraft(baseline)}\n disabled={!isDirty || isSaving || isLoading}\n >\n {t('staff.time_tracking.settings.actions.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" onClick={() => void handleSave()} disabled={!canSave} data-testid=\"save-settings\">\n {isSaving ? <Spinner className=\"size-4\" /> : <Save className=\"size-4\" aria-hidden=\"true\" />}\n {t('staff.time_tracking.settings.actions.save', 'Save')}\n </Button>\n </div>\n </>\n )\n\n return (\n <Page>\n <PageHeader\n title={t('staff.time_tracking.settings.title', 'Module settings')}\n description={t(\n 'staff.time_tracking.settings.subtitle',\n 'Rules shared by all customers and projects',\n )}\n />\n <PageBody>\n {injectedTabs.length > 0 ? (\n <Tabs value={selectedTab} onValueChange={selectTab} variant=\"underline\">\n <TabsList aria-label={t('staff.time_tracking.settings.tabs.label', 'Settings sections')} className=\"mb-4\">\n <TabsTrigger value={TIME_TRACKING_SETTINGS_GENERAL_TAB}>\n {t('staff.time_tracking.settings.tabs.general', 'Time tracking')}\n </TabsTrigger>\n {injectedTabs.map((tab) => (\n <TabsTrigger key={tab.tabId} value={tab.tabId}>\n {tab.label}\n </TabsTrigger>\n ))}\n </TabsList>\n <TabsContent value={TIME_TRACKING_SETTINGS_GENERAL_TAB}>{generalSettings}</TabsContent>\n {injectedTabs.map((tab) => (\n <TabsContent key={tab.tabId} value={tab.tabId}>\n {tabInjectionContext ? <tab.Widget context={tabInjectionContext} /> : null}\n </TabsContent>\n ))}\n </Tabs>\n ) : (\n generalSettings\n )}\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
5
|
+
import { Page, PageBody } from "@open-mercato/ui/backend/Page";
|
|
6
|
+
import { DataTable } from "@open-mercato/ui/backend/DataTable";
|
|
7
|
+
import { ErrorMessage } from "@open-mercato/ui/backend/detail";
|
|
8
|
+
import { Badge } from "@open-mercato/ui/primitives/badge";
|
|
9
|
+
import { StatusBadge } from "@open-mercato/ui/primitives/status-badge";
|
|
10
|
+
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
11
|
+
import { useAppEvent } from "@open-mercato/ui/backend/injection/useAppEvent";
|
|
12
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
13
|
+
import { useOrganizationScopeVersion } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
|
|
14
|
+
import { formatDateTime } from "@open-mercato/shared/lib/time";
|
|
15
|
+
import { TaskDrawer } from "../../../../lib/time-tracking-ui/TaskDrawer.js";
|
|
16
|
+
import { readItems, readString, readTotal } from "../../../../lib/time-tracking-ui/kanbanBoardData.js";
|
|
17
|
+
import {
|
|
18
|
+
readSupportTicket,
|
|
19
|
+
SUPPORT_ACTIVITY_EVENT,
|
|
20
|
+
SUPPORT_PORTAL_STATUS_VARIANT
|
|
21
|
+
} from "../../../../lib/time-tracking-ui/supportTicketData.js";
|
|
22
|
+
import { supportPortalStatusLabel } from "../../../../lib/time-tracking-ui/SupportRequestPanel.js";
|
|
23
|
+
import { searchCustomers } from "../../../../lib/time-tracking-ui/CustomerPicker.js";
|
|
24
|
+
const PAGE_SIZE = 25;
|
|
25
|
+
const PROJECT_SEARCH_PAGE_SIZE = 50;
|
|
26
|
+
const OPTIONS_URL = "/api/staff/timesheets/support-tickets/options";
|
|
27
|
+
const UNASSIGNED_VALUE = "__unassigned__";
|
|
28
|
+
const ASSIGNEE_CHANGED_EVENT = "staff.timesheets.time_task.assignee_changed";
|
|
29
|
+
function toCategoryOptions(response) {
|
|
30
|
+
return (response?.categories ?? []).map(
|
|
31
|
+
(category) => typeof category.value === "string" ? { value: category.value, label: typeof category.label === "string" ? category.label : category.value } : null
|
|
32
|
+
).filter((option) => option !== null);
|
|
33
|
+
}
|
|
34
|
+
function toAssigneeOptions(response) {
|
|
35
|
+
return (response?.assignees ?? []).map(
|
|
36
|
+
(assignee) => typeof assignee.id === "string" ? { value: assignee.id, label: typeof assignee.displayName === "string" ? assignee.displayName : assignee.id } : null
|
|
37
|
+
).filter((option) => option !== null);
|
|
38
|
+
}
|
|
39
|
+
function readContext(row) {
|
|
40
|
+
const context = row._staff;
|
|
41
|
+
return context && typeof context === "object" ? context : null;
|
|
42
|
+
}
|
|
43
|
+
function toSupportRow(row) {
|
|
44
|
+
const id = readString(row, "id");
|
|
45
|
+
const ticket = readSupportTicket(row);
|
|
46
|
+
if (!id || !ticket) return null;
|
|
47
|
+
const context = readContext(row);
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
ticket,
|
|
51
|
+
projectName: typeof context?.projectName === "string" ? context.projectName : null,
|
|
52
|
+
assigneeName: typeof context?.assigneeName === "string" ? context.assigneeName : null,
|
|
53
|
+
updatedAt: readString(row, "updated_at", "updatedAt")
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function loadOptions(url, labelOf) {
|
|
57
|
+
const call = await apiCall(url);
|
|
58
|
+
if (!call.ok) return [];
|
|
59
|
+
return readItems(call.result).map((row) => {
|
|
60
|
+
const id = readString(row, "id");
|
|
61
|
+
const label = labelOf(row);
|
|
62
|
+
return id && label ? { value: id, label } : null;
|
|
63
|
+
}).filter((option) => option !== null);
|
|
64
|
+
}
|
|
65
|
+
function SupportTicketsPage() {
|
|
66
|
+
const t = useT();
|
|
67
|
+
const router = useRouter();
|
|
68
|
+
const searchParams = useSearchParams();
|
|
69
|
+
const scopeVersion = useOrganizationScopeVersion();
|
|
70
|
+
const [rows, setRows] = React.useState([]);
|
|
71
|
+
const [total, setTotal] = React.useState(0);
|
|
72
|
+
const [totalIsCapped, setTotalIsCapped] = React.useState(false);
|
|
73
|
+
const [page, setPage] = React.useState(1);
|
|
74
|
+
const [isLoading, setIsLoading] = React.useState(true);
|
|
75
|
+
const [loadError, setLoadError] = React.useState(false);
|
|
76
|
+
const [search, setSearch] = React.useState("");
|
|
77
|
+
const [filterValues, setFilterValues] = React.useState(
|
|
78
|
+
() => searchParams?.get("supportUnnotified") === "true" ? { supportUnnotified: true } : {}
|
|
79
|
+
);
|
|
80
|
+
const [refreshKey, setRefreshKey] = React.useState(0);
|
|
81
|
+
const [projectLabels, setProjectLabels] = React.useState({});
|
|
82
|
+
const [assigneeOptions, setAssigneeOptions] = React.useState([]);
|
|
83
|
+
const [categoryOptions, setCategoryOptions] = React.useState([]);
|
|
84
|
+
const [customerLabels, setCustomerLabels] = React.useState({});
|
|
85
|
+
const [optionsError, setOptionsError] = React.useState(false);
|
|
86
|
+
const drawerTaskId = searchParams?.get("task") ?? null;
|
|
87
|
+
const refresh = React.useCallback(() => setRefreshKey((value) => value + 1), []);
|
|
88
|
+
React.useEffect(() => {
|
|
89
|
+
let cancelled = false;
|
|
90
|
+
void (async () => {
|
|
91
|
+
try {
|
|
92
|
+
const options = await apiCall(OPTIONS_URL);
|
|
93
|
+
if (cancelled) return;
|
|
94
|
+
if (!options.ok) {
|
|
95
|
+
setOptionsError(true);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
setOptionsError(false);
|
|
99
|
+
setCategoryOptions(toCategoryOptions(options.result));
|
|
100
|
+
setAssigneeOptions(toAssigneeOptions(options.result));
|
|
101
|
+
} catch {
|
|
102
|
+
if (!cancelled) setOptionsError(true);
|
|
103
|
+
}
|
|
104
|
+
})();
|
|
105
|
+
return () => {
|
|
106
|
+
cancelled = true;
|
|
107
|
+
};
|
|
108
|
+
}, [scopeVersion]);
|
|
109
|
+
React.useEffect(() => {
|
|
110
|
+
let cancelled = false;
|
|
111
|
+
setIsLoading(true);
|
|
112
|
+
const params = new URLSearchParams({
|
|
113
|
+
source: "portal",
|
|
114
|
+
page: String(page),
|
|
115
|
+
pageSize: String(PAGE_SIZE),
|
|
116
|
+
sortField: "updatedAt",
|
|
117
|
+
sortDir: "desc"
|
|
118
|
+
});
|
|
119
|
+
const portalStatus = typeof filterValues.portalStatus === "string" ? filterValues.portalStatus : "";
|
|
120
|
+
if (portalStatus) params.set("portalStatus", portalStatus);
|
|
121
|
+
const category = typeof filterValues.supportCategory === "string" ? filterValues.supportCategory : "";
|
|
122
|
+
if (category) params.set("supportCategory", category);
|
|
123
|
+
const projectId = typeof filterValues.timeProjectId === "string" ? filterValues.timeProjectId : "";
|
|
124
|
+
if (projectId) params.set("timeProjectId", projectId);
|
|
125
|
+
const customerId = typeof filterValues.customerId === "string" ? filterValues.customerId : "";
|
|
126
|
+
if (customerId) params.set("customerId", customerId);
|
|
127
|
+
const assignee = typeof filterValues.assignee === "string" ? filterValues.assignee : "";
|
|
128
|
+
if (assignee === UNASSIGNED_VALUE) params.set("unassigned", "true");
|
|
129
|
+
else if (assignee) params.set("assigneeStaffMemberId", assignee);
|
|
130
|
+
if (filterValues.supportUnnotified === true || filterValues.supportUnnotified === "true") {
|
|
131
|
+
params.set("supportUnnotified", "true");
|
|
132
|
+
}
|
|
133
|
+
if (search.trim()) params.set("supportReference", search.trim());
|
|
134
|
+
void (async () => {
|
|
135
|
+
try {
|
|
136
|
+
const call = await apiCall(`/api/staff/timesheets/tasks?${params.toString()}`);
|
|
137
|
+
if (cancelled) return;
|
|
138
|
+
if (!call.ok) {
|
|
139
|
+
setLoadError(true);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
setLoadError(false);
|
|
143
|
+
setRows(readItems(call.result).map(toSupportRow).filter((row) => row !== null));
|
|
144
|
+
setTotal(readTotal(call.result, 0));
|
|
145
|
+
setTotalIsCapped(call.result?.totalIsCapped === true);
|
|
146
|
+
} catch {
|
|
147
|
+
if (!cancelled) setLoadError(true);
|
|
148
|
+
} finally {
|
|
149
|
+
if (!cancelled) setIsLoading(false);
|
|
150
|
+
}
|
|
151
|
+
})();
|
|
152
|
+
return () => {
|
|
153
|
+
cancelled = true;
|
|
154
|
+
};
|
|
155
|
+
}, [filterValues, page, refreshKey, scopeVersion, search]);
|
|
156
|
+
useAppEvent(SUPPORT_ACTIVITY_EVENT, () => refresh(), [refresh]);
|
|
157
|
+
useAppEvent(ASSIGNEE_CHANGED_EVENT, () => refresh(), [refresh]);
|
|
158
|
+
const openTask = React.useCallback(
|
|
159
|
+
(taskId) => {
|
|
160
|
+
const params = new URLSearchParams(searchParams?.toString() ?? "");
|
|
161
|
+
params.set("task", taskId);
|
|
162
|
+
router.push(`?${params.toString()}`, { scroll: false });
|
|
163
|
+
},
|
|
164
|
+
[router, searchParams]
|
|
165
|
+
);
|
|
166
|
+
const closeTask = React.useCallback(() => {
|
|
167
|
+
const params = new URLSearchParams(searchParams?.toString() ?? "");
|
|
168
|
+
params.delete("task");
|
|
169
|
+
const query = params.toString();
|
|
170
|
+
router.push(query.length > 0 ? `?${query}` : "?", { scroll: false });
|
|
171
|
+
}, [router, searchParams]);
|
|
172
|
+
const loadCustomerOptions = React.useCallback(
|
|
173
|
+
async (query) => {
|
|
174
|
+
const customers = await searchCustomers(query ?? "");
|
|
175
|
+
setCustomerLabels((current) => ({
|
|
176
|
+
...current,
|
|
177
|
+
...Object.fromEntries(customers.map((customer) => [customer.id, customer.name]))
|
|
178
|
+
}));
|
|
179
|
+
return customers.map((customer) => ({
|
|
180
|
+
value: customer.id,
|
|
181
|
+
label: customer.name,
|
|
182
|
+
description: customer.kind === "company" ? t("staff.support.tickets.customerKind.company", "Company") : t("staff.support.tickets.customerKind.person", "Person")
|
|
183
|
+
}));
|
|
184
|
+
},
|
|
185
|
+
[t]
|
|
186
|
+
);
|
|
187
|
+
const loadProjectOptions = React.useCallback(async (query) => {
|
|
188
|
+
const params = new URLSearchParams({ page: "1", pageSize: String(PROJECT_SEARCH_PAGE_SIZE) });
|
|
189
|
+
const trimmed = (query ?? "").trim();
|
|
190
|
+
if (trimmed) params.set("q", trimmed);
|
|
191
|
+
const projects = await loadOptions(
|
|
192
|
+
`/api/staff/timesheets/time-projects?${params.toString()}`,
|
|
193
|
+
(row) => readString(row, "name")
|
|
194
|
+
);
|
|
195
|
+
setProjectLabels((current) => ({
|
|
196
|
+
...current,
|
|
197
|
+
...Object.fromEntries(projects.map((project) => [project.value, project.label]))
|
|
198
|
+
}));
|
|
199
|
+
return projects;
|
|
200
|
+
}, []);
|
|
201
|
+
const filters = React.useMemo(
|
|
202
|
+
() => [
|
|
203
|
+
{
|
|
204
|
+
id: "portalStatus",
|
|
205
|
+
label: t("staff.support.tickets.filters.portalStatus", "Status the customer sees"),
|
|
206
|
+
type: "select",
|
|
207
|
+
options: [
|
|
208
|
+
{ value: "received", label: supportPortalStatusLabel(t, "received") },
|
|
209
|
+
{ value: "in_progress", label: supportPortalStatusLabel(t, "in_progress") },
|
|
210
|
+
{ value: "resolved", label: supportPortalStatusLabel(t, "resolved") }
|
|
211
|
+
]
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: "supportCategory",
|
|
215
|
+
label: t("staff.support.tickets.filters.category", "Category"),
|
|
216
|
+
type: "select",
|
|
217
|
+
options: categoryOptions
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: "timeProjectId",
|
|
221
|
+
label: t("staff.support.tickets.filters.project", "Project"),
|
|
222
|
+
type: "combobox",
|
|
223
|
+
placeholder: t("staff.support.tickets.filters.projectPlaceholder", "Search projects"),
|
|
224
|
+
loadOptions: loadProjectOptions,
|
|
225
|
+
formatValue: (value) => projectLabels[value] ?? value
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: "customerId",
|
|
229
|
+
label: t("staff.support.tickets.filters.customer", "Customer"),
|
|
230
|
+
type: "combobox",
|
|
231
|
+
placeholder: t("staff.support.tickets.filters.customerPlaceholder", "Search customers"),
|
|
232
|
+
loadOptions: loadCustomerOptions,
|
|
233
|
+
formatValue: (value) => customerLabels[value] ?? value
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: "assignee",
|
|
237
|
+
label: t("staff.support.tickets.filters.assignee", "Assignee"),
|
|
238
|
+
type: "select",
|
|
239
|
+
options: [{ value: UNASSIGNED_VALUE, label: t("staff.support.tickets.unassigned", "Unassigned") }, ...assigneeOptions]
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: "supportUnnotified",
|
|
243
|
+
label: t("staff.support.tickets.filters.nobodyNotified", "Nobody notified"),
|
|
244
|
+
type: "checkbox"
|
|
245
|
+
}
|
|
246
|
+
],
|
|
247
|
+
[assigneeOptions, categoryOptions, customerLabels, loadCustomerOptions, loadProjectOptions, projectLabels, t]
|
|
248
|
+
);
|
|
249
|
+
const columns = React.useMemo(
|
|
250
|
+
() => [
|
|
251
|
+
{
|
|
252
|
+
accessorKey: "reference",
|
|
253
|
+
header: t("staff.support.tickets.columns.reference", "Ticket"),
|
|
254
|
+
cell: ({ row }) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
255
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: row.original.ticket.supportReference }),
|
|
256
|
+
row.original.ticket.unnotifiedAt ? /* @__PURE__ */ jsx(Badge, { variant: "warning", size: "sm", "data-testid": "support-ticket-unnotified", children: t("staff.support.tickets.nobodyNotified", "Nobody notified") }) : null
|
|
257
|
+
] })
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
accessorKey: "subject",
|
|
261
|
+
header: t("staff.support.tickets.columns.subject", "Subject"),
|
|
262
|
+
cell: ({ row }) => row.original.ticket.requestSubject ?? ""
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
accessorKey: "customer",
|
|
266
|
+
header: t("staff.support.tickets.columns.customer", "Customer"),
|
|
267
|
+
cell: ({ row }) => row.original.ticket.customerName ?? ""
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
accessorKey: "category",
|
|
271
|
+
header: t("staff.support.tickets.columns.category", "Category"),
|
|
272
|
+
cell: ({ row }) => row.original.ticket.categoryLabel
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
accessorKey: "portalStatus",
|
|
276
|
+
header: t("staff.support.tickets.columns.portalStatus", "Status"),
|
|
277
|
+
cell: ({ row }) => /* @__PURE__ */ jsx(StatusBadge, { variant: SUPPORT_PORTAL_STATUS_VARIANT[row.original.ticket.portalStatus], dot: true, children: supportPortalStatusLabel(t, row.original.ticket.portalStatus) })
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
accessorKey: "project",
|
|
281
|
+
header: t("staff.support.tickets.columns.project", "Project"),
|
|
282
|
+
cell: ({ row }) => row.original.projectName ?? ""
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
accessorKey: "assignee",
|
|
286
|
+
header: t("staff.support.tickets.columns.assignee", "Assignee"),
|
|
287
|
+
cell: ({ row }) => row.original.assigneeName ?? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("staff.support.tickets.unassigned", "Unassigned") })
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
accessorKey: "updatedAt",
|
|
291
|
+
header: t("staff.support.tickets.columns.lastActivity", "Last activity"),
|
|
292
|
+
cell: ({ row }) => row.original.updatedAt ? formatDateTime(row.original.updatedAt) ?? "" : ""
|
|
293
|
+
}
|
|
294
|
+
],
|
|
295
|
+
[t]
|
|
296
|
+
);
|
|
297
|
+
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
298
|
+
return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsxs(PageBody, { children: [
|
|
299
|
+
loadError ? /* @__PURE__ */ jsx(ErrorMessage, { label: t("staff.support.tickets.loadError", "Could not load the support tickets.") }) : null,
|
|
300
|
+
optionsError ? /* @__PURE__ */ jsx(
|
|
301
|
+
ErrorMessage,
|
|
302
|
+
{
|
|
303
|
+
label: t("staff.support.tickets.optionsLoadError", "Could not load the filter options.")
|
|
304
|
+
}
|
|
305
|
+
) : null,
|
|
306
|
+
/* @__PURE__ */ jsx(
|
|
307
|
+
DataTable,
|
|
308
|
+
{
|
|
309
|
+
title: t("staff.support.tickets.title", "Support Inbox"),
|
|
310
|
+
titleHeadingLevel: 1,
|
|
311
|
+
data: rows,
|
|
312
|
+
columns,
|
|
313
|
+
isLoading,
|
|
314
|
+
searchValue: search,
|
|
315
|
+
onSearchChange: (value) => {
|
|
316
|
+
setSearch(value);
|
|
317
|
+
setPage(1);
|
|
318
|
+
},
|
|
319
|
+
searchPlaceholder: t("staff.support.tickets.searchPlaceholder", "Search by ticket reference"),
|
|
320
|
+
filters,
|
|
321
|
+
filterValues,
|
|
322
|
+
onFiltersApply: (values) => {
|
|
323
|
+
setFilterValues(values);
|
|
324
|
+
setPage(1);
|
|
325
|
+
},
|
|
326
|
+
onFiltersClear: () => {
|
|
327
|
+
setFilterValues({});
|
|
328
|
+
setPage(1);
|
|
329
|
+
},
|
|
330
|
+
emptyState: /* @__PURE__ */ jsx("div", { className: "py-12 text-center text-sm text-muted-foreground", children: t("staff.support.tickets.empty", "No support tickets match these filters.") }),
|
|
331
|
+
refreshButton: {
|
|
332
|
+
label: t("staff.support.tickets.refresh", "Refresh"),
|
|
333
|
+
onRefresh: refresh,
|
|
334
|
+
isRefreshing: isLoading
|
|
335
|
+
},
|
|
336
|
+
pagination: {
|
|
337
|
+
page,
|
|
338
|
+
pageSize: PAGE_SIZE,
|
|
339
|
+
total,
|
|
340
|
+
totalPages,
|
|
341
|
+
totalIsCapped,
|
|
342
|
+
onPageChange: setPage
|
|
343
|
+
},
|
|
344
|
+
onRowClick: (row) => openTask(row.id)
|
|
345
|
+
}
|
|
346
|
+
),
|
|
347
|
+
drawerTaskId ? /* @__PURE__ */ jsx(
|
|
348
|
+
TaskDrawer,
|
|
349
|
+
{
|
|
350
|
+
taskId: drawerTaskId,
|
|
351
|
+
open: true,
|
|
352
|
+
onOpenChange: (next) => {
|
|
353
|
+
if (!next) closeTask();
|
|
354
|
+
},
|
|
355
|
+
onChanged: refresh
|
|
356
|
+
}
|
|
357
|
+
) : null
|
|
358
|
+
] }) });
|
|
359
|
+
}
|
|
360
|
+
export {
|
|
361
|
+
SupportTicketsPage as default
|
|
362
|
+
};
|
|
363
|
+
//# sourceMappingURL=page.js.map
|