@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
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET|POST|DELETE /api/staff/portal/support-tickets/{id}/attachments`
|
|
3
|
+
*
|
|
4
|
+
* Modeled on `warranty_claims/api/portal/attachments/route.ts`, with two
|
|
5
|
+
* differences: the attachments module is loaded with a dynamic import (staff keeps
|
|
6
|
+
* no static edge to it, and answers 503 when it is absent), and a customer may only
|
|
7
|
+
* delete files they uploaded themselves — every customer upload is tagged
|
|
8
|
+
* `customer-upload:<customerUserId>` because the attachment row has no uploader
|
|
9
|
+
* column. Only files tagged `customer-visible` are ever listed or streamed.
|
|
10
|
+
*/
|
|
11
|
+
import { NextResponse } from 'next/server'
|
|
12
|
+
import { z } from 'zod'
|
|
13
|
+
import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
|
|
14
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
15
|
+
import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
|
|
16
|
+
import { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'
|
|
17
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
18
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
19
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
20
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
21
|
+
import type { Attachment as AttachmentEntity } from '@open-mercato/core/modules/attachments/data/entities'
|
|
22
|
+
import type { ScopedAttachmentUploadService } from '@open-mercato/core/modules/attachments/lib/scoped-upload-service'
|
|
23
|
+
import {
|
|
24
|
+
PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE,
|
|
25
|
+
PORTAL_SUPPORT_TICKETS_VIEW_FEATURE,
|
|
26
|
+
loadPortalOwnedTicket,
|
|
27
|
+
resolveSupportPortalScope,
|
|
28
|
+
type SupportPortalScope,
|
|
29
|
+
} from '../../../../../lib/support/portalTickets'
|
|
30
|
+
import {
|
|
31
|
+
SUPPORT_TICKET_RESOURCE_KIND,
|
|
32
|
+
checkPortalRateLimit,
|
|
33
|
+
portalErrorResponse,
|
|
34
|
+
readPathId,
|
|
35
|
+
} from '../../../../../lib/support/portalRoute'
|
|
36
|
+
import {
|
|
37
|
+
CUSTOMER_VISIBLE_ATTACHMENT_TAG,
|
|
38
|
+
SUPPORT_TICKET_ATTACHMENT_ENTITY_ID,
|
|
39
|
+
customerUploadTag,
|
|
40
|
+
translateSupportUploadError,
|
|
41
|
+
} from '../../../../../lib/support/attachments'
|
|
42
|
+
import {
|
|
43
|
+
listCustomerVisibleAttachments,
|
|
44
|
+
loadPortalAttachmentsApi,
|
|
45
|
+
portalAttachmentAuth,
|
|
46
|
+
readAttachmentTags,
|
|
47
|
+
serializePortalAttachment,
|
|
48
|
+
portalSupportAttachmentItemSchema,
|
|
49
|
+
type PortalAttachmentsApi,
|
|
50
|
+
} from '../../../../../lib/support/portalAttachments'
|
|
51
|
+
import { emitSupportAttachmentActivity } from '../../../../../lib/support/supportActivity'
|
|
52
|
+
|
|
53
|
+
const logger = createLogger('staff').child({ component: 'api/portal/support-tickets/attachments' })
|
|
54
|
+
|
|
55
|
+
export const metadata = {
|
|
56
|
+
GET: { requireAuth: false },
|
|
57
|
+
POST: { requireAuth: false },
|
|
58
|
+
DELETE: { requireAuth: false },
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const uploadGuardPayloadSchema = z.object({
|
|
62
|
+
fileName: z.string().trim().min(1).max(255),
|
|
63
|
+
fileSize: z.number().int().nonnegative(),
|
|
64
|
+
mimeType: z.string().max(255),
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
function unavailable(): Response {
|
|
68
|
+
return NextResponse.json({ ok: false, error: 'staff.support.errors.attachmentsUnavailable' }, { status: 503 })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function notFound(): Response {
|
|
72
|
+
return NextResponse.json({ ok: false, error: 'staff.errors.notFound' }, { status: 404 })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function resolveDriverFactory(api: PortalAttachmentsApi, scope: SupportPortalScope) {
|
|
76
|
+
try {
|
|
77
|
+
const resolved = scope.container.resolve('storageDriverFactory') as InstanceType<PortalAttachmentsApi['StorageDriverFactory']> | null
|
|
78
|
+
return resolved ?? new api.StorageDriverFactory(scope.em)
|
|
79
|
+
} catch {
|
|
80
|
+
return new api.StorageDriverFactory(scope.em)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function loadVisibleAttachment(
|
|
85
|
+
api: PortalAttachmentsApi,
|
|
86
|
+
scope: SupportPortalScope,
|
|
87
|
+
taskId: string,
|
|
88
|
+
attachmentId: string,
|
|
89
|
+
): Promise<AttachmentEntity | null> {
|
|
90
|
+
const attachment = await findOneWithDecryption(
|
|
91
|
+
scope.em,
|
|
92
|
+
api.Attachment,
|
|
93
|
+
{
|
|
94
|
+
id: attachmentId,
|
|
95
|
+
entityId: SUPPORT_TICKET_ATTACHMENT_ENTITY_ID,
|
|
96
|
+
recordId: taskId,
|
|
97
|
+
tenantId: scope.tenantId,
|
|
98
|
+
organizationId: scope.organizationId,
|
|
99
|
+
},
|
|
100
|
+
{},
|
|
101
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
102
|
+
)
|
|
103
|
+
if (!attachment) return null
|
|
104
|
+
return readAttachmentTags(api, attachment).includes(CUSTOMER_VISIBLE_ATTACHMENT_TAG) ? attachment : null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function GET(req: Request) {
|
|
108
|
+
try {
|
|
109
|
+
const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_VIEW_FEATURE)
|
|
110
|
+
if (scope instanceof Response) return scope
|
|
111
|
+
const taskId = readPathId(req, /\/support-tickets\/([^/]+)\/attachments/)
|
|
112
|
+
const owned = taskId ? await loadPortalOwnedTicket(scope, taskId) : null
|
|
113
|
+
if (!owned) return notFound()
|
|
114
|
+
const api = await loadPortalAttachmentsApi()
|
|
115
|
+
if (!api) return unavailable()
|
|
116
|
+
const attachmentId = new URL(req.url).searchParams.get('attachmentId')
|
|
117
|
+
if (attachmentId) {
|
|
118
|
+
if (!z.string().uuid().safeParse(attachmentId).success) return notFound()
|
|
119
|
+
const attachment = await loadVisibleAttachment(api, scope, owned.task.id, attachmentId)
|
|
120
|
+
if (!attachment) return notFound()
|
|
121
|
+
const partition = await scope.em.findOne(api.AttachmentPartition, { code: attachment.partitionCode })
|
|
122
|
+
if (!partition) return notFound()
|
|
123
|
+
if (!api.checkAttachmentAccess(portalAttachmentAuth(scope), attachment, partition, { requireAuthForPublic: true }).ok) return notFound()
|
|
124
|
+
const driver = await (await resolveDriverFactory(api, scope)).resolveForPartition(attachment.partitionCode, {
|
|
125
|
+
tenantId: attachment.tenantId ?? scope.tenantId,
|
|
126
|
+
organizationId: attachment.organizationId ?? scope.organizationId,
|
|
127
|
+
})
|
|
128
|
+
let buffer: Buffer
|
|
129
|
+
try {
|
|
130
|
+
buffer = (await driver.read(attachment.partitionCode, attachment.storagePath)).buffer
|
|
131
|
+
} catch {
|
|
132
|
+
return notFound()
|
|
133
|
+
}
|
|
134
|
+
const headers: Record<string, string> = {
|
|
135
|
+
'Cache-Control': 'private, max-age=60',
|
|
136
|
+
'Content-Security-Policy': "default-src 'none'; sandbox",
|
|
137
|
+
'Content-Type': 'application/octet-stream',
|
|
138
|
+
'Content-Disposition': `attachment; filename="${api.slugifyAttachmentFileName(attachment.fileName)}"`,
|
|
139
|
+
'X-Content-Type-Options': 'nosniff',
|
|
140
|
+
}
|
|
141
|
+
if (attachment.fileSize > 0) headers['Content-Length'] = String(attachment.fileSize)
|
|
142
|
+
return new NextResponse(new Uint8Array(buffer), { status: 200, headers })
|
|
143
|
+
}
|
|
144
|
+
const canManage = await scope.hasFeature(PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE)
|
|
145
|
+
return NextResponse.json({ items: await listCustomerVisibleAttachments(api, scope, owned.task.id, canManage) })
|
|
146
|
+
} catch (err) {
|
|
147
|
+
return portalErrorResponse(err, 'staff portal support attachments GET failed')
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function POST(req: Request) {
|
|
152
|
+
try {
|
|
153
|
+
const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE)
|
|
154
|
+
if (scope instanceof Response) return scope
|
|
155
|
+
const taskId = readPathId(req, /\/support-tickets\/([^/]+)\/attachments/)
|
|
156
|
+
const owned = taskId ? await loadPortalOwnedTicket(scope, taskId) : null
|
|
157
|
+
if (!owned) return notFound()
|
|
158
|
+
const limited = await checkPortalRateLimit(scope, 'upload')
|
|
159
|
+
if (limited) return limited
|
|
160
|
+
const api = await loadPortalAttachmentsApi()
|
|
161
|
+
if (!api) return unavailable()
|
|
162
|
+
const { t } = await resolveTranslations()
|
|
163
|
+
if (!(req.headers.get('content-type') || '').toLowerCase().includes('multipart/form-data')) {
|
|
164
|
+
return NextResponse.json({ ok: false, error: t('staff.errors.invalid_request', 'Invalid request') }, { status: 400 })
|
|
165
|
+
}
|
|
166
|
+
if (!api.isMultipartRequestWithinUploadLimit(req.headers.get('content-length'))) {
|
|
167
|
+
return NextResponse.json({ ok: false, error: t('attachments.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.') }, { status: 413 })
|
|
168
|
+
}
|
|
169
|
+
const form = await req.formData()
|
|
170
|
+
const file = form.get('file')
|
|
171
|
+
if (!(file instanceof File)) {
|
|
172
|
+
return NextResponse.json({ ok: false, error: t('staff.support.errors.fileRequired', 'Choose a file before uploading.') }, { status: 400 })
|
|
173
|
+
}
|
|
174
|
+
const uploadPayload = { fileName: file.name, fileSize: file.size, mimeType: file.type }
|
|
175
|
+
const guarded = await runRouteMutationGuards({
|
|
176
|
+
container: scope.container,
|
|
177
|
+
req,
|
|
178
|
+
auth: { userId: scope.auth.sub, tenantId: scope.tenantId, organizationId: scope.organizationId, userFeatures: [] },
|
|
179
|
+
input: {
|
|
180
|
+
resourceKind: SUPPORT_TICKET_RESOURCE_KIND,
|
|
181
|
+
resourceId: owned.task.id,
|
|
182
|
+
operation: 'update',
|
|
183
|
+
mutationPayload: uploadPayload,
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
if (!guarded.ok) return guarded.response
|
|
187
|
+
const effectiveUpload = guarded.modifiedPayload
|
|
188
|
+
? uploadGuardPayloadSchema.parse({ ...uploadPayload, ...guarded.modifiedPayload, fileSize: file.size })
|
|
189
|
+
: uploadPayload
|
|
190
|
+
let uploadService: ScopedAttachmentUploadService | null = null
|
|
191
|
+
try {
|
|
192
|
+
uploadService = scope.container.resolve('attachmentScopedUploadService') as ScopedAttachmentUploadService
|
|
193
|
+
} catch {
|
|
194
|
+
uploadService = null
|
|
195
|
+
}
|
|
196
|
+
if (!uploadService) return unavailable()
|
|
197
|
+
let attachment: AttachmentEntity
|
|
198
|
+
try {
|
|
199
|
+
attachment = await uploadService.upload({
|
|
200
|
+
tenantId: scope.tenantId,
|
|
201
|
+
organizationId: scope.organizationId,
|
|
202
|
+
entityId: SUPPORT_TICKET_ATTACHMENT_ENTITY_ID,
|
|
203
|
+
recordId: owned.task.id,
|
|
204
|
+
fileName: effectiveUpload.fileName,
|
|
205
|
+
buffer: Buffer.from(await file.arrayBuffer()),
|
|
206
|
+
declaredMimeType: effectiveUpload.mimeType,
|
|
207
|
+
tags: [CUSTOMER_VISIBLE_ATTACHMENT_TAG, customerUploadTag(scope.auth.sub)],
|
|
208
|
+
})
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (api.isScopedAttachmentUploadError(error)) {
|
|
211
|
+
return NextResponse.json(
|
|
212
|
+
{ ok: false, error: translateSupportUploadError(error.code, t), code: error.code },
|
|
213
|
+
{ status: error.status },
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
throw error
|
|
217
|
+
}
|
|
218
|
+
await guarded.runAfterSuccess()
|
|
219
|
+
emitSupportAttachmentActivity(owned.task, owned.ticket, 'attachment_added', attachment.id)
|
|
220
|
+
return NextResponse.json({ ok: true, item: serializePortalAttachment(api, attachment, owned.task.id, scope.auth.sub, true) }, { status: 201 })
|
|
221
|
+
} catch (err) {
|
|
222
|
+
return portalErrorResponse(err, 'staff portal support attachments POST failed')
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function DELETE(req: Request) {
|
|
227
|
+
try {
|
|
228
|
+
const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE)
|
|
229
|
+
if (scope instanceof Response) return scope
|
|
230
|
+
const taskId = readPathId(req, /\/support-tickets\/([^/]+)\/attachments/)
|
|
231
|
+
const owned = taskId ? await loadPortalOwnedTicket(scope, taskId) : null
|
|
232
|
+
if (!owned) return notFound()
|
|
233
|
+
const api = await loadPortalAttachmentsApi()
|
|
234
|
+
if (!api) return unavailable()
|
|
235
|
+
const attachmentId = new URL(req.url).searchParams.get('attachmentId')
|
|
236
|
+
if (!attachmentId || !z.string().uuid().safeParse(attachmentId).success) return notFound()
|
|
237
|
+
const attachment = await loadVisibleAttachment(api, scope, owned.task.id, attachmentId)
|
|
238
|
+
if (!attachment) return notFound()
|
|
239
|
+
if (!readAttachmentTags(api, attachment).includes(customerUploadTag(scope.auth.sub))) {
|
|
240
|
+
return NextResponse.json({ ok: false, error: 'staff.support.errors.attachmentNotYours' }, { status: 403 })
|
|
241
|
+
}
|
|
242
|
+
const guarded = await runRouteMutationGuards({
|
|
243
|
+
container: scope.container,
|
|
244
|
+
req,
|
|
245
|
+
auth: { userId: scope.auth.sub, tenantId: scope.tenantId, organizationId: scope.organizationId, userFeatures: [] },
|
|
246
|
+
input: { resourceKind: SUPPORT_TICKET_RESOURCE_KIND, resourceId: owned.task.id, operation: 'delete', mutationPayload: { attachmentId } },
|
|
247
|
+
})
|
|
248
|
+
if (!guarded.ok) return guarded.response
|
|
249
|
+
await scope.em.remove(attachment).flush()
|
|
250
|
+
await api.clearAttachmentThumbnailCache(attachment.partitionCode, attachment.id).catch((err) => {
|
|
251
|
+
logger.error('staff portal support attachment thumbnail cleanup failed', { err, attachmentId })
|
|
252
|
+
getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.portal_attachment_thumbnail_cleanup_failed' })
|
|
253
|
+
})
|
|
254
|
+
if (attachment.storagePath) {
|
|
255
|
+
try {
|
|
256
|
+
const driver = await (await resolveDriverFactory(api, scope)).resolveForPartition(attachment.partitionCode, {
|
|
257
|
+
tenantId: attachment.tenantId ?? scope.tenantId,
|
|
258
|
+
organizationId: attachment.organizationId ?? scope.organizationId,
|
|
259
|
+
})
|
|
260
|
+
await driver.delete(attachment.partitionCode, attachment.storagePath)
|
|
261
|
+
} catch (err) {
|
|
262
|
+
logger.error('staff portal support attachment storage cleanup failed', { err, attachmentId })
|
|
263
|
+
getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.portal_attachment_storage_cleanup_failed' })
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const dataEngine = scope.container.resolve('dataEngine') as DataEngine
|
|
268
|
+
await emitCrudSideEffects({
|
|
269
|
+
dataEngine,
|
|
270
|
+
action: 'deleted',
|
|
271
|
+
entity: attachment,
|
|
272
|
+
identifiers: { id: attachment.id, organizationId: attachment.organizationId ?? null, tenantId: attachment.tenantId ?? null },
|
|
273
|
+
events: api.attachmentCrudEvents,
|
|
274
|
+
indexer: api.attachmentCrudIndexer,
|
|
275
|
+
})
|
|
276
|
+
await dataEngine.flushOrmEntityChanges()
|
|
277
|
+
} catch (err) {
|
|
278
|
+
logger.error('staff portal support attachment side effects failed', { err, attachmentId })
|
|
279
|
+
getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.portal_attachment_side_effects_failed' })
|
|
280
|
+
}
|
|
281
|
+
await guarded.runAfterSuccess()
|
|
282
|
+
emitSupportAttachmentActivity(owned.task, owned.ticket, 'attachment_removed', attachmentId)
|
|
283
|
+
return NextResponse.json({ ok: true })
|
|
284
|
+
} catch (err) {
|
|
285
|
+
return portalErrorResponse(err, 'staff portal support attachments DELETE failed')
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const attachmentItemSchema = portalSupportAttachmentItemSchema
|
|
290
|
+
|
|
291
|
+
export const openApi: OpenApiRouteDoc = {
|
|
292
|
+
tag: 'Staff Portal',
|
|
293
|
+
summary: 'Support ticket attachments',
|
|
294
|
+
methods: {
|
|
295
|
+
GET: {
|
|
296
|
+
summary: 'List or download support ticket files',
|
|
297
|
+
description:
|
|
298
|
+
'Lists customer-visible files of a ticket the caller owns, or streams one when `attachmentId` is given. Staff files that were not shared with the customer are never listed.',
|
|
299
|
+
responses: [{ status: 200, description: 'Files', schema: z.object({ items: z.array(attachmentItemSchema) }) }],
|
|
300
|
+
errors: [
|
|
301
|
+
{ status: 401, description: 'No portal session' },
|
|
302
|
+
{ status: 404, description: 'Ticket or file not found' },
|
|
303
|
+
{ status: 503, description: 'Attachments module unavailable' },
|
|
304
|
+
],
|
|
305
|
+
},
|
|
306
|
+
POST: {
|
|
307
|
+
summary: 'Upload a file to a support ticket',
|
|
308
|
+
description: 'Multipart upload (`file`). The file is tagged customer-visible and as uploaded by the caller. Rate limited per customer user; rejected files answer with a translated `error` and the machine `code`.',
|
|
309
|
+
responses: [{ status: 201, description: 'Uploaded', schema: z.object({ ok: z.literal(true), item: attachmentItemSchema }) }],
|
|
310
|
+
errors: [
|
|
311
|
+
{ status: 403, description: 'Missing portal.support_tickets.manage' },
|
|
312
|
+
{ status: 404, description: 'Ticket not found' },
|
|
313
|
+
{ status: 413, description: 'File too large' },
|
|
314
|
+
{ status: 429, description: 'Upload rate limit exceeded (per customer user)' },
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
DELETE: {
|
|
318
|
+
summary: 'Delete a file you uploaded',
|
|
319
|
+
description: 'Deletes a file the caller uploaded (`attachmentId` query). Files uploaded by staff or by other customer users answer 403.',
|
|
320
|
+
responses: [{ status: 200, description: 'Deleted', schema: z.object({ ok: z.literal(true) }) }],
|
|
321
|
+
errors: [
|
|
322
|
+
{ status: 403, description: 'The file was not uploaded by the caller' },
|
|
323
|
+
{ status: 404, description: 'Ticket or file not found' },
|
|
324
|
+
],
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
|
|
4
|
+
import type { CommandBus } from '@open-mercato/shared/lib/commands'
|
|
5
|
+
import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
|
|
6
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
7
|
+
import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
|
|
8
|
+
import {
|
|
9
|
+
PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE,
|
|
10
|
+
loadPortalOwnedTicket,
|
|
11
|
+
resolveCustomerDisplay,
|
|
12
|
+
resolveSupportPortalScope,
|
|
13
|
+
} from '../../../../../lib/support/portalTickets'
|
|
14
|
+
import {
|
|
15
|
+
SUPPORT_TICKET_RESOURCE_KIND,
|
|
16
|
+
buildPortalCommandContext,
|
|
17
|
+
checkPortalRateLimit,
|
|
18
|
+
portalErrorResponse,
|
|
19
|
+
readPathId,
|
|
20
|
+
} from '../../../../../lib/support/portalRoute'
|
|
21
|
+
import {
|
|
22
|
+
staffSupportTicketCommandIds,
|
|
23
|
+
type SupportTicketMessageResult,
|
|
24
|
+
type SupportTicketPortalMessageInput,
|
|
25
|
+
} from '../../../../../commands/support-tickets'
|
|
26
|
+
|
|
27
|
+
export const metadata: { requireAuth?: boolean } = { requireAuth: false }
|
|
28
|
+
|
|
29
|
+
const bodySchema = z.object({ body: z.string().trim().min(1).max(10000) })
|
|
30
|
+
const responseSchema = z.object({ ok: z.literal(true), id: z.string().uuid(), reopened: z.boolean() })
|
|
31
|
+
|
|
32
|
+
export async function POST(req: Request) {
|
|
33
|
+
try {
|
|
34
|
+
const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE)
|
|
35
|
+
if (scope instanceof Response) return scope
|
|
36
|
+
const taskId = readPathId(req, /\/support-tickets\/([^/]+)\/messages/)
|
|
37
|
+
const owned = taskId ? await loadPortalOwnedTicket(scope, taskId) : null
|
|
38
|
+
if (!owned) return NextResponse.json({ ok: false, error: 'staff.errors.notFound' }, { status: 404 })
|
|
39
|
+
const limited = await checkPortalRateLimit(scope, 'message')
|
|
40
|
+
if (limited) return limited
|
|
41
|
+
const body = bodySchema.parse(await readJsonSafe<Record<string, unknown>>(req, {}))
|
|
42
|
+
const guarded = await runRouteMutationGuards({
|
|
43
|
+
container: scope.container,
|
|
44
|
+
req,
|
|
45
|
+
auth: { userId: scope.auth.sub, tenantId: scope.tenantId, organizationId: scope.organizationId, userFeatures: [] },
|
|
46
|
+
input: { resourceKind: SUPPORT_TICKET_RESOURCE_KIND, resourceId: owned.task.id, operation: 'update', mutationPayload: body },
|
|
47
|
+
})
|
|
48
|
+
if (!guarded.ok) return guarded.response
|
|
49
|
+
const effectiveBody = guarded.modifiedPayload ? bodySchema.parse({ ...body, ...guarded.modifiedPayload }) : body
|
|
50
|
+
const customer = await resolveCustomerDisplay(scope)
|
|
51
|
+
const input: SupportTicketPortalMessageInput = {
|
|
52
|
+
tenantId: scope.tenantId,
|
|
53
|
+
organizationId: scope.organizationId,
|
|
54
|
+
customerId: scope.customerId,
|
|
55
|
+
customerUserId: scope.auth.sub,
|
|
56
|
+
customerDisplayName: customer.displayName,
|
|
57
|
+
customerKind: customer.kind,
|
|
58
|
+
reporterDisplayName: scope.auth.displayName ?? null,
|
|
59
|
+
taskId: owned.task.id,
|
|
60
|
+
body: effectiveBody.body,
|
|
61
|
+
}
|
|
62
|
+
const commandBus = scope.container.resolve('commandBus') as CommandBus
|
|
63
|
+
const { result } = await commandBus.execute<SupportTicketPortalMessageInput, SupportTicketMessageResult>(
|
|
64
|
+
staffSupportTicketCommandIds.portalMessage,
|
|
65
|
+
{ input, ctx: buildPortalCommandContext(scope, req) },
|
|
66
|
+
)
|
|
67
|
+
await guarded.runAfterSuccess()
|
|
68
|
+
return NextResponse.json(responseSchema.parse({ ok: true, id: result.commentId, reopened: result.reopened }), { status: 201 })
|
|
69
|
+
} catch (err) {
|
|
70
|
+
const rejection = getCommandInterceptorHttpRejection(err)
|
|
71
|
+
if (rejection) return NextResponse.json(rejection.body, { status: rejection.status })
|
|
72
|
+
return portalErrorResponse(err, 'staff portal support message POST failed')
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const openApi: OpenApiRouteDoc = {
|
|
77
|
+
tag: 'Staff Portal',
|
|
78
|
+
summary: 'Reply on a support ticket',
|
|
79
|
+
methods: {
|
|
80
|
+
POST: {
|
|
81
|
+
summary: 'Reply on a support ticket',
|
|
82
|
+
description:
|
|
83
|
+
'Adds a customer message to a ticket the signed-in customer owns. Replying on a resolved ticket reopens it (it moves back to the project default column). Rate limited per customer user.',
|
|
84
|
+
requestBody: { contentType: 'application/json', schema: bodySchema },
|
|
85
|
+
responses: [{ status: 201, description: 'Message added', schema: responseSchema }],
|
|
86
|
+
errors: [
|
|
87
|
+
{ status: 401, description: 'No portal session' },
|
|
88
|
+
{ status: 403, description: 'Missing portal.support_tickets.manage' },
|
|
89
|
+
{ status: 404, description: 'Not found or not owned by this customer' },
|
|
90
|
+
{ status: 429, description: 'Rate limit exceeded' },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
3
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
6
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
7
|
+
import { User } from '@open-mercato/core/modules/auth/data/entities'
|
|
8
|
+
import { StaffTimeTaskComment, StaffTimeTaskStatus } from '../../../../data/entities'
|
|
9
|
+
import {
|
|
10
|
+
PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE,
|
|
11
|
+
PORTAL_SUPPORT_TICKETS_VIEW_FEATURE,
|
|
12
|
+
loadPortalOwnedTicket,
|
|
13
|
+
resolveSupportPortalScope,
|
|
14
|
+
} from '../../../../lib/support/portalTickets'
|
|
15
|
+
import { portalErrorResponse, readPathId } from '../../../../lib/support/portalRoute'
|
|
16
|
+
import {
|
|
17
|
+
listCustomerVisibleAttachments,
|
|
18
|
+
loadPortalAttachmentsApi,
|
|
19
|
+
portalSupportAttachmentItemSchema,
|
|
20
|
+
type PortalSupportAttachmentItem,
|
|
21
|
+
} from '../../../../lib/support/portalAttachments'
|
|
22
|
+
import { derivePortalStatus } from '../../../../lib/support/portalStatus'
|
|
23
|
+
|
|
24
|
+
const logger = createLogger('staff').child({ component: 'api/portal/support-tickets/detail' })
|
|
25
|
+
|
|
26
|
+
export const metadata: { requireAuth?: boolean } = { requireAuth: false }
|
|
27
|
+
|
|
28
|
+
const messageSchema = z.object({
|
|
29
|
+
id: z.string().uuid(),
|
|
30
|
+
authorKind: z.enum(['staff', 'customer']),
|
|
31
|
+
authorName: z.string().nullable(),
|
|
32
|
+
isSelf: z.boolean(),
|
|
33
|
+
body: z.string(),
|
|
34
|
+
createdAt: z.string().nullable(),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const detailSchema = z.object({
|
|
38
|
+
id: z.string().uuid(),
|
|
39
|
+
supportReference: z.string(),
|
|
40
|
+
subject: z.string(),
|
|
41
|
+
description: z.string(),
|
|
42
|
+
category: z.object({ value: z.string(), label: z.string() }),
|
|
43
|
+
portalStatus: z.enum(['received', 'in_progress', 'resolved']),
|
|
44
|
+
createdAt: z.string().nullable(),
|
|
45
|
+
updatedAt: z.string().nullable(),
|
|
46
|
+
canReply: z.boolean(),
|
|
47
|
+
requester: z.object({ name: z.string().nullable(), isSelf: z.boolean() }),
|
|
48
|
+
messages: z.array(messageSchema),
|
|
49
|
+
attachments: z.array(portalSupportAttachmentItemSchema),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
function nonEmptyName(value: unknown): string | null {
|
|
53
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function loadVisibleAttachments(
|
|
57
|
+
scope: Parameters<typeof listCustomerVisibleAttachments>[1],
|
|
58
|
+
taskId: string,
|
|
59
|
+
canManage: boolean,
|
|
60
|
+
): Promise<PortalSupportAttachmentItem[]> {
|
|
61
|
+
const api = await loadPortalAttachmentsApi()
|
|
62
|
+
if (!api) return []
|
|
63
|
+
try {
|
|
64
|
+
return await listCustomerVisibleAttachments(api, scope, taskId, canManage)
|
|
65
|
+
} catch (err) {
|
|
66
|
+
logger.warn('staff portal support ticket attachments could not be listed', { err, taskId })
|
|
67
|
+
getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.portal_support_attachments_failed' })
|
|
68
|
+
return []
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function GET(req: Request) {
|
|
73
|
+
try {
|
|
74
|
+
const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_VIEW_FEATURE)
|
|
75
|
+
if (scope instanceof Response) return scope
|
|
76
|
+
const taskId = readPathId(req, /\/support-tickets\/([^/]+)/)
|
|
77
|
+
const owned = taskId ? await loadPortalOwnedTicket(scope, taskId) : null
|
|
78
|
+
if (!owned) return NextResponse.json({ ok: false, error: 'staff.errors.notFound' }, { status: 404 })
|
|
79
|
+
const { task, ticket } = owned
|
|
80
|
+
const decryptionScope = { tenantId: scope.tenantId, organizationId: scope.organizationId }
|
|
81
|
+
const status = await scope.em.findOne(StaffTimeTaskStatus, { id: task.taskStatusId, ...decryptionScope }, { fields: ['id', 'isDefault', 'isDone'] })
|
|
82
|
+
const comments = await scope.em.find(
|
|
83
|
+
StaffTimeTaskComment,
|
|
84
|
+
{ taskId: task.id, ...decryptionScope, visibility: 'customer', deletedAt: null },
|
|
85
|
+
{ orderBy: { createdAt: 'asc', id: 'asc' } },
|
|
86
|
+
)
|
|
87
|
+
const staffUserIds = [...new Set(comments.map((comment) => comment.authorUserId).filter((id): id is string => typeof id === 'string'))]
|
|
88
|
+
const staffNames = new Map<string, string | null>()
|
|
89
|
+
if (staffUserIds.length) {
|
|
90
|
+
try {
|
|
91
|
+
const users = await findWithDecryption(
|
|
92
|
+
scope.em,
|
|
93
|
+
User,
|
|
94
|
+
{ id: { $in: staffUserIds }, tenantId: scope.tenantId },
|
|
95
|
+
undefined,
|
|
96
|
+
decryptionScope,
|
|
97
|
+
)
|
|
98
|
+
for (const user of users) staffNames.set(user.id, nonEmptyName(user.name))
|
|
99
|
+
} catch (err) {
|
|
100
|
+
logger.warn('staff portal support ticket could not resolve staff names', { err, taskId: task.id })
|
|
101
|
+
getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.portal_support_staff_names_failed' })
|
|
102
|
+
staffNames.clear()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const request = ticket.customerRequestSnapshot
|
|
106
|
+
const canReply = await scope.hasFeature(PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE)
|
|
107
|
+
const attachments = await loadVisibleAttachments(scope, task.id, canReply)
|
|
108
|
+
return NextResponse.json(
|
|
109
|
+
detailSchema.parse({
|
|
110
|
+
id: task.id,
|
|
111
|
+
supportReference: ticket.supportReference,
|
|
112
|
+
subject: request && typeof request.subject === 'string' ? request.subject : '',
|
|
113
|
+
description: request && typeof request.body === 'string' ? request.body : '',
|
|
114
|
+
category: ticket.categorySnapshot,
|
|
115
|
+
portalStatus: derivePortalStatus(status),
|
|
116
|
+
createdAt: task.createdAt ? task.createdAt.toISOString() : null,
|
|
117
|
+
updatedAt: task.updatedAt ? task.updatedAt.toISOString() : null,
|
|
118
|
+
canReply,
|
|
119
|
+
requester: {
|
|
120
|
+
name: nonEmptyName(ticket.customerSnapshot?.reporterDisplayName),
|
|
121
|
+
isSelf: ticket.reportedByCustomerUserId === scope.auth.sub,
|
|
122
|
+
},
|
|
123
|
+
messages: comments.map((comment) => ({
|
|
124
|
+
id: comment.id,
|
|
125
|
+
authorKind: comment.authorKind === 'customer' ? 'customer' : 'staff',
|
|
126
|
+
authorName:
|
|
127
|
+
comment.authorKind === 'customer'
|
|
128
|
+
? nonEmptyName(comment.authorDisplaySnapshot?.displayName)
|
|
129
|
+
: comment.authorUserId
|
|
130
|
+
? staffNames.get(comment.authorUserId) ?? null
|
|
131
|
+
: null,
|
|
132
|
+
isSelf: comment.authorKind === 'customer' && comment.authorCustomerUserId === scope.auth.sub,
|
|
133
|
+
body: comment.body,
|
|
134
|
+
createdAt: comment.createdAt ? comment.createdAt.toISOString() : null,
|
|
135
|
+
})),
|
|
136
|
+
attachments,
|
|
137
|
+
}),
|
|
138
|
+
)
|
|
139
|
+
} catch (err) {
|
|
140
|
+
return portalErrorResponse(err, 'staff portal support ticket GET failed')
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const openApi: OpenApiRouteDoc = {
|
|
145
|
+
tag: 'Staff Portal',
|
|
146
|
+
summary: 'Support ticket detail',
|
|
147
|
+
methods: {
|
|
148
|
+
GET: {
|
|
149
|
+
summary: 'Support ticket detail',
|
|
150
|
+
description:
|
|
151
|
+
'One ticket owned by the signed-in customer: the original request, the customer-facing status, and customer-visible messages only, and the customer-visible files (`attachments`, same items as the attachments list; an empty list when the attachments module is unavailable). Internal notes and internal files are never returned. A ticket of another customer answers 404.',
|
|
152
|
+
responses: [{ status: 200, description: 'Ticket', schema: detailSchema }],
|
|
153
|
+
errors: [
|
|
154
|
+
{ status: 401, description: 'No portal session' },
|
|
155
|
+
{ status: 403, description: 'Missing portal.support_tickets.view' },
|
|
156
|
+
{ status: 404, description: 'Not found or not owned by this customer' },
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
4
|
+
import { loadSupportTicketCategories } from '../../../../lib/support/dictionaries'
|
|
5
|
+
import { resolveRoutableCategoryValues } from '../../../../lib/support/routing'
|
|
6
|
+
import {
|
|
7
|
+
PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE,
|
|
8
|
+
PORTAL_SUPPORT_TICKETS_VIEW_FEATURE,
|
|
9
|
+
resolveSupportPortalScope,
|
|
10
|
+
} from '../../../../lib/support/portalTickets'
|
|
11
|
+
import { portalErrorResponse } from '../../../../lib/support/portalRoute'
|
|
12
|
+
import { loadPortalAttachmentsApi } from '../../../../lib/support/portalAttachments'
|
|
13
|
+
|
|
14
|
+
export const metadata: { requireAuth?: boolean } = { requireAuth: false }
|
|
15
|
+
|
|
16
|
+
const optionsResponseSchema = z.object({
|
|
17
|
+
categories: z.array(z.object({ value: z.string(), label: z.string() })),
|
|
18
|
+
canCreate: z.boolean(),
|
|
19
|
+
maxUploadBytes: z.number().int().positive().nullable(),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export async function GET(req: Request) {
|
|
23
|
+
try {
|
|
24
|
+
const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_VIEW_FEATURE)
|
|
25
|
+
if (scope instanceof Response) return scope
|
|
26
|
+
const supportScope = { tenantId: scope.tenantId, organizationId: scope.organizationId }
|
|
27
|
+
const categories = await loadSupportTicketCategories(scope.em, supportScope)
|
|
28
|
+
const routes = await resolveRoutableCategoryValues(scope.em, supportScope, categories.map((category) => category.value))
|
|
29
|
+
const routable = categories.filter((category) => routes.has(category.value))
|
|
30
|
+
const canManage = await scope.hasFeature(PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE)
|
|
31
|
+
const attachmentsApi = await loadPortalAttachmentsApi()
|
|
32
|
+
const maxUploadBytes = attachmentsApi ? attachmentsApi.resolveAttachmentMaxBytes() : null
|
|
33
|
+
return NextResponse.json(
|
|
34
|
+
optionsResponseSchema.parse({
|
|
35
|
+
categories: routable,
|
|
36
|
+
canCreate: canManage && routable.length > 0,
|
|
37
|
+
maxUploadBytes: maxUploadBytes && maxUploadBytes > 0 ? maxUploadBytes : null,
|
|
38
|
+
}),
|
|
39
|
+
)
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return portalErrorResponse(err, 'staff portal support options GET failed')
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const openApi: OpenApiRouteDoc = {
|
|
46
|
+
tag: 'Staff Portal',
|
|
47
|
+
summary: 'Support ticket form options',
|
|
48
|
+
methods: {
|
|
49
|
+
GET: {
|
|
50
|
+
summary: 'Support ticket form options',
|
|
51
|
+
description:
|
|
52
|
+
'Categories the signed-in customer can file a ticket in — only those that currently route to a usable project — whether the caller may create tickets (`portal.support_tickets.manage`), and the server upload size limit in bytes (`maxUploadBytes`, `null` when the attachments module is unavailable).',
|
|
53
|
+
responses: [{ status: 200, description: 'Options', schema: optionsResponseSchema }],
|
|
54
|
+
errors: [
|
|
55
|
+
{ status: 401, description: 'No portal session' },
|
|
56
|
+
{ status: 403, description: 'Portal account not linked to a customer, or missing portal.support_tickets.view' },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
}
|