@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,138 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
3
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
4
|
+
import { notificationTypes } from '../../notifications'
|
|
5
|
+
import { StaffSupportTicket, StaffTeamMember, StaffTimeProject, StaffTimeTask } from '../../data/entities'
|
|
6
|
+
import { resolveAssignmentGraceDaysFromContainer } from '../time-tracking/projectAssignment'
|
|
7
|
+
import {
|
|
8
|
+
loadActiveAssignerUserIds,
|
|
9
|
+
notifyProjectWork,
|
|
10
|
+
resolveProjectWorkRecipients,
|
|
11
|
+
} from '../time-tracking/workRecipients'
|
|
12
|
+
|
|
13
|
+
const logger = createLogger('staff').child({ component: 'lib/support/notifications' })
|
|
14
|
+
|
|
15
|
+
type ContainerLike = { resolve: <T = unknown>(name: string) => T }
|
|
16
|
+
|
|
17
|
+
export type SupportNotificationKind = 'created' | 'customer_replied'
|
|
18
|
+
|
|
19
|
+
const TYPE_BY_KIND: Record<SupportNotificationKind, string> = {
|
|
20
|
+
created: 'staff.support_ticket.created',
|
|
21
|
+
customer_replied: 'staff.support_ticket.customer_replied',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function resolveAssigneeUserId(em: EntityManager, task: StaffTimeTask): Promise<string | null> {
|
|
25
|
+
if (!task.assigneeStaffMemberId) return null
|
|
26
|
+
const member = await findOneWithDecryption(
|
|
27
|
+
em,
|
|
28
|
+
StaffTeamMember,
|
|
29
|
+
{ id: task.assigneeStaffMemberId, tenantId: task.tenantId, organizationId: task.organizationId, deletedAt: null },
|
|
30
|
+
undefined,
|
|
31
|
+
{ tenantId: task.tenantId, organizationId: task.organizationId },
|
|
32
|
+
)
|
|
33
|
+
return member?.userId ?? null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Notifies staff about a support ticket through the project escalation chain and
|
|
38
|
+
* records the outcome on the ticket: `unnotified_at` is set (earliest value kept)
|
|
39
|
+
* when the chain reached nobody, and cleared when a notification reaches someone.
|
|
40
|
+
* Notifying is best-effort — the ticket or reply that triggered it is never undone.
|
|
41
|
+
*/
|
|
42
|
+
export async function notifySupportTicket(
|
|
43
|
+
container: ContainerLike,
|
|
44
|
+
input: { taskId: string; tenantId: string; organizationId: string; kind: SupportNotificationKind },
|
|
45
|
+
): Promise<{ notifiedCount: number; step: number } | null> {
|
|
46
|
+
const typeDef = notificationTypes.find((type) => type.type === TYPE_BY_KIND[input.kind])
|
|
47
|
+
if (!typeDef) return null
|
|
48
|
+
const scope = { tenantId: input.tenantId, organizationId: input.organizationId }
|
|
49
|
+
const em = (container.resolve('em') as EntityManager).fork()
|
|
50
|
+
const task = await em.findOne(StaffTimeTask, { id: input.taskId, ...scope, source: 'portal', deletedAt: null })
|
|
51
|
+
if (!task) return null
|
|
52
|
+
const ticket = await findOneWithDecryption(em, StaffSupportTicket, { taskId: task.id, ...scope, deletedAt: null }, undefined, scope)
|
|
53
|
+
if (!ticket) return null
|
|
54
|
+
const project = await em.findOne(StaffTimeProject, { id: task.timeProjectId, ...scope }, { fields: ['id', 'name'] })
|
|
55
|
+
const graceDays = await resolveAssignmentGraceDaysFromContainer(container, input.tenantId)
|
|
56
|
+
|
|
57
|
+
const assigneeUserId = await resolveAssigneeUserId(em, task)
|
|
58
|
+
const audience =
|
|
59
|
+
input.kind === 'created'
|
|
60
|
+
? [assigneeUserId, ...(await loadActiveAssignerUserIds(em, scope, { timeProjectId: task.timeProjectId, graceDays }))]
|
|
61
|
+
: [assigneeUserId]
|
|
62
|
+
const recipients = await resolveProjectWorkRecipients(em, scope, {
|
|
63
|
+
timeProjectId: task.timeProjectId,
|
|
64
|
+
audienceUserIds: audience,
|
|
65
|
+
graceDays,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const snapshot = ticket.customerSnapshot ?? {}
|
|
69
|
+
const customerName =
|
|
70
|
+
(typeof snapshot.reporterDisplayName === 'string' && snapshot.reporterDisplayName) ||
|
|
71
|
+
(typeof snapshot.displayName === 'string' && snapshot.displayName) ||
|
|
72
|
+
''
|
|
73
|
+
const request = ticket.customerRequestSnapshot
|
|
74
|
+
const subject = request && typeof request.subject === 'string' ? request.subject : task.title
|
|
75
|
+
const result = await notifyProjectWork(container, scope, {
|
|
76
|
+
typeDef,
|
|
77
|
+
recipients,
|
|
78
|
+
build: {
|
|
79
|
+
titleVariables: { reference: ticket.supportReference },
|
|
80
|
+
bodyVariables: {
|
|
81
|
+
reference: ticket.supportReference,
|
|
82
|
+
customer: customerName,
|
|
83
|
+
subject,
|
|
84
|
+
category: ticket.categorySnapshot?.label ?? ticket.categoryValue,
|
|
85
|
+
project: project?.name ?? '',
|
|
86
|
+
},
|
|
87
|
+
sourceEntityType: 'staff:staff_time_task',
|
|
88
|
+
sourceEntityId: task.id,
|
|
89
|
+
linkHref: `/backend/staff/time-tracking/support?task=${encodeURIComponent(task.id)}`,
|
|
90
|
+
groupKey: `staff:support-ticket-${input.kind}:${task.id}`,
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const fresh = await findOneWithDecryption(em, StaffSupportTicket, { id: ticket.id, ...scope }, undefined, scope)
|
|
95
|
+
if (fresh) {
|
|
96
|
+
if (result.notifiedCount === 0) {
|
|
97
|
+
if (!fresh.unnotifiedAt) {
|
|
98
|
+
fresh.unnotifiedAt = new Date()
|
|
99
|
+
fresh.updatedAt = new Date()
|
|
100
|
+
await em.flush()
|
|
101
|
+
}
|
|
102
|
+
logger.warn('staff support ticket notification reached nobody', {
|
|
103
|
+
tenantId: input.tenantId,
|
|
104
|
+
organizationId: input.organizationId,
|
|
105
|
+
timeProjectId: task.timeProjectId,
|
|
106
|
+
ticketId: ticket.id,
|
|
107
|
+
taskId: task.id,
|
|
108
|
+
eventType: typeDef.type,
|
|
109
|
+
emptyStep: result.step,
|
|
110
|
+
})
|
|
111
|
+
} else if (fresh.unnotifiedAt) {
|
|
112
|
+
fresh.unnotifiedAt = null
|
|
113
|
+
fresh.updatedAt = new Date()
|
|
114
|
+
await em.flush()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return result
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The ticket got an assignee, so somebody now owns it: clear the "nobody notified" flag. */
|
|
121
|
+
export async function clearSupportTicketUnnotified(
|
|
122
|
+
em: EntityManager,
|
|
123
|
+
input: { taskId: string; tenantId: string; organizationId: string },
|
|
124
|
+
): Promise<boolean> {
|
|
125
|
+
const scope = { tenantId: input.tenantId, organizationId: input.organizationId }
|
|
126
|
+
const ticket = await findOneWithDecryption(
|
|
127
|
+
em,
|
|
128
|
+
StaffSupportTicket,
|
|
129
|
+
{ taskId: input.taskId, ...scope, deletedAt: null, unnotifiedAt: { $ne: null } },
|
|
130
|
+
undefined,
|
|
131
|
+
scope,
|
|
132
|
+
)
|
|
133
|
+
if (!ticket) return false
|
|
134
|
+
ticket.unnotifiedAt = null
|
|
135
|
+
ticket.updatedAt = new Date()
|
|
136
|
+
await em.flush()
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
3
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
4
|
+
import type { Attachment as AttachmentEntity } from '@open-mercato/core/modules/attachments/data/entities'
|
|
5
|
+
import type { SupportPortalScope } from './portalTickets'
|
|
6
|
+
import {
|
|
7
|
+
CUSTOMER_UPLOAD_TAG_PREFIX,
|
|
8
|
+
CUSTOMER_VISIBLE_ATTACHMENT_TAG,
|
|
9
|
+
SUPPORT_TICKET_ATTACHMENT_ENTITY_ID,
|
|
10
|
+
customerUploadTag,
|
|
11
|
+
} from './attachments'
|
|
12
|
+
|
|
13
|
+
const logger = createLogger('staff').child({ component: 'lib/support/portalAttachments' })
|
|
14
|
+
|
|
15
|
+
export type PortalAttachmentsApi = {
|
|
16
|
+
Attachment: typeof import('@open-mercato/core/modules/attachments/data/entities').Attachment
|
|
17
|
+
AttachmentPartition: typeof import('@open-mercato/core/modules/attachments/data/entities').AttachmentPartition
|
|
18
|
+
StorageDriverFactory: typeof import('@open-mercato/core/modules/attachments/lib/drivers').StorageDriverFactory
|
|
19
|
+
checkAttachmentAccess: typeof import('@open-mercato/core/modules/attachments/lib/access').checkAttachmentAccess
|
|
20
|
+
attachmentCrudEvents: typeof import('@open-mercato/core/modules/attachments/lib/crud').attachmentCrudEvents
|
|
21
|
+
attachmentCrudIndexer: typeof import('@open-mercato/core/modules/attachments/lib/crud').attachmentCrudIndexer
|
|
22
|
+
readAttachmentMetadata: typeof import('@open-mercato/core/modules/attachments/lib/metadata').readAttachmentMetadata
|
|
23
|
+
slugifyAttachmentFileName: typeof import('@open-mercato/core/modules/attachments/lib/imageUrls').slugifyAttachmentFileName
|
|
24
|
+
clearAttachmentThumbnailCache: typeof import('@open-mercato/core/modules/attachments/lib/thumbnailCache').clearAttachmentThumbnailCache
|
|
25
|
+
isMultipartRequestWithinUploadLimit: typeof import('@open-mercato/core/modules/attachments/lib/upload-limits').isMultipartRequestWithinUploadLimit
|
|
26
|
+
resolveAttachmentMaxBytes: typeof import('@open-mercato/core/modules/attachments/lib/upload-limits').resolveAttachmentMaxBytes
|
|
27
|
+
isScopedAttachmentUploadError: typeof import('@open-mercato/core/modules/attachments/lib/scoped-upload-service').isScopedAttachmentUploadError
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const portalSupportAttachmentItemSchema = z.object({
|
|
31
|
+
id: z.string().uuid(),
|
|
32
|
+
fileName: z.string(),
|
|
33
|
+
fileSize: z.number(),
|
|
34
|
+
mimeType: z.string().nullable(),
|
|
35
|
+
downloadUrl: z.string(),
|
|
36
|
+
uploadedByCustomer: z.boolean(),
|
|
37
|
+
canDelete: z.boolean(),
|
|
38
|
+
createdAt: z.string().nullable(),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
export type PortalSupportAttachmentItem = z.infer<typeof portalSupportAttachmentItemSchema>
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Staff keeps no static edge to the attachments module: it is loaded on demand and a
|
|
45
|
+
* deployment without it reads as "attachments unavailable" (`null`).
|
|
46
|
+
*/
|
|
47
|
+
export async function loadPortalAttachmentsApi(): Promise<PortalAttachmentsApi | null> {
|
|
48
|
+
try {
|
|
49
|
+
const [entities, drivers, access, crud, metadataLib, imageUrls, thumbnails, limits, uploadService] = await Promise.all([
|
|
50
|
+
import('@open-mercato/core/modules/attachments/data/entities'),
|
|
51
|
+
import('@open-mercato/core/modules/attachments/lib/drivers'),
|
|
52
|
+
import('@open-mercato/core/modules/attachments/lib/access'),
|
|
53
|
+
import('@open-mercato/core/modules/attachments/lib/crud'),
|
|
54
|
+
import('@open-mercato/core/modules/attachments/lib/metadata'),
|
|
55
|
+
import('@open-mercato/core/modules/attachments/lib/imageUrls'),
|
|
56
|
+
import('@open-mercato/core/modules/attachments/lib/thumbnailCache'),
|
|
57
|
+
import('@open-mercato/core/modules/attachments/lib/upload-limits'),
|
|
58
|
+
import('@open-mercato/core/modules/attachments/lib/scoped-upload-service'),
|
|
59
|
+
])
|
|
60
|
+
return {
|
|
61
|
+
Attachment: entities.Attachment,
|
|
62
|
+
AttachmentPartition: entities.AttachmentPartition,
|
|
63
|
+
StorageDriverFactory: drivers.StorageDriverFactory,
|
|
64
|
+
checkAttachmentAccess: access.checkAttachmentAccess,
|
|
65
|
+
attachmentCrudEvents: crud.attachmentCrudEvents,
|
|
66
|
+
attachmentCrudIndexer: crud.attachmentCrudIndexer,
|
|
67
|
+
readAttachmentMetadata: metadataLib.readAttachmentMetadata,
|
|
68
|
+
slugifyAttachmentFileName: imageUrls.slugifyAttachmentFileName,
|
|
69
|
+
clearAttachmentThumbnailCache: thumbnails.clearAttachmentThumbnailCache,
|
|
70
|
+
isMultipartRequestWithinUploadLimit: limits.isMultipartRequestWithinUploadLimit,
|
|
71
|
+
resolveAttachmentMaxBytes: limits.resolveAttachmentMaxBytes,
|
|
72
|
+
isScopedAttachmentUploadError: uploadService.isScopedAttachmentUploadError,
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
logger.warn('staff portal support attachments module unavailable', { err })
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function readAttachmentTags(api: PortalAttachmentsApi, attachment: AttachmentEntity): string[] {
|
|
81
|
+
return api.readAttachmentMetadata(attachment.storageMetadata).tags ?? []
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function portalAttachmentAuth(scope: SupportPortalScope) {
|
|
85
|
+
return { sub: scope.auth.sub, tenantId: scope.tenantId, orgId: scope.organizationId, email: scope.auth.email }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function serializePortalAttachment(
|
|
89
|
+
api: PortalAttachmentsApi,
|
|
90
|
+
attachment: AttachmentEntity,
|
|
91
|
+
taskId: string,
|
|
92
|
+
customerUserId: string,
|
|
93
|
+
canManage = false,
|
|
94
|
+
): PortalSupportAttachmentItem {
|
|
95
|
+
const tags = readAttachmentTags(api, attachment)
|
|
96
|
+
return {
|
|
97
|
+
id: attachment.id,
|
|
98
|
+
fileName: attachment.fileName,
|
|
99
|
+
fileSize: attachment.fileSize,
|
|
100
|
+
mimeType: attachment.mimeType ?? null,
|
|
101
|
+
downloadUrl: `/api/staff/portal/support-tickets/${encodeURIComponent(taskId)}/attachments?attachmentId=${encodeURIComponent(attachment.id)}`,
|
|
102
|
+
uploadedByCustomer: tags.some((tag) => tag.startsWith(CUSTOMER_UPLOAD_TAG_PREFIX)),
|
|
103
|
+
canDelete: canManage && tags.includes(customerUploadTag(customerUserId)),
|
|
104
|
+
createdAt: attachment.createdAt ? attachment.createdAt.toISOString() : null,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The files of a ticket the customer may see: tagged `customer-visible` and passing
|
|
110
|
+
* the attachments module's own access check. Internal staff files are never returned.
|
|
111
|
+
* `canManage` is whether the caller holds `portal.support_tickets.manage`; without it
|
|
112
|
+
* no item reports `canDelete`, matching the DELETE route's own feature check.
|
|
113
|
+
*/
|
|
114
|
+
export async function listCustomerVisibleAttachments(
|
|
115
|
+
api: PortalAttachmentsApi,
|
|
116
|
+
scope: SupportPortalScope,
|
|
117
|
+
taskId: string,
|
|
118
|
+
canManage = false,
|
|
119
|
+
): Promise<PortalSupportAttachmentItem[]> {
|
|
120
|
+
const decryptionScope = { tenantId: scope.tenantId, organizationId: scope.organizationId }
|
|
121
|
+
const attachments = await findWithDecryption(
|
|
122
|
+
scope.em,
|
|
123
|
+
api.Attachment,
|
|
124
|
+
{ entityId: SUPPORT_TICKET_ATTACHMENT_ENTITY_ID, recordId: taskId, ...decryptionScope },
|
|
125
|
+
{ orderBy: { createdAt: 'ASC' } },
|
|
126
|
+
decryptionScope,
|
|
127
|
+
)
|
|
128
|
+
const codes = Array.from(new Set(attachments.map((attachment) => attachment.partitionCode)))
|
|
129
|
+
const partitions = codes.length ? await scope.em.find(api.AttachmentPartition, { code: { $in: codes } }) : []
|
|
130
|
+
const partitionByCode = new Map(partitions.map((partition) => [partition.code, partition]))
|
|
131
|
+
const auth = portalAttachmentAuth(scope)
|
|
132
|
+
return attachments
|
|
133
|
+
.filter((attachment) => {
|
|
134
|
+
const partition = partitionByCode.get(attachment.partitionCode)
|
|
135
|
+
if (!partition) return false
|
|
136
|
+
if (!readAttachmentTags(api, attachment).includes(CUSTOMER_VISIBLE_ATTACHMENT_TAG)) return false
|
|
137
|
+
return api.checkAttachmentAccess(auth, attachment, partition, { requireAuthForPublic: true }).ok
|
|
138
|
+
})
|
|
139
|
+
.map((attachment) => serializePortalAttachment(api, attachment, taskId, scope.auth.sub, canManage))
|
|
140
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
3
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
4
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
5
|
+
import { StaffSupportTicket, StaffTimeTask, StaffTimeTaskComment, StaffTimeTaskStatus } from '../../data/entities'
|
|
6
|
+
import { emitStaffEvent } from '../../events'
|
|
7
|
+
import { filterPortalRecipientsByFeatureStrict, resolvePortalRecipientUserIdsStrict } from '../time-tracking/portalRecipients'
|
|
8
|
+
import { derivePortalStatus } from './portalStatus'
|
|
9
|
+
import { PORTAL_SUPPORT_TICKETS_VIEW_FEATURE } from './portalTickets'
|
|
10
|
+
|
|
11
|
+
const logger = createLogger('staff').child({ component: 'lib/support/portalBroadcast' })
|
|
12
|
+
|
|
13
|
+
export const PORTAL_FANOUT_BATCH_SIZE = 200
|
|
14
|
+
|
|
15
|
+
type Scope = { tenantId: string; organizationId: string }
|
|
16
|
+
|
|
17
|
+
export type PortalUpdateKind = 'status' | 'reply'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `version` names the change being announced (the task version for a status change,
|
|
21
|
+
* the comment for a reply). The portal event bridge drops an event whose payload
|
|
22
|
+
* repeats one seen in the last 500 ms, so two different changes must never produce
|
|
23
|
+
* identical payloads, while two publishers of the same change still collapse.
|
|
24
|
+
*
|
|
25
|
+
* Resolves `true` when the update was emitted and `false` when there is genuinely no
|
|
26
|
+
* one to tell (no customer, no portal users, nobody holding the view feature, or the
|
|
27
|
+
* `customer_accounts` module absent). Any other recipient-lookup failure rejects, so a
|
|
28
|
+
* caller never records a publication that did not happen.
|
|
29
|
+
*/
|
|
30
|
+
async function emitPortalUpdate(
|
|
31
|
+
em: EntityManager,
|
|
32
|
+
task: StaffTimeTask,
|
|
33
|
+
ticket: StaffSupportTicket,
|
|
34
|
+
portalStatus: string,
|
|
35
|
+
kind: PortalUpdateKind,
|
|
36
|
+
version: string,
|
|
37
|
+
): Promise<boolean> {
|
|
38
|
+
if (!task.customerId) return false
|
|
39
|
+
const customerUserIds = await resolvePortalRecipientUserIdsStrict(em, {
|
|
40
|
+
tenantId: task.tenantId,
|
|
41
|
+
organizationId: task.organizationId,
|
|
42
|
+
customerId: task.customerId,
|
|
43
|
+
})
|
|
44
|
+
const recipientUserIds = await filterPortalRecipientsByFeatureStrict(
|
|
45
|
+
em,
|
|
46
|
+
customerUserIds,
|
|
47
|
+
{ tenantId: task.tenantId, organizationId: task.organizationId },
|
|
48
|
+
PORTAL_SUPPORT_TICKETS_VIEW_FEATURE,
|
|
49
|
+
)
|
|
50
|
+
if (recipientUserIds.length === 0) return false
|
|
51
|
+
await emitStaffEvent(
|
|
52
|
+
'staff.support_ticket.portal_updated',
|
|
53
|
+
{
|
|
54
|
+
id: task.id,
|
|
55
|
+
tenantId: task.tenantId,
|
|
56
|
+
organizationId: task.organizationId,
|
|
57
|
+
ticketId: task.id,
|
|
58
|
+
supportReference: ticket.supportReference,
|
|
59
|
+
portalStatus,
|
|
60
|
+
kind,
|
|
61
|
+
version,
|
|
62
|
+
recipientUserIds,
|
|
63
|
+
},
|
|
64
|
+
{ persistent: true },
|
|
65
|
+
)
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function emitActivity(task: StaffTimeTask, ticket: StaffSupportTicket, kind: 'portal_status'): void {
|
|
70
|
+
void emitStaffEvent(
|
|
71
|
+
'staff.support_ticket.activity_recorded',
|
|
72
|
+
{
|
|
73
|
+
id: ticket.id,
|
|
74
|
+
tenantId: task.tenantId,
|
|
75
|
+
organizationId: task.organizationId,
|
|
76
|
+
taskId: task.id,
|
|
77
|
+
projectId: task.timeProjectId,
|
|
78
|
+
kind,
|
|
79
|
+
},
|
|
80
|
+
{ persistent: true },
|
|
81
|
+
).catch((err) => {
|
|
82
|
+
logger.error('staff support activity emit failed', { err, taskId: task.id })
|
|
83
|
+
getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.support_activity_emit_failed' })
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Compare-and-publish: re-derive the customer-facing status of a portal ticket and
|
|
89
|
+
* broadcast only when it differs from what the portal was last told. Safe to call
|
|
90
|
+
* on any task event; internal tasks and tasks without a ticket row are ignored.
|
|
91
|
+
*
|
|
92
|
+
* `always` skips the comparison for triggers that are a status change by definition.
|
|
93
|
+
* Concurrent publishers (a reopen followed quickly by a resolve) each run on their own
|
|
94
|
+
* fork, so both can read a stale `portalStatusPublished` and skip; the portal refetches
|
|
95
|
+
* the ticket on every event, so publishing after each status change keeps it correct.
|
|
96
|
+
*
|
|
97
|
+
* `portalStatusPublished` advances only after `emitPortalUpdate` settles: an emitted
|
|
98
|
+
* update or a genuinely empty audience. A recipient-lookup or emit failure propagates
|
|
99
|
+
* with the marker untouched, so the persistent fan-out retries and republishes.
|
|
100
|
+
*/
|
|
101
|
+
export async function publishTaskPortalStatus(
|
|
102
|
+
em: EntityManager,
|
|
103
|
+
taskId: string,
|
|
104
|
+
scope: Scope,
|
|
105
|
+
options: { always?: boolean } = {},
|
|
106
|
+
): Promise<boolean> {
|
|
107
|
+
const task = await em.findOne(StaffTimeTask, { id: taskId, ...scope, source: 'portal', deletedAt: null })
|
|
108
|
+
if (!task) return false
|
|
109
|
+
const ticket = await findOneWithDecryption(em, StaffSupportTicket, { taskId: task.id, ...scope, deletedAt: null }, undefined, scope)
|
|
110
|
+
if (!ticket) return false
|
|
111
|
+
const status = await em.findOne(StaffTimeTaskStatus, { id: task.taskStatusId, ...scope }, { fields: ['id', 'isDefault', 'isDone'] })
|
|
112
|
+
const portalStatus = derivePortalStatus(status)
|
|
113
|
+
const unchanged = ticket.portalStatusPublished === portalStatus
|
|
114
|
+
if (unchanged && !options.always) return false
|
|
115
|
+
await emitPortalUpdate(em, task, ticket, portalStatus, 'status', task.updatedAt ? task.updatedAt.toISOString() : task.id)
|
|
116
|
+
if (!unchanged) {
|
|
117
|
+
ticket.portalStatusPublished = portalStatus
|
|
118
|
+
ticket.updatedAt = new Date()
|
|
119
|
+
await em.flush()
|
|
120
|
+
}
|
|
121
|
+
emitActivity(task, ticket, 'portal_status')
|
|
122
|
+
return true
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A status column's `isDone`/`isDefault` flags changed: re-derive every live portal ticket in it, in batches. */
|
|
126
|
+
export async function fanOutStatusColumn(em: EntityManager, taskStatusId: string, scope: Scope): Promise<number> {
|
|
127
|
+
let published = 0
|
|
128
|
+
let offset = 0
|
|
129
|
+
for (;;) {
|
|
130
|
+
const tasks = await em.find(
|
|
131
|
+
StaffTimeTask,
|
|
132
|
+
{ taskStatusId, ...scope, source: 'portal', deletedAt: null },
|
|
133
|
+
{ fields: ['id'], limit: PORTAL_FANOUT_BATCH_SIZE, offset, orderBy: { id: 'asc' } },
|
|
134
|
+
)
|
|
135
|
+
if (tasks.length === 0) break
|
|
136
|
+
for (const task of tasks) {
|
|
137
|
+
if (await publishTaskPortalStatus(em.fork(), task.id, scope)) published += 1
|
|
138
|
+
}
|
|
139
|
+
if (tasks.length < PORTAL_FANOUT_BATCH_SIZE) break
|
|
140
|
+
offset += PORTAL_FANOUT_BATCH_SIZE
|
|
141
|
+
}
|
|
142
|
+
return published
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** A customer-visible comment was published, retracted or deleted: tell the ticket's own customer users to refetch. */
|
|
146
|
+
export async function publishTicketReply(em: EntityManager, commentId: string, scope: Scope): Promise<boolean> {
|
|
147
|
+
const comment = await em.findOne(StaffTimeTaskComment, { id: commentId, ...scope }, { fields: ['id', 'taskId', 'updatedAt'] })
|
|
148
|
+
if (!comment) return false
|
|
149
|
+
const task = await em.findOne(StaffTimeTask, { id: comment.taskId, ...scope, source: 'portal', deletedAt: null })
|
|
150
|
+
if (!task) return false
|
|
151
|
+
const ticket = await findOneWithDecryption(em, StaffSupportTicket, { taskId: task.id, ...scope, deletedAt: null }, undefined, scope)
|
|
152
|
+
if (!ticket) return false
|
|
153
|
+
const status = await em.findOne(StaffTimeTaskStatus, { id: task.taskStatusId, ...scope }, { fields: ['id', 'isDefault', 'isDone'] })
|
|
154
|
+
return emitPortalUpdate(em, task, ticket, derivePortalStatus(status), 'reply', `${comment.id}:${comment.updatedAt ? comment.updatedAt.toISOString() : ''}`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function readEventScope(payload: { tenantId?: unknown; organizationId?: unknown }): Scope | null {
|
|
158
|
+
const tenantId = typeof payload.tenantId === 'string' ? payload.tenantId : null
|
|
159
|
+
const organizationId = typeof payload.organizationId === 'string' ? payload.organizationId : null
|
|
160
|
+
return tenantId && organizationId ? { tenantId, organizationId } : null
|
|
161
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
4
|
+
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
5
|
+
import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
|
|
6
|
+
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
7
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
8
|
+
import { checkRateLimit, RATE_LIMIT_ERROR_FALLBACK, RATE_LIMIT_ERROR_KEY } from '@open-mercato/shared/lib/ratelimit/helpers'
|
|
9
|
+
import { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'
|
|
10
|
+
import type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'
|
|
11
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
12
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
13
|
+
import type { SupportPortalScope } from './portalTickets'
|
|
14
|
+
|
|
15
|
+
const logger = createLogger('staff').child({ component: 'lib/support/portalRoute' })
|
|
16
|
+
|
|
17
|
+
export const SUPPORT_TICKET_RESOURCE_KIND = 'staff.support_ticket'
|
|
18
|
+
|
|
19
|
+
const RATE_LIMITS = {
|
|
20
|
+
create: readEndpointRateLimitConfig('STAFF_SUPPORT_TICKET_CREATE', {
|
|
21
|
+
points: 10,
|
|
22
|
+
duration: 3600,
|
|
23
|
+
keyPrefix: 'staff_support_ticket_create',
|
|
24
|
+
}),
|
|
25
|
+
message: readEndpointRateLimitConfig('STAFF_SUPPORT_TICKET_MESSAGE', {
|
|
26
|
+
points: 60,
|
|
27
|
+
duration: 3600,
|
|
28
|
+
keyPrefix: 'staff_support_ticket_message',
|
|
29
|
+
}),
|
|
30
|
+
upload: readEndpointRateLimitConfig('STAFF_SUPPORT_TICKET_UPLOAD', {
|
|
31
|
+
points: 30,
|
|
32
|
+
duration: 3600,
|
|
33
|
+
keyPrefix: 'staff_support_ticket_upload',
|
|
34
|
+
}),
|
|
35
|
+
} as const
|
|
36
|
+
|
|
37
|
+
export type PortalRateLimitKind = keyof typeof RATE_LIMITS
|
|
38
|
+
|
|
39
|
+
export function buildPortalCommandContext(scope: SupportPortalScope, req: Request): CommandRuntimeContext {
|
|
40
|
+
const auth: NonNullable<AuthContext> = {
|
|
41
|
+
sub: scope.auth.sub,
|
|
42
|
+
sid: scope.auth.sid,
|
|
43
|
+
tenantId: scope.auth.tenantId,
|
|
44
|
+
orgId: scope.auth.orgId,
|
|
45
|
+
email: scope.auth.email,
|
|
46
|
+
customerEntityId: scope.auth.customerEntityId ?? null,
|
|
47
|
+
personEntityId: scope.auth.personEntityId ?? null,
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
container: scope.container,
|
|
51
|
+
auth,
|
|
52
|
+
organizationScope: null,
|
|
53
|
+
selectedOrganizationId: scope.organizationId,
|
|
54
|
+
organizationIds: [scope.organizationId],
|
|
55
|
+
request: req,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Per customer user, so one noisy account cannot exhaust its colleagues' budget. */
|
|
60
|
+
export async function checkPortalRateLimit(
|
|
61
|
+
scope: SupportPortalScope,
|
|
62
|
+
kind: PortalRateLimitKind,
|
|
63
|
+
): Promise<Response | null> {
|
|
64
|
+
let service: RateLimiterService | null = null
|
|
65
|
+
try {
|
|
66
|
+
service = scope.container.resolve('rateLimiterService') as RateLimiterService
|
|
67
|
+
} catch {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
if (!service) return null
|
|
71
|
+
const { translate } = await resolveTranslations()
|
|
72
|
+
return checkRateLimit(
|
|
73
|
+
service,
|
|
74
|
+
RATE_LIMITS[kind],
|
|
75
|
+
`${scope.tenantId}:${scope.auth.sub}`,
|
|
76
|
+
translate(RATE_LIMIT_ERROR_KEY, RATE_LIMIT_ERROR_FALLBACK),
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function portalErrorResponse(err: unknown, label: string): Promise<Response> {
|
|
81
|
+
if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })
|
|
82
|
+
const rejection = getCommandInterceptorHttpRejection(err)
|
|
83
|
+
if (rejection) return NextResponse.json(rejection.body, { status: rejection.status })
|
|
84
|
+
if (err instanceof z.ZodError) {
|
|
85
|
+
const { translate } = await resolveTranslations()
|
|
86
|
+
return NextResponse.json(
|
|
87
|
+
{ ok: false, error: translate('staff.errors.invalid_request', 'Invalid request'), details: err.issues },
|
|
88
|
+
{ status: 400 },
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
logger.error(label, { err })
|
|
92
|
+
getTelemetryRuntime()?.reportError(err, {
|
|
93
|
+
module: 'staff',
|
|
94
|
+
code: 'staff.portal_support_route_failed',
|
|
95
|
+
attributes: { operation: label },
|
|
96
|
+
})
|
|
97
|
+
return NextResponse.json({ ok: false, error: 'staff.errors.internal' }, { status: 500 })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function readPathId(req: Request, pattern: RegExp): string | null {
|
|
101
|
+
try {
|
|
102
|
+
const match = new URL(req.url).pathname.match(pattern)
|
|
103
|
+
const id = match?.[1] ? decodeURIComponent(match[1]) : null
|
|
104
|
+
return id && z.string().uuid().safeParse(id).success ? id : null
|
|
105
|
+
} catch {
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StaffSupportPortalStatus } from '../../data/entities'
|
|
2
|
+
|
|
3
|
+
export type PortalStatusFlags = {
|
|
4
|
+
isDefault: boolean
|
|
5
|
+
isDone: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function derivePortalStatus(status: PortalStatusFlags | null | undefined): StaffSupportPortalStatus {
|
|
9
|
+
if (!status) return 'received'
|
|
10
|
+
if (status.isDone) return 'resolved'
|
|
11
|
+
if (status.isDefault) return 'received'
|
|
12
|
+
return 'in_progress'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const SUPPORT_PORTAL_STATUSES: readonly StaffSupportPortalStatus[] = ['received', 'in_progress', 'resolved']
|