@open-mercato/core 0.10.1-develop.7717.1.2befe9c634 → 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
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET|POST /api/staff/portal/support-tickets` — customer portal support intake.
|
|
3
|
+
*
|
|
4
|
+
* Every row is loaded with `portalOwnedTaskWhere` (tenant, organization, the
|
|
5
|
+
* signed-in portal user's own `customerEntityId` on the TASK, `source = 'portal'`,
|
|
6
|
+
* not deleted). The project's customer is never consulted, so tickets from many
|
|
7
|
+
* customers can share one project. Subject and description come from the encrypted
|
|
8
|
+
* `customer_request_snapshot`, never from the staff-editable task text, and the
|
|
9
|
+
* response carries no project, task reference, assignee, internal comment or money.
|
|
10
|
+
*/
|
|
11
|
+
import { NextResponse } from 'next/server'
|
|
12
|
+
import { z } from 'zod'
|
|
13
|
+
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
14
|
+
import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
|
|
15
|
+
import type { CommandBus } from '@open-mercato/shared/lib/commands'
|
|
16
|
+
import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
|
|
17
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
18
|
+
import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
|
|
19
|
+
import {
|
|
20
|
+
StaffSupportTicket,
|
|
21
|
+
StaffTimeTask,
|
|
22
|
+
StaffTimeTaskStatus,
|
|
23
|
+
} from '../../../data/entities'
|
|
24
|
+
import {
|
|
25
|
+
PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE,
|
|
26
|
+
PORTAL_SUPPORT_TICKETS_VIEW_FEATURE,
|
|
27
|
+
listPortalTicketPage,
|
|
28
|
+
portalOwnedTaskWhere,
|
|
29
|
+
resolveCustomerDisplay,
|
|
30
|
+
resolveSupportPortalScope,
|
|
31
|
+
} from '../../../lib/support/portalTickets'
|
|
32
|
+
import {
|
|
33
|
+
SUPPORT_TICKET_RESOURCE_KIND,
|
|
34
|
+
buildPortalCommandContext,
|
|
35
|
+
checkPortalRateLimit,
|
|
36
|
+
portalErrorResponse,
|
|
37
|
+
} from '../../../lib/support/portalRoute'
|
|
38
|
+
import { derivePortalStatus } from '../../../lib/support/portalStatus'
|
|
39
|
+
import {
|
|
40
|
+
staffSupportTicketCommandIds,
|
|
41
|
+
type SupportTicketCreateFromPortalInput,
|
|
42
|
+
type SupportTicketCreateResult,
|
|
43
|
+
} from '../../../commands/support-tickets'
|
|
44
|
+
|
|
45
|
+
export const metadata: { requireAuth?: boolean } = { requireAuth: false }
|
|
46
|
+
|
|
47
|
+
const portalStatusSchema = z.enum(['received', 'in_progress', 'resolved'])
|
|
48
|
+
|
|
49
|
+
const listQuerySchema = z.object({
|
|
50
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
51
|
+
pageSize: z.coerce.number().int().min(1).max(50).default(20),
|
|
52
|
+
status: portalStatusSchema.optional(),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const ticketListItemSchema = z.object({
|
|
56
|
+
id: z.string().uuid(),
|
|
57
|
+
supportReference: z.string(),
|
|
58
|
+
subject: z.string(),
|
|
59
|
+
category: z.object({ value: z.string(), label: z.string() }),
|
|
60
|
+
portalStatus: portalStatusSchema,
|
|
61
|
+
createdAt: z.string().nullable(),
|
|
62
|
+
updatedAt: z.string().nullable(),
|
|
63
|
+
lastActivityAt: z.string().nullable(),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const listResponseSchema = z.object({
|
|
67
|
+
items: z.array(ticketListItemSchema),
|
|
68
|
+
total: z.number().int(),
|
|
69
|
+
page: z.number().int(),
|
|
70
|
+
pageSize: z.number().int(),
|
|
71
|
+
totalPages: z.number().int(),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const createBodySchema = z.object({
|
|
75
|
+
categoryValue: z.string().trim().min(1).max(150),
|
|
76
|
+
subject: z.string().trim().min(3).max(200),
|
|
77
|
+
description: z.string().trim().min(1).max(10000),
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const createResponseSchema = z.object({
|
|
81
|
+
ok: z.literal(true),
|
|
82
|
+
id: z.string().uuid(),
|
|
83
|
+
supportReference: z.string(),
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
function isoOrNull(value: Date | null | undefined): string | null {
|
|
87
|
+
return value ? value.toISOString() : null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function statusIdsFor(
|
|
91
|
+
scope: { em: import('@mikro-orm/postgresql').EntityManager; tenantId: string; organizationId: string },
|
|
92
|
+
status: z.infer<typeof portalStatusSchema>,
|
|
93
|
+
): Promise<string[]> {
|
|
94
|
+
const where: Record<string, unknown> = { tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null }
|
|
95
|
+
if (status === 'resolved') where.isDone = true
|
|
96
|
+
else if (status === 'received') Object.assign(where, { isDone: false, isDefault: true })
|
|
97
|
+
else Object.assign(where, { isDone: false, isDefault: false })
|
|
98
|
+
const rows = await scope.em.find(StaffTimeTaskStatus, where, { fields: ['id'] })
|
|
99
|
+
return rows.map((row) => row.id)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function GET(req: Request) {
|
|
103
|
+
try {
|
|
104
|
+
const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_VIEW_FEATURE)
|
|
105
|
+
if (scope instanceof Response) return scope
|
|
106
|
+
const url = new URL(req.url)
|
|
107
|
+
const query = listQuerySchema.parse(Object.fromEntries(url.searchParams.entries()))
|
|
108
|
+
const statusIds = query.status ? await statusIdsFor(scope, query.status) : null
|
|
109
|
+
const page = await listPortalTicketPage(scope, {
|
|
110
|
+
statusIds,
|
|
111
|
+
limit: query.pageSize,
|
|
112
|
+
offset: (query.page - 1) * query.pageSize,
|
|
113
|
+
})
|
|
114
|
+
const total = page.total
|
|
115
|
+
const pageIds = page.rows.map((row) => row.id)
|
|
116
|
+
const lastActivityByTask = new Map(page.rows.map((row) => [row.id, row.lastActivityAt]))
|
|
117
|
+
const loadedTasks = pageIds.length
|
|
118
|
+
? await scope.em.find(StaffTimeTask, { id: { $in: pageIds }, ...portalOwnedTaskWhere(scope) })
|
|
119
|
+
: []
|
|
120
|
+
const taskById = new Map(loadedTasks.map((task) => [task.id, task]))
|
|
121
|
+
const tasks = pageIds.flatMap((id) => {
|
|
122
|
+
const task = taskById.get(id)
|
|
123
|
+
return task ? [task] : []
|
|
124
|
+
})
|
|
125
|
+
const decryptionScope = { tenantId: scope.tenantId, organizationId: scope.organizationId }
|
|
126
|
+
const taskIds = tasks.map((task) => task.id)
|
|
127
|
+
const tickets = taskIds.length
|
|
128
|
+
? await findWithDecryption(
|
|
129
|
+
scope.em,
|
|
130
|
+
StaffSupportTicket,
|
|
131
|
+
{ taskId: { $in: taskIds }, ...decryptionScope, deletedAt: null },
|
|
132
|
+
undefined,
|
|
133
|
+
decryptionScope,
|
|
134
|
+
)
|
|
135
|
+
: []
|
|
136
|
+
const ticketByTask = new Map(tickets.map((ticket) => [ticket.taskId, ticket]))
|
|
137
|
+
const taskStatusIds = [...new Set(tasks.map((task) => task.taskStatusId))]
|
|
138
|
+
const statuses = taskStatusIds.length
|
|
139
|
+
? await scope.em.find(StaffTimeTaskStatus, { id: { $in: taskStatusIds }, ...decryptionScope }, { fields: ['id', 'isDefault', 'isDone'] })
|
|
140
|
+
: []
|
|
141
|
+
const statusById = new Map(statuses.map((status) => [status.id, status]))
|
|
142
|
+
const items = tasks.flatMap((task) => {
|
|
143
|
+
const ticket = ticketByTask.get(task.id)
|
|
144
|
+
if (!ticket) return []
|
|
145
|
+
const request = ticket.customerRequestSnapshot
|
|
146
|
+
const lastActivity = lastActivityByTask.get(task.id) ?? task.createdAt
|
|
147
|
+
return [
|
|
148
|
+
{
|
|
149
|
+
id: task.id,
|
|
150
|
+
supportReference: ticket.supportReference,
|
|
151
|
+
subject: request && typeof request.subject === 'string' ? request.subject : '',
|
|
152
|
+
category: ticket.categorySnapshot,
|
|
153
|
+
portalStatus: derivePortalStatus(statusById.get(task.taskStatusId) ?? null),
|
|
154
|
+
createdAt: isoOrNull(task.createdAt),
|
|
155
|
+
updatedAt: isoOrNull(task.updatedAt),
|
|
156
|
+
lastActivityAt: isoOrNull(lastActivity),
|
|
157
|
+
},
|
|
158
|
+
]
|
|
159
|
+
})
|
|
160
|
+
return NextResponse.json(
|
|
161
|
+
listResponseSchema.parse({
|
|
162
|
+
items,
|
|
163
|
+
total,
|
|
164
|
+
page: query.page,
|
|
165
|
+
pageSize: query.pageSize,
|
|
166
|
+
totalPages: Math.max(1, Math.ceil(total / query.pageSize)),
|
|
167
|
+
}),
|
|
168
|
+
)
|
|
169
|
+
} catch (err) {
|
|
170
|
+
return portalErrorResponse(err, 'staff portal support tickets GET failed')
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function POST(req: Request) {
|
|
175
|
+
try {
|
|
176
|
+
const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE)
|
|
177
|
+
if (scope instanceof Response) return scope
|
|
178
|
+
const limited = await checkPortalRateLimit(scope, 'create')
|
|
179
|
+
if (limited) return limited
|
|
180
|
+
const body = createBodySchema.parse(await readJsonSafe<Record<string, unknown>>(req, {}))
|
|
181
|
+
const guarded = await runRouteMutationGuards({
|
|
182
|
+
container: scope.container,
|
|
183
|
+
req,
|
|
184
|
+
auth: { userId: scope.auth.sub, tenantId: scope.tenantId, organizationId: scope.organizationId, userFeatures: [] },
|
|
185
|
+
input: { resourceKind: SUPPORT_TICKET_RESOURCE_KIND, resourceId: null, operation: 'create', mutationPayload: body },
|
|
186
|
+
})
|
|
187
|
+
if (!guarded.ok) return guarded.response
|
|
188
|
+
const effectiveBody = guarded.modifiedPayload
|
|
189
|
+
? createBodySchema.parse({ ...body, ...guarded.modifiedPayload })
|
|
190
|
+
: body
|
|
191
|
+
const customer = await resolveCustomerDisplay(scope)
|
|
192
|
+
const input: SupportTicketCreateFromPortalInput = {
|
|
193
|
+
tenantId: scope.tenantId,
|
|
194
|
+
organizationId: scope.organizationId,
|
|
195
|
+
customerId: scope.customerId,
|
|
196
|
+
customerUserId: scope.auth.sub,
|
|
197
|
+
customerDisplayName: customer.displayName,
|
|
198
|
+
customerKind: customer.kind,
|
|
199
|
+
reporterDisplayName: scope.auth.displayName ?? null,
|
|
200
|
+
categoryValue: effectiveBody.categoryValue,
|
|
201
|
+
subject: effectiveBody.subject,
|
|
202
|
+
description: effectiveBody.description,
|
|
203
|
+
}
|
|
204
|
+
const commandBus = scope.container.resolve('commandBus') as CommandBus
|
|
205
|
+
const { result } = await commandBus.execute<SupportTicketCreateFromPortalInput, SupportTicketCreateResult>(
|
|
206
|
+
staffSupportTicketCommandIds.createFromPortal,
|
|
207
|
+
{ input, ctx: buildPortalCommandContext(scope, req) },
|
|
208
|
+
)
|
|
209
|
+
await guarded.runAfterSuccess()
|
|
210
|
+
return NextResponse.json(
|
|
211
|
+
createResponseSchema.parse({ ok: true, id: result.taskId, supportReference: result.supportReference }),
|
|
212
|
+
{ status: 201 },
|
|
213
|
+
)
|
|
214
|
+
} catch (err) {
|
|
215
|
+
const rejection = getCommandInterceptorHttpRejection(err)
|
|
216
|
+
if (rejection) return NextResponse.json(rejection.body, { status: rejection.status })
|
|
217
|
+
return portalErrorResponse(err, 'staff portal support tickets POST failed')
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const errorSchema = z.object({ ok: z.literal(false).optional(), error: z.string(), code: z.string().optional() })
|
|
222
|
+
|
|
223
|
+
export const openApi: OpenApiRouteDoc = {
|
|
224
|
+
tag: 'Staff Portal',
|
|
225
|
+
summary: 'Customer support tickets',
|
|
226
|
+
methods: {
|
|
227
|
+
GET: {
|
|
228
|
+
summary: 'List my support tickets',
|
|
229
|
+
description:
|
|
230
|
+
"Support tickets filed by the signed-in portal user's customer, newest activity first: `lastActivityAt` is the later of the filing time and the newest customer-visible message (from either side), and the list is ordered by it. Internal edits do not reorder the list. Status is the customer-facing status derived from the board column (received, in_progress, resolved).",
|
|
231
|
+
query: listQuerySchema,
|
|
232
|
+
responses: [{ status: 200, description: 'Tickets', schema: listResponseSchema }],
|
|
233
|
+
errors: [
|
|
234
|
+
{ status: 401, description: 'No portal session', schema: errorSchema },
|
|
235
|
+
{ status: 403, description: 'Not linked to a customer, or missing portal.support_tickets.view', schema: errorSchema },
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
POST: {
|
|
239
|
+
summary: 'File a support ticket',
|
|
240
|
+
description:
|
|
241
|
+
'Creates a ticket in the category the customer chose. The category routes to a project (its own rule, else the organization fallback rule); the ticket lands in that project default column. Rate limited per customer user.',
|
|
242
|
+
requestBody: { contentType: 'application/json', schema: createBodySchema },
|
|
243
|
+
responses: [{ status: 201, description: 'Ticket filed', schema: createResponseSchema }],
|
|
244
|
+
errors: [
|
|
245
|
+
{ status: 401, description: 'No portal session', schema: errorSchema },
|
|
246
|
+
{ status: 403, description: 'Missing portal.support_tickets.manage', schema: errorSchema },
|
|
247
|
+
{ status: 409, description: 'Too many tickets were filed at once; resubmit', schema: errorSchema },
|
|
248
|
+
{ status: 422, description: 'The category is not available', schema: errorSchema },
|
|
249
|
+
{ status: 429, description: 'Rate limit exceeded', schema: errorSchema },
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
runTimesheetInterceptors,
|
|
19
19
|
} from '../_shared/withTimesheetInterceptors'
|
|
20
20
|
import {
|
|
21
|
+
preserveOmittedSupportSettings,
|
|
21
22
|
readTimeTrackingSettings,
|
|
22
23
|
writeTimeTrackingSettings,
|
|
23
24
|
type TimeTrackingSettings,
|
|
@@ -152,7 +153,12 @@ async function PUT(req: Request) {
|
|
|
152
153
|
const { session } = interceptors
|
|
153
154
|
|
|
154
155
|
const settingsSchema = buildTimeTrackingSettingsSchema()
|
|
155
|
-
const
|
|
156
|
+
const requestBody = await preserveOmittedSupportSettings(
|
|
157
|
+
resolveConfigService(context),
|
|
158
|
+
{ tenantId: context.tenantId },
|
|
159
|
+
session.body,
|
|
160
|
+
)
|
|
161
|
+
const parsed = settingsSchema.parse(requestBody)
|
|
156
162
|
|
|
157
163
|
const guardResult = await runRouteMutationGuards({
|
|
158
164
|
container: context.container,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
|
+
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
5
|
+
import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
6
|
+
import { CrudHttpError, forbidden, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
7
|
+
import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
|
|
8
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
9
|
+
import type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
10
|
+
import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
|
|
11
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
12
|
+
import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
|
|
13
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
14
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
15
|
+
import { resolveFeatureAccess } from '../../../../lib/time-tracking/featureAccess'
|
|
16
|
+
import {
|
|
17
|
+
SUPPORT_ROUTING_RULE_RESOURCE_KIND,
|
|
18
|
+
staffSupportRoutingRuleCommandIds,
|
|
19
|
+
type SupportFallbackProjectResult,
|
|
20
|
+
} from '../../../../commands/support-routing-rules'
|
|
21
|
+
import type { StaffSupportFallbackProjectCreateInput } from '../../../../data/validators'
|
|
22
|
+
|
|
23
|
+
const logger = createLogger('staff').child({ component: 'api/timesheets/support-routing-rules/fallback-project' })
|
|
24
|
+
|
|
25
|
+
const SETTINGS_FEATURE = 'staff.timesheets.settings.manage'
|
|
26
|
+
|
|
27
|
+
export const metadata = {
|
|
28
|
+
POST: { requireAuth: true, requireFeatures: [SETTINGS_FEATURE] },
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function POST(req: Request): Promise<Response> {
|
|
32
|
+
try {
|
|
33
|
+
const container = await createRequestContainer()
|
|
34
|
+
const auth = await getAuthFromRequest(req)
|
|
35
|
+
const { translate } = await resolveTranslations()
|
|
36
|
+
if (!auth) throw new CrudHttpError(401, { error: translate('staff.errors.unauthorized', 'Unauthorized') })
|
|
37
|
+
|
|
38
|
+
const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
|
|
39
|
+
const tenantId = scope?.tenantId ?? auth.tenantId ?? null
|
|
40
|
+
const organizationId = scope?.selectedId ?? auth.orgId ?? null
|
|
41
|
+
if (!tenantId || !organizationId) {
|
|
42
|
+
throw new CrudHttpError(400, { error: translate('staff.errors.missingScope', 'Missing tenant or organization scope.') })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const actorId = auth.sub ?? ''
|
|
46
|
+
const access = await resolveFeatureAccess(container, actorId, [SETTINGS_FEATURE], { tenantId, organizationId })
|
|
47
|
+
if (!access.allowed) throw forbidden(translate('staff.errors.forbidden', 'Forbidden'))
|
|
48
|
+
|
|
49
|
+
const input: StaffSupportFallbackProjectCreateInput = { tenantId, organizationId }
|
|
50
|
+
const guardResult = await runRouteMutationGuards({
|
|
51
|
+
container,
|
|
52
|
+
req,
|
|
53
|
+
auth: { userId: actorId, tenantId, organizationId, userFeatures: access.grantedFeatures },
|
|
54
|
+
input: {
|
|
55
|
+
resourceKind: SUPPORT_ROUTING_RULE_RESOURCE_KIND,
|
|
56
|
+
resourceId: null,
|
|
57
|
+
operation: 'create',
|
|
58
|
+
mutationPayload: input,
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
if (!guardResult.ok) return guardResult.response
|
|
62
|
+
|
|
63
|
+
const routeCtx: CommandRuntimeContext = {
|
|
64
|
+
container,
|
|
65
|
+
auth,
|
|
66
|
+
organizationScope: scope,
|
|
67
|
+
selectedOrganizationId: organizationId,
|
|
68
|
+
organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),
|
|
69
|
+
request: req,
|
|
70
|
+
}
|
|
71
|
+
const commandBus = container.resolve('commandBus') as CommandBus
|
|
72
|
+
const { result, logEntry } = await commandBus.execute<StaffSupportFallbackProjectCreateInput, SupportFallbackProjectResult>(
|
|
73
|
+
staffSupportRoutingRuleCommandIds.createFallbackProject,
|
|
74
|
+
{ input, ctx: routeCtx },
|
|
75
|
+
)
|
|
76
|
+
await guardResult.runAfterSuccess()
|
|
77
|
+
const response = NextResponse.json(
|
|
78
|
+
{ ruleId: result?.ruleId ?? null, timeProjectId: result?.timeProjectId ?? null, created: result?.created === true },
|
|
79
|
+
{ status: result?.created ? 201 : 200 },
|
|
80
|
+
)
|
|
81
|
+
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
82
|
+
response.headers.set(
|
|
83
|
+
'x-om-operation',
|
|
84
|
+
serializeOperationMetadata({
|
|
85
|
+
id: logEntry.id,
|
|
86
|
+
undoToken: logEntry.undoToken,
|
|
87
|
+
commandId: logEntry.commandId,
|
|
88
|
+
actionLabel: logEntry.actionLabel ?? null,
|
|
89
|
+
resourceKind: logEntry.resourceKind ?? SUPPORT_ROUTING_RULE_RESOURCE_KIND,
|
|
90
|
+
resourceId: logEntry.resourceId ?? result?.ruleId ?? null,
|
|
91
|
+
executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,
|
|
92
|
+
}),
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
return response
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })
|
|
98
|
+
const interceptorRejection = getCommandInterceptorHttpRejection(err)
|
|
99
|
+
if (interceptorRejection) return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })
|
|
100
|
+
const { translate } = await resolveTranslations()
|
|
101
|
+
logger.error('staff.support.fallback-project POST failed', { err })
|
|
102
|
+
getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.support_fallback_project_failed' })
|
|
103
|
+
return NextResponse.json({ error: translate('staff.errors.internal', 'Internal server error') }, { status: 500 })
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const responseSchema = z.object({
|
|
108
|
+
ruleId: z.string().uuid().nullable(),
|
|
109
|
+
timeProjectId: z.string().uuid().nullable(),
|
|
110
|
+
created: z.boolean(),
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
export const openApi: OpenApiRouteDoc = {
|
|
114
|
+
tag: 'Staff',
|
|
115
|
+
summary: 'Create the support fallback project',
|
|
116
|
+
methods: {
|
|
117
|
+
POST: {
|
|
118
|
+
summary: 'Create the support fallback project and rule',
|
|
119
|
+
description:
|
|
120
|
+
'Creates the "Customer support" project (no customer, standard task statuses) and the organization fallback routing rule pointing at it. Idempotent: when a fallback rule already exists nothing is created and the existing rule is returned with `created: false`.',
|
|
121
|
+
responses: [
|
|
122
|
+
{ status: 201, description: 'Fallback project and rule created', schema: responseSchema },
|
|
123
|
+
{ status: 200, description: 'A fallback rule already existed', schema: responseSchema },
|
|
124
|
+
],
|
|
125
|
+
errors: [
|
|
126
|
+
{ status: 401, description: 'Unauthorized' },
|
|
127
|
+
{ status: 403, description: 'Missing staff.timesheets.settings.manage' },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
|
+
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
6
|
+
import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
7
|
+
import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
8
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
9
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
10
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
11
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
12
|
+
import { StaffTimeProject } from '../../../../data/entities'
|
|
13
|
+
import { resolveAssignmentGraceDaysFromContainer } from '../../../../lib/time-tracking/projectAssignment'
|
|
14
|
+
import { projectReachesRecipientBeforeManagers } from '../../../../lib/time-tracking/workRecipients'
|
|
15
|
+
import { loadSupportTicketCategories } from '../../../../lib/support/dictionaries'
|
|
16
|
+
import {
|
|
17
|
+
loadSupportRoutingRules,
|
|
18
|
+
loadUsableSupportProjectIds,
|
|
19
|
+
resolveSupportRouteFromRules,
|
|
20
|
+
type SupportRoutingRuleView,
|
|
21
|
+
} from '../../../../lib/support/routing'
|
|
22
|
+
|
|
23
|
+
const logger = createLogger('staff').child({ component: 'api/timesheets/support-routing-rules/preview' })
|
|
24
|
+
|
|
25
|
+
export const metadata = {
|
|
26
|
+
GET: { requireAuth: true, requireFeatures: ['staff.timesheets.settings.manage'] },
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type RuleProblem = 'paused' | 'project_unusable' | 'category_missing' | null
|
|
30
|
+
|
|
31
|
+
type UnnotifiedCountDb = {
|
|
32
|
+
staff_support_tickets: {
|
|
33
|
+
task_id: string
|
|
34
|
+
tenant_id: string
|
|
35
|
+
organization_id: string
|
|
36
|
+
unnotified_at: Date | null
|
|
37
|
+
deleted_at: Date | null
|
|
38
|
+
}
|
|
39
|
+
staff_time_tasks: {
|
|
40
|
+
id: string
|
|
41
|
+
tenant_id: string
|
|
42
|
+
organization_id: string
|
|
43
|
+
time_project_id: string
|
|
44
|
+
source: string
|
|
45
|
+
deleted_at: Date | null
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function countUnnotifiedTicketsByProject(
|
|
50
|
+
em: EntityManager,
|
|
51
|
+
scope: { tenantId: string; organizationId: string },
|
|
52
|
+
): Promise<Map<string, number>> {
|
|
53
|
+
const rows = await em
|
|
54
|
+
.getKysely<UnnotifiedCountDb>()
|
|
55
|
+
.selectFrom('staff_support_tickets')
|
|
56
|
+
.innerJoin('staff_time_tasks', 'staff_time_tasks.id', 'staff_support_tickets.task_id')
|
|
57
|
+
.select(['staff_time_tasks.time_project_id as time_project_id'])
|
|
58
|
+
.select((builder) => builder.fn.countAll<string | number>().as('ticket_count'))
|
|
59
|
+
.where('staff_support_tickets.tenant_id', '=', scope.tenantId)
|
|
60
|
+
.where('staff_support_tickets.organization_id', '=', scope.organizationId)
|
|
61
|
+
.where('staff_support_tickets.deleted_at', 'is', null)
|
|
62
|
+
.where('staff_support_tickets.unnotified_at', 'is not', null)
|
|
63
|
+
.where('staff_time_tasks.tenant_id', '=', scope.tenantId)
|
|
64
|
+
.where('staff_time_tasks.organization_id', '=', scope.organizationId)
|
|
65
|
+
.where('staff_time_tasks.source', '=', 'portal')
|
|
66
|
+
.where('staff_time_tasks.deleted_at', 'is', null)
|
|
67
|
+
.groupBy('staff_time_tasks.time_project_id')
|
|
68
|
+
.execute()
|
|
69
|
+
const counts = new Map<string, number>()
|
|
70
|
+
for (const row of rows) {
|
|
71
|
+
const count = Number(row.ticket_count)
|
|
72
|
+
if (Number.isFinite(count) && count > 0) counts.set(row.time_project_id, count)
|
|
73
|
+
}
|
|
74
|
+
return counts
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function GET(req: Request): Promise<Response> {
|
|
78
|
+
try {
|
|
79
|
+
const container = await createRequestContainer()
|
|
80
|
+
const auth = await getAuthFromRequest(req)
|
|
81
|
+
const { translate } = await resolveTranslations()
|
|
82
|
+
if (!auth) throw new CrudHttpError(401, { error: translate('staff.errors.unauthorized', 'Unauthorized') })
|
|
83
|
+
const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
|
|
84
|
+
const tenantId = scope?.tenantId ?? auth.tenantId ?? null
|
|
85
|
+
const organizationId = scope?.selectedId ?? auth.orgId ?? null
|
|
86
|
+
if (!tenantId || !organizationId) {
|
|
87
|
+
throw new CrudHttpError(400, { error: translate('staff.errors.missingScope', 'Missing tenant or organization scope.') })
|
|
88
|
+
}
|
|
89
|
+
const supportScope = { tenantId, organizationId }
|
|
90
|
+
const em = (container.resolve('em') as EntityManager).fork()
|
|
91
|
+
const [categories, rules] = await Promise.all([
|
|
92
|
+
loadSupportTicketCategories(em, supportScope),
|
|
93
|
+
loadSupportRoutingRules(em, supportScope),
|
|
94
|
+
])
|
|
95
|
+
const projectIds = rules.map((rule) => rule.timeProjectId)
|
|
96
|
+
const usable = await loadUsableSupportProjectIds(em, supportScope, projectIds)
|
|
97
|
+
const projects = projectIds.length
|
|
98
|
+
? await em.find(
|
|
99
|
+
StaffTimeProject,
|
|
100
|
+
{ id: { $in: Array.from(new Set(projectIds)) }, tenantId, organizationId },
|
|
101
|
+
{ fields: ['id', 'name', 'code', 'status', 'deletedAt', 'ownerUserId', 'defaultAssigneeStaffMemberId'] },
|
|
102
|
+
)
|
|
103
|
+
: []
|
|
104
|
+
const projectById = new Map(projects.map((project) => [project.id, project]))
|
|
105
|
+
const graceDays = await resolveAssignmentGraceDaysFromContainer(container, tenantId)
|
|
106
|
+
const [unnotifiedByProject, reachability] = await Promise.all([
|
|
107
|
+
countUnnotifiedTicketsByProject(em.fork(), supportScope),
|
|
108
|
+
Promise.all(
|
|
109
|
+
projects.map(async (project) => {
|
|
110
|
+
if (project.deletedAt) return [project.id, true] as const
|
|
111
|
+
const reaches = await projectReachesRecipientBeforeManagers(em.fork(), supportScope, project, { graceDays })
|
|
112
|
+
return [project.id, reaches] as const
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
])
|
|
116
|
+
const reachesById = new Map<string, boolean>(reachability)
|
|
117
|
+
let unnotifiedTicketCount = 0
|
|
118
|
+
for (const count of unnotifiedByProject.values()) unnotifiedTicketCount += count
|
|
119
|
+
const categoryValues = new Set(categories.map((category) => category.value))
|
|
120
|
+
const fallbackRule = rules.find((rule) => rule.categoryValue === null) ?? null
|
|
121
|
+
|
|
122
|
+
const describeProject = (id: string) => {
|
|
123
|
+
const project = projectById.get(id)
|
|
124
|
+
return {
|
|
125
|
+
id,
|
|
126
|
+
name: project?.name ?? null,
|
|
127
|
+
code: project?.code ?? null,
|
|
128
|
+
status: project && !project.deletedAt ? project.status : 'deleted',
|
|
129
|
+
usable: usable.has(id),
|
|
130
|
+
nobodyNotified: usable.has(id) && reachesById.get(id) === false,
|
|
131
|
+
unnotifiedTicketCount: unnotifiedByProject.get(id) ?? 0,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const ruleVersion = (rule: SupportRoutingRuleView): string | null =>
|
|
136
|
+
rule.updatedAt instanceof Date ? rule.updatedAt.toISOString() : null
|
|
137
|
+
|
|
138
|
+
const ruleViews = rules.map((rule) => {
|
|
139
|
+
let problem: RuleProblem = null
|
|
140
|
+
if (rule.categoryValue !== null) {
|
|
141
|
+
if (!categoryValues.has(rule.categoryValue)) problem = 'category_missing'
|
|
142
|
+
else if (!rule.isActive) problem = 'paused'
|
|
143
|
+
else if (!usable.has(rule.timeProjectId)) problem = 'project_unusable'
|
|
144
|
+
} else if (!usable.has(rule.timeProjectId)) {
|
|
145
|
+
problem = 'project_unusable'
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
id: rule.id,
|
|
149
|
+
categoryValue: rule.categoryValue,
|
|
150
|
+
isActive: rule.isActive,
|
|
151
|
+
isFallback: rule.categoryValue === null,
|
|
152
|
+
project: describeProject(rule.timeProjectId),
|
|
153
|
+
problem,
|
|
154
|
+
updatedAt: ruleVersion(rule),
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const categoryViews = categories.map((category) => {
|
|
159
|
+
const route = resolveSupportRouteFromRules(rules, category.value, usable)
|
|
160
|
+
return {
|
|
161
|
+
value: category.value,
|
|
162
|
+
label: category.label,
|
|
163
|
+
route: route ? { via: route.via, project: describeProject(route.timeProjectId) } : null,
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
return NextResponse.json({
|
|
168
|
+
fallback: fallbackRule
|
|
169
|
+
? { ruleId: fallbackRule.id, project: describeProject(fallbackRule.timeProjectId), updatedAt: ruleVersion(fallbackRule) }
|
|
170
|
+
: null,
|
|
171
|
+
rules: ruleViews,
|
|
172
|
+
categories: categoryViews,
|
|
173
|
+
unnotifiedTicketCount,
|
|
174
|
+
})
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })
|
|
177
|
+
const { translate } = await resolveTranslations()
|
|
178
|
+
logger.error('staff.support.routing preview failed', { err })
|
|
179
|
+
getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.support_routing_preview_failed' })
|
|
180
|
+
return NextResponse.json({ error: translate('staff.errors.internal', 'Internal server error') }, { status: 500 })
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const projectSchema = z.object({
|
|
185
|
+
id: z.string().uuid(),
|
|
186
|
+
name: z.string().nullable(),
|
|
187
|
+
code: z.string().nullable(),
|
|
188
|
+
status: z.string(),
|
|
189
|
+
usable: z.boolean(),
|
|
190
|
+
nobodyNotified: z
|
|
191
|
+
.boolean()
|
|
192
|
+
.optional()
|
|
193
|
+
.describe('True for a usable project with no active assigner, no usable default assignee and no owner: new-ticket notifications fall through to the organization project managers'),
|
|
194
|
+
unnotifiedTicketCount: z
|
|
195
|
+
.number()
|
|
196
|
+
.int()
|
|
197
|
+
.optional()
|
|
198
|
+
.describe('Live portal tickets in this project whose support notification reached nobody (`unnotified_at` set)'),
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
const ruleVersionSchema = z
|
|
202
|
+
.string()
|
|
203
|
+
.nullable()
|
|
204
|
+
.describe('The rule `updated_at` read in the same snapshot as its values; send it as the optimistic-lock expected version when editing or deleting the rule')
|
|
205
|
+
|
|
206
|
+
const previewSchema = z.object({
|
|
207
|
+
fallback: z.object({ ruleId: z.string().uuid(), project: projectSchema, updatedAt: ruleVersionSchema }).nullable(),
|
|
208
|
+
rules: z.array(
|
|
209
|
+
z.object({
|
|
210
|
+
id: z.string().uuid(),
|
|
211
|
+
categoryValue: z.string().nullable(),
|
|
212
|
+
isActive: z.boolean(),
|
|
213
|
+
isFallback: z.boolean(),
|
|
214
|
+
project: projectSchema,
|
|
215
|
+
problem: z.enum(['paused', 'project_unusable', 'category_missing']).nullable(),
|
|
216
|
+
updatedAt: ruleVersionSchema,
|
|
217
|
+
}),
|
|
218
|
+
),
|
|
219
|
+
categories: z.array(
|
|
220
|
+
z.object({
|
|
221
|
+
value: z.string(),
|
|
222
|
+
label: z.string(),
|
|
223
|
+
route: z.object({ via: z.enum(['category', 'fallback']), project: projectSchema }).nullable(),
|
|
224
|
+
}),
|
|
225
|
+
),
|
|
226
|
+
unnotifiedTicketCount: z
|
|
227
|
+
.number()
|
|
228
|
+
.int()
|
|
229
|
+
.optional()
|
|
230
|
+
.describe('Live portal tickets in the organization whose support notification reached nobody (`unnotified_at` set)'),
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
export const openApi: OpenApiRouteDoc = {
|
|
234
|
+
tag: 'Staff',
|
|
235
|
+
summary: 'Preview support ticket routing',
|
|
236
|
+
methods: {
|
|
237
|
+
GET: {
|
|
238
|
+
summary: 'Preview support ticket routing',
|
|
239
|
+
description:
|
|
240
|
+
'Returns the fallback rule, every routing rule with the problem that makes it fall back (paused, unusable project, deleted category), the project each support category routes to right now, which routing targets would notify nobody before the organization project managers (`project.nobodyNotified`), how many live tickets were not notified to anyone (`project.unnotifiedTicketCount`, `unnotifiedTicketCount`), and each rule `updatedAt` version token read together with its values.',
|
|
241
|
+
responses: [{ status: 200, description: 'Routing preview', schema: previewSchema }],
|
|
242
|
+
errors: [
|
|
243
|
+
{ status: 401, description: 'Unauthorized' },
|
|
244
|
+
{ status: 403, description: 'Missing staff.timesheets.settings.manage' },
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
}
|