@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,233 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { sql } from 'kysely'
|
|
3
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
|
+
import type { CustomerAuthContext } from '@open-mercato/shared/modules/customer-auth'
|
|
5
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
7
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
8
|
+
import { CustomerEntity } from '@open-mercato/core/modules/customers/data/entities'
|
|
9
|
+
import { StaffSupportTicket, StaffTimeTask } from '../../data/entities'
|
|
10
|
+
import { PORTAL_SUPPORT_NOT_LINKED_CODE } from '../support-ui/portalSupport'
|
|
11
|
+
|
|
12
|
+
const logger = createLogger('staff').child({ component: 'lib/support/portalTickets' })
|
|
13
|
+
|
|
14
|
+
export const PORTAL_SUPPORT_TICKETS_VIEW_FEATURE = 'portal.support_tickets.view'
|
|
15
|
+
export const PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE = 'portal.support_tickets.manage'
|
|
16
|
+
|
|
17
|
+
type AppContainer = Awaited<ReturnType<typeof createRequestContainer>>
|
|
18
|
+
|
|
19
|
+
export type SupportPortalScope = {
|
|
20
|
+
auth: CustomerAuthContext
|
|
21
|
+
customerId: string
|
|
22
|
+
tenantId: string
|
|
23
|
+
organizationId: string
|
|
24
|
+
container: AppContainer
|
|
25
|
+
em: EntityManager
|
|
26
|
+
hasFeature: (feature: string) => Promise<boolean>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type CustomerFeatureGuard = (auth: CustomerAuthContext, features: string[], rbac: unknown) => Promise<void>
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The portal identity is resolved through `customer_accounts` with a dynamic import,
|
|
33
|
+
* exactly like the EP-50 time-reports routes, so `staff` keeps no static edge to the
|
|
34
|
+
* portal module: a deployment without it answers 401 instead of failing to load.
|
|
35
|
+
* A session with no `customerEntityId` is refused (403); a missing feature guard
|
|
36
|
+
* fails closed (403) so a revoked grant keeps working as a revocation.
|
|
37
|
+
*/
|
|
38
|
+
export async function resolveSupportPortalScope(
|
|
39
|
+
req: Request,
|
|
40
|
+
requiredFeature: string,
|
|
41
|
+
): Promise<SupportPortalScope | Response> {
|
|
42
|
+
let auth: CustomerAuthContext | null = null
|
|
43
|
+
let requireFeature: CustomerFeatureGuard | null = null
|
|
44
|
+
try {
|
|
45
|
+
const customerAuth = await import('@open-mercato/core/modules/customer_accounts/lib/customerAuth')
|
|
46
|
+
auth = await customerAuth.getCustomerAuthFromRequest(req)
|
|
47
|
+
const guard: unknown = (customerAuth as { requireCustomerFeature?: unknown }).requireCustomerFeature
|
|
48
|
+
requireFeature = typeof guard === 'function' ? (guard as CustomerFeatureGuard) : null
|
|
49
|
+
} catch (err) {
|
|
50
|
+
logger.warn('staff portal support could not resolve customer auth', { err })
|
|
51
|
+
return NextResponse.json({ ok: false, error: 'staff.errors.unauthorized' }, { status: 401 })
|
|
52
|
+
}
|
|
53
|
+
if (!auth) return NextResponse.json({ ok: false, error: 'staff.errors.unauthorized' }, { status: 401 })
|
|
54
|
+
const customerId =
|
|
55
|
+
typeof auth.customerEntityId === 'string' && auth.customerEntityId.trim().length > 0
|
|
56
|
+
? auth.customerEntityId.trim()
|
|
57
|
+
: null
|
|
58
|
+
if (!customerId) {
|
|
59
|
+
return NextResponse.json(
|
|
60
|
+
{ ok: false, error: 'staff.errors.customerAccountNotLinked', code: PORTAL_SUPPORT_NOT_LINKED_CODE },
|
|
61
|
+
{ status: 403 },
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
if (!requireFeature) {
|
|
65
|
+
logger.error('staff portal support could not resolve requireCustomerFeature; refusing the request', {
|
|
66
|
+
feature: requiredFeature,
|
|
67
|
+
})
|
|
68
|
+
return NextResponse.json({ ok: false, error: 'staff.errors.forbidden' }, { status: 403 })
|
|
69
|
+
}
|
|
70
|
+
const guard = requireFeature
|
|
71
|
+
const container = await createRequestContainer()
|
|
72
|
+
const resolvedAuth = auth
|
|
73
|
+
const hasFeature = async (feature: string): Promise<boolean> => {
|
|
74
|
+
try {
|
|
75
|
+
await guard(resolvedAuth, [feature], container.resolve('customerRbacService'))
|
|
76
|
+
return true
|
|
77
|
+
} catch {
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
await guard(auth, [requiredFeature], container.resolve('customerRbacService'))
|
|
83
|
+
} catch (response) {
|
|
84
|
+
if (response instanceof Response) return response
|
|
85
|
+
logger.warn('staff portal support feature check failed', { err: response })
|
|
86
|
+
return NextResponse.json({ ok: false, error: 'staff.errors.forbidden' }, { status: 403 })
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
auth,
|
|
90
|
+
customerId,
|
|
91
|
+
tenantId: auth.tenantId,
|
|
92
|
+
organizationId: auth.orgId,
|
|
93
|
+
container,
|
|
94
|
+
em: (container.resolve('em') as EntityManager).fork(),
|
|
95
|
+
hasFeature,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* THE ownership clause for every portal support read and write. Ownership comes from
|
|
101
|
+
* the task's own `customer_id` — never the project's — so tickets from many customers
|
|
102
|
+
* can share one project. Callers load WITH this clause rather than loading and
|
|
103
|
+
* checking afterwards, so a foreign id is never read even momentarily.
|
|
104
|
+
*/
|
|
105
|
+
export function portalOwnedTaskWhere(scope: Pick<SupportPortalScope, 'tenantId' | 'organizationId' | 'customerId'>) {
|
|
106
|
+
return {
|
|
107
|
+
tenantId: scope.tenantId,
|
|
108
|
+
organizationId: scope.organizationId,
|
|
109
|
+
customerId: scope.customerId,
|
|
110
|
+
source: 'portal' as const,
|
|
111
|
+
deletedAt: null,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function loadPortalOwnedTicket(
|
|
116
|
+
scope: Pick<SupportPortalScope, 'tenantId' | 'organizationId' | 'customerId' | 'em'>,
|
|
117
|
+
taskId: string,
|
|
118
|
+
): Promise<{ task: StaffTimeTask; ticket: StaffSupportTicket } | null> {
|
|
119
|
+
const task = await scope.em.findOne(StaffTimeTask, { id: taskId, ...portalOwnedTaskWhere(scope) })
|
|
120
|
+
if (!task) return null
|
|
121
|
+
const ticket = await findOneWithDecryption(
|
|
122
|
+
scope.em,
|
|
123
|
+
StaffSupportTicket,
|
|
124
|
+
{ taskId: task.id, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },
|
|
125
|
+
undefined,
|
|
126
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
127
|
+
)
|
|
128
|
+
if (!ticket) return null
|
|
129
|
+
return { task, ticket }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function resolveCustomerDisplay(
|
|
133
|
+
scope: Pick<SupportPortalScope, 'tenantId' | 'organizationId' | 'customerId' | 'em'>,
|
|
134
|
+
): Promise<{ displayName: string | null; kind: string | null }> {
|
|
135
|
+
try {
|
|
136
|
+
const customer = await findOneWithDecryption(
|
|
137
|
+
scope.em,
|
|
138
|
+
CustomerEntity,
|
|
139
|
+
{ id: scope.customerId, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },
|
|
140
|
+
undefined,
|
|
141
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
142
|
+
)
|
|
143
|
+
const displayName = typeof customer?.displayName === 'string' ? customer.displayName : null
|
|
144
|
+
const kind = typeof customer?.kind === 'string' ? customer.kind : null
|
|
145
|
+
return { displayName, kind }
|
|
146
|
+
} catch {
|
|
147
|
+
return { displayName: null, kind: null }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
type PortalTicketActivityDb = {
|
|
152
|
+
staff_time_tasks: {
|
|
153
|
+
id: string
|
|
154
|
+
tenant_id: string
|
|
155
|
+
organization_id: string
|
|
156
|
+
customer_id: string | null
|
|
157
|
+
source: string
|
|
158
|
+
task_status_id: string
|
|
159
|
+
created_at: Date
|
|
160
|
+
deleted_at: Date | null
|
|
161
|
+
}
|
|
162
|
+
staff_time_task_comments: {
|
|
163
|
+
task_id: string
|
|
164
|
+
tenant_id: string
|
|
165
|
+
organization_id: string
|
|
166
|
+
visibility: string
|
|
167
|
+
created_at: Date
|
|
168
|
+
deleted_at: Date | null
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export type PortalTicketPageRow = { id: string; lastActivityAt: Date }
|
|
173
|
+
|
|
174
|
+
function toDate(value: unknown, fallback: Date): Date {
|
|
175
|
+
if (value instanceof Date) return value
|
|
176
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
177
|
+
const parsed = new Date(value)
|
|
178
|
+
if (!Number.isNaN(parsed.getTime())) return parsed
|
|
179
|
+
}
|
|
180
|
+
return fallback
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* One page of the caller's tickets, newest customer-visible activity first. Activity
|
|
185
|
+
* is the latest of the ticket's filing time and its newest customer-visible comment
|
|
186
|
+
* (either side), so internal staff edits — which bump `updated_at` — never reorder the
|
|
187
|
+
* customer's list while a reply the customer can read always does. The ownership
|
|
188
|
+
* clause is the same as `portalOwnedTaskWhere`; the activity is computed per row by a
|
|
189
|
+
* correlated `max()` served by the comments `(organization_id, task_id, created_at)` index.
|
|
190
|
+
*/
|
|
191
|
+
export async function listPortalTicketPage(
|
|
192
|
+
scope: Pick<SupportPortalScope, 'tenantId' | 'organizationId' | 'customerId' | 'em'>,
|
|
193
|
+
options: { statusIds: readonly string[] | null; limit: number; offset: number },
|
|
194
|
+
): Promise<{ rows: PortalTicketPageRow[]; total: number }> {
|
|
195
|
+
if (options.statusIds && options.statusIds.length === 0) return { rows: [], total: 0 }
|
|
196
|
+
const db = scope.em.getKysely<PortalTicketActivityDb>()
|
|
197
|
+
let owned = db
|
|
198
|
+
.selectFrom('staff_time_tasks as task')
|
|
199
|
+
.where('task.tenant_id', '=', scope.tenantId)
|
|
200
|
+
.where('task.organization_id', '=', scope.organizationId)
|
|
201
|
+
.where('task.customer_id', '=', scope.customerId)
|
|
202
|
+
.where('task.source', '=', 'portal')
|
|
203
|
+
.where('task.deleted_at', 'is', null)
|
|
204
|
+
if (options.statusIds) owned = owned.where('task.task_status_id', 'in', [...options.statusIds])
|
|
205
|
+
const lastActivity = sql<Date>`greatest(task.created_at, coalesce((
|
|
206
|
+
select max(visible_comment.created_at)
|
|
207
|
+
from staff_time_task_comments as visible_comment
|
|
208
|
+
where visible_comment.organization_id = task.organization_id
|
|
209
|
+
and visible_comment.task_id = task.id
|
|
210
|
+
and visible_comment.tenant_id = task.tenant_id
|
|
211
|
+
and visible_comment.visibility = 'customer'
|
|
212
|
+
and visible_comment.deleted_at is null
|
|
213
|
+
), task.created_at))`
|
|
214
|
+
const [countRow, pageRows] = await Promise.all([
|
|
215
|
+
owned.select((builder) => builder.fn.countAll<string | number>().as('total')).executeTakeFirst(),
|
|
216
|
+
owned
|
|
217
|
+
.select(['task.id as id', 'task.created_at as created_at'])
|
|
218
|
+
.select(lastActivity.as('last_activity_at'))
|
|
219
|
+
.orderBy('last_activity_at', 'desc')
|
|
220
|
+
.orderBy('task.id', 'desc')
|
|
221
|
+
.limit(options.limit)
|
|
222
|
+
.offset(options.offset)
|
|
223
|
+
.execute(),
|
|
224
|
+
])
|
|
225
|
+
const total = Number(countRow?.total ?? 0)
|
|
226
|
+
return {
|
|
227
|
+
total: Number.isFinite(total) ? total : 0,
|
|
228
|
+
rows: pageRows.map((row) => ({
|
|
229
|
+
id: row.id,
|
|
230
|
+
lastActivityAt: toDate(row.last_activity_at, toDate(row.created_at, new Date(0))),
|
|
231
|
+
})),
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import {
|
|
3
|
+
StaffSupportRoutingRule,
|
|
4
|
+
StaffTimeProject,
|
|
5
|
+
StaffTimeTaskStatus,
|
|
6
|
+
type StaffSupportRoutedVia,
|
|
7
|
+
} from '../../data/entities'
|
|
8
|
+
|
|
9
|
+
export type SupportScope = {
|
|
10
|
+
tenantId: string
|
|
11
|
+
organizationId: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type SupportRoutingRuleView = {
|
|
15
|
+
id: string
|
|
16
|
+
categoryValue: string | null
|
|
17
|
+
timeProjectId: string
|
|
18
|
+
isActive: boolean
|
|
19
|
+
updatedAt?: Date | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type SupportRoute = {
|
|
23
|
+
timeProjectId: string
|
|
24
|
+
via: StaffSupportRoutedVia
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveSupportRouteFromRules(
|
|
28
|
+
rules: SupportRoutingRuleView[],
|
|
29
|
+
categoryValue: string,
|
|
30
|
+
usableProjectIds: ReadonlySet<string>,
|
|
31
|
+
): SupportRoute | null {
|
|
32
|
+
const categoryRule = rules.find((rule) => rule.categoryValue === categoryValue)
|
|
33
|
+
if (categoryRule && categoryRule.isActive && usableProjectIds.has(categoryRule.timeProjectId)) {
|
|
34
|
+
return { timeProjectId: categoryRule.timeProjectId, via: 'category' }
|
|
35
|
+
}
|
|
36
|
+
const fallbackRule = rules.find((rule) => rule.categoryValue === null)
|
|
37
|
+
if (fallbackRule && usableProjectIds.has(fallbackRule.timeProjectId)) {
|
|
38
|
+
return { timeProjectId: fallbackRule.timeProjectId, via: 'fallback' }
|
|
39
|
+
}
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function loadSupportRoutingRules(
|
|
44
|
+
em: EntityManager,
|
|
45
|
+
scope: SupportScope,
|
|
46
|
+
): Promise<SupportRoutingRuleView[]> {
|
|
47
|
+
const rows = await em.find(StaffSupportRoutingRule, {
|
|
48
|
+
tenantId: scope.tenantId,
|
|
49
|
+
organizationId: scope.organizationId,
|
|
50
|
+
deletedAt: null,
|
|
51
|
+
})
|
|
52
|
+
return rows.map((row) => ({
|
|
53
|
+
id: row.id,
|
|
54
|
+
categoryValue: row.categoryValue ?? null,
|
|
55
|
+
timeProjectId: row.timeProjectId,
|
|
56
|
+
isActive: row.isActive,
|
|
57
|
+
updatedAt: row.updatedAt ?? null,
|
|
58
|
+
}))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function loadUsableSupportProjectIds(
|
|
62
|
+
em: EntityManager,
|
|
63
|
+
scope: SupportScope,
|
|
64
|
+
projectIds: string[],
|
|
65
|
+
): Promise<Set<string>> {
|
|
66
|
+
const unique = Array.from(new Set(projectIds))
|
|
67
|
+
if (unique.length === 0) return new Set()
|
|
68
|
+
const projects = await em.find(
|
|
69
|
+
StaffTimeProject,
|
|
70
|
+
{
|
|
71
|
+
id: { $in: unique },
|
|
72
|
+
tenantId: scope.tenantId,
|
|
73
|
+
organizationId: scope.organizationId,
|
|
74
|
+
deletedAt: null,
|
|
75
|
+
status: { $in: ['active', 'on_hold'] },
|
|
76
|
+
},
|
|
77
|
+
{ fields: ['id'] },
|
|
78
|
+
)
|
|
79
|
+
if (projects.length === 0) return new Set()
|
|
80
|
+
const defaults = await em.find(
|
|
81
|
+
StaffTimeTaskStatus,
|
|
82
|
+
{
|
|
83
|
+
timeProjectId: { $in: projects.map((project) => project.id) },
|
|
84
|
+
tenantId: scope.tenantId,
|
|
85
|
+
organizationId: scope.organizationId,
|
|
86
|
+
isDefault: true,
|
|
87
|
+
deletedAt: null,
|
|
88
|
+
},
|
|
89
|
+
{ fields: ['timeProjectId'] },
|
|
90
|
+
)
|
|
91
|
+
return new Set(defaults.map((status) => status.timeProjectId))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function resolveSupportRoute(
|
|
95
|
+
em: EntityManager,
|
|
96
|
+
scope: SupportScope,
|
|
97
|
+
categoryValue: string,
|
|
98
|
+
): Promise<SupportRoute | null> {
|
|
99
|
+
const rules = await loadSupportRoutingRules(em, scope)
|
|
100
|
+
const usable = await loadUsableSupportProjectIds(em, scope, rules.map((rule) => rule.timeProjectId))
|
|
101
|
+
return resolveSupportRouteFromRules(rules, categoryValue, usable)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function resolveRoutableCategoryValues(
|
|
105
|
+
em: EntityManager,
|
|
106
|
+
scope: SupportScope,
|
|
107
|
+
categoryValues: string[],
|
|
108
|
+
): Promise<Map<string, SupportRoute>> {
|
|
109
|
+
const rules = await loadSupportRoutingRules(em, scope)
|
|
110
|
+
const usable = await loadUsableSupportProjectIds(em, scope, rules.map((rule) => rule.timeProjectId))
|
|
111
|
+
const routes = new Map<string, SupportRoute>()
|
|
112
|
+
for (const value of categoryValues) {
|
|
113
|
+
const route = resolveSupportRouteFromRules(rules, value, usable)
|
|
114
|
+
if (route) routes.set(value, route)
|
|
115
|
+
}
|
|
116
|
+
return routes
|
|
117
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type RoutingRuleSnapshot = {
|
|
2
|
+
id: string
|
|
3
|
+
categoryValue: string | null
|
|
4
|
+
project: { id: string }
|
|
5
|
+
updatedAt?: string | null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type RoutingRuleSnapshotSet = {
|
|
9
|
+
rules: RoutingRuleSnapshot[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type RoutingRuleDialogState =
|
|
13
|
+
| { mode: 'create'; categoryValue: string; timeProjectId: string }
|
|
14
|
+
| {
|
|
15
|
+
mode: 'edit'
|
|
16
|
+
ruleId: string
|
|
17
|
+
categoryValue: string
|
|
18
|
+
timeProjectId: string
|
|
19
|
+
expectedUpdatedAt: string | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function routingRuleVersion(rule: { updatedAt?: string | null }): string | null {
|
|
23
|
+
return typeof rule.updatedAt === 'string' && rule.updatedAt.length > 0 ? rule.updatedAt : null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildRoutingRuleEditDraft(rule: RoutingRuleSnapshot): RoutingRuleDialogState {
|
|
27
|
+
return {
|
|
28
|
+
mode: 'edit',
|
|
29
|
+
ruleId: rule.id,
|
|
30
|
+
categoryValue: rule.categoryValue ?? '',
|
|
31
|
+
timeProjectId: rule.project.id,
|
|
32
|
+
expectedUpdatedAt: routingRuleVersion(rule),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function rebaseRoutingRuleDialog(
|
|
37
|
+
draft: RoutingRuleDialogState | null,
|
|
38
|
+
snapshot: RoutingRuleSnapshotSet,
|
|
39
|
+
): RoutingRuleDialogState | null {
|
|
40
|
+
if (!draft || draft.mode === 'create') return draft
|
|
41
|
+
const current = snapshot.rules.find((rule) => rule.id === draft.ruleId)
|
|
42
|
+
if (!current) return null
|
|
43
|
+
if (routingRuleVersion(current) === draft.expectedUpdatedAt) return draft
|
|
44
|
+
return buildRoutingRuleEditDraft(current)
|
|
45
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
|
|
3
|
+
import { isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'
|
|
4
|
+
import { StaffSupportRoutingRule, StaffTimeProject, StaffTimeTaskStatus } from '../../data/entities'
|
|
5
|
+
import { seedProjectTaskStatuses } from '../timesheets-tasks/seedProjectStatuses'
|
|
6
|
+
import type { TaskStatusLabelTranslate } from '../timesheets-tasks/defaultStatusTemplate'
|
|
7
|
+
import { deriveProjectCode } from '../time-tracking/projectCode'
|
|
8
|
+
import { seedSupportTicketCategories } from './dictionaries'
|
|
9
|
+
|
|
10
|
+
export const SUPPORT_FALLBACK_PROJECT_NAME = 'Customer support'
|
|
11
|
+
|
|
12
|
+
type SupportScope = {
|
|
13
|
+
tenantId: string
|
|
14
|
+
organizationId: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ProjectCodeGenerator = (name: string, taken: Set<string>) => string
|
|
18
|
+
|
|
19
|
+
export type EnsureFallbackResult = {
|
|
20
|
+
ruleId: string
|
|
21
|
+
timeProjectId: string
|
|
22
|
+
created: boolean
|
|
23
|
+
/** Board columns seeded for a freshly created project; empty when nothing was created. */
|
|
24
|
+
taskStatusIds: string[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type ProjectCodeResolverLike = { generate(name: string, taken: Set<string>): string }
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The `timeProjectCodeResolver` DI override (EP-39), so a seeded project gets the
|
|
31
|
+
* same code a project created through the UI would. `undefined` (built-in
|
|
32
|
+
* generator) when the container does not carry one.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveContainerProjectCodeGenerator(
|
|
35
|
+
container: { resolve: (name: string) => unknown } | null | undefined,
|
|
36
|
+
): ProjectCodeGenerator | undefined {
|
|
37
|
+
if (!container) return undefined
|
|
38
|
+
try {
|
|
39
|
+
const resolver = container.resolve('timeProjectCodeResolver') as ProjectCodeResolverLike | undefined
|
|
40
|
+
if (!resolver || typeof resolver.generate !== 'function') return undefined
|
|
41
|
+
return (name, taken) => resolver.generate(name, taken)
|
|
42
|
+
} catch {
|
|
43
|
+
return undefined
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function seedSupportDefaults(
|
|
48
|
+
em: EntityManager,
|
|
49
|
+
scope: SupportScope,
|
|
50
|
+
options: { translate: TaskStatusLabelTranslate; generateCode?: ProjectCodeGenerator },
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
await seedSupportTicketCategories(em, scope)
|
|
53
|
+
await ensureFallbackProjectAndRule(em, scope, {
|
|
54
|
+
translate: options.translate,
|
|
55
|
+
generateCode: options.generateCode,
|
|
56
|
+
projectName: options.translate('staff.support.fallbackProject.name', SUPPORT_FALLBACK_PROJECT_NAME),
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function findFallbackRule(em: EntityManager, scope: SupportScope): Promise<StaffSupportRoutingRule | null> {
|
|
61
|
+
return em.findOne(StaffSupportRoutingRule, {
|
|
62
|
+
tenantId: scope.tenantId,
|
|
63
|
+
organizationId: scope.organizationId,
|
|
64
|
+
categoryValue: null,
|
|
65
|
+
deletedAt: null,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Idempotent: an existing fallback rule is returned untouched, and a call that
|
|
71
|
+
* loses a race to a concurrent one (unique index on the fallback rule or on the
|
|
72
|
+
* project code) re-reads the winner's rule and reports `created: false`.
|
|
73
|
+
*/
|
|
74
|
+
export async function ensureFallbackProjectAndRule(
|
|
75
|
+
em: EntityManager,
|
|
76
|
+
scope: SupportScope,
|
|
77
|
+
options: { translate: TaskStatusLabelTranslate; generateCode?: ProjectCodeGenerator; projectName?: string },
|
|
78
|
+
): Promise<EnsureFallbackResult> {
|
|
79
|
+
const existingRule = await findFallbackRule(em, scope)
|
|
80
|
+
if (existingRule) {
|
|
81
|
+
return { ruleId: existingRule.id, timeProjectId: existingRule.timeProjectId, created: false, taskStatusIds: [] }
|
|
82
|
+
}
|
|
83
|
+
const takenRows = await em.find(
|
|
84
|
+
StaffTimeProject,
|
|
85
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },
|
|
86
|
+
{ fields: ['code'] },
|
|
87
|
+
)
|
|
88
|
+
const taken = new Set(takenRows.map((row) => row.code))
|
|
89
|
+
const name = options.projectName ?? SUPPORT_FALLBACK_PROJECT_NAME
|
|
90
|
+
const generate = options.generateCode ?? ((projectName: string, codes: Set<string>) => deriveProjectCode(projectName, codes))
|
|
91
|
+
const now = new Date()
|
|
92
|
+
const project = em.create(StaffTimeProject, {
|
|
93
|
+
tenantId: scope.tenantId,
|
|
94
|
+
organizationId: scope.organizationId,
|
|
95
|
+
name,
|
|
96
|
+
code: generate(name, taken),
|
|
97
|
+
customerId: null,
|
|
98
|
+
customerSnapshot: null,
|
|
99
|
+
projectType: 'internal',
|
|
100
|
+
status: 'active',
|
|
101
|
+
billableByDefault: false,
|
|
102
|
+
budgetKind: 'none',
|
|
103
|
+
createdAt: now,
|
|
104
|
+
updatedAt: now,
|
|
105
|
+
deletedAt: null,
|
|
106
|
+
})
|
|
107
|
+
let rule: StaffSupportRoutingRule | null = null
|
|
108
|
+
let statuses: StaffTimeTaskStatus[] = []
|
|
109
|
+
try {
|
|
110
|
+
await withAtomicFlush(
|
|
111
|
+
em,
|
|
112
|
+
[
|
|
113
|
+
() => {
|
|
114
|
+
em.persist(project)
|
|
115
|
+
},
|
|
116
|
+
() => {
|
|
117
|
+
statuses = seedProjectTaskStatuses(
|
|
118
|
+
em,
|
|
119
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId, timeProjectId: project.id },
|
|
120
|
+
options.translate,
|
|
121
|
+
)
|
|
122
|
+
rule = em.create(StaffSupportRoutingRule, {
|
|
123
|
+
tenantId: scope.tenantId,
|
|
124
|
+
organizationId: scope.organizationId,
|
|
125
|
+
categoryValue: null,
|
|
126
|
+
timeProjectId: project.id,
|
|
127
|
+
isActive: true,
|
|
128
|
+
createdAt: now,
|
|
129
|
+
updatedAt: now,
|
|
130
|
+
deletedAt: null,
|
|
131
|
+
})
|
|
132
|
+
em.persist(rule)
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
{ transaction: true, label: 'staff.support.ensure_fallback_project' },
|
|
136
|
+
)
|
|
137
|
+
} catch (err) {
|
|
138
|
+
if (!isUniqueViolation(err)) throw err
|
|
139
|
+
const winner = await findFallbackRule(em.fork(), scope)
|
|
140
|
+
if (!winner) throw err
|
|
141
|
+
return { ruleId: winner.id, timeProjectId: winner.timeProjectId, created: false, taskStatusIds: [] }
|
|
142
|
+
}
|
|
143
|
+
const created = rule as StaffSupportRoutingRule | null
|
|
144
|
+
if (!created) throw new Error('[internal] fallback routing rule was not created')
|
|
145
|
+
return {
|
|
146
|
+
ruleId: created.id,
|
|
147
|
+
timeProjectId: project.id,
|
|
148
|
+
created: true,
|
|
149
|
+
taskStatusIds: statuses.map((status) => status.id),
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
2
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
3
|
+
import type { StaffSupportTicket, StaffTimeTask } from '../../data/entities'
|
|
4
|
+
import { emitStaffEvent } from '../../events'
|
|
5
|
+
|
|
6
|
+
const logger = createLogger('staff').child({ component: 'lib/support/supportActivity' })
|
|
7
|
+
|
|
8
|
+
export type SupportAttachmentActivityKind = 'attachment_added' | 'attachment_removed'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Tells open backoffice screens that a customer changed the files on a ticket.
|
|
12
|
+
* Ids only, like every `activity_recorded` broadcast. The attachment id keeps
|
|
13
|
+
* two quick changes of the same kind distinct for the browser bridge's 500 ms
|
|
14
|
+
* duplicate filter. A failed publish is reported and never fails the upload or
|
|
15
|
+
* deletion that already happened.
|
|
16
|
+
*/
|
|
17
|
+
export function emitSupportAttachmentActivity(
|
|
18
|
+
task: Pick<StaffTimeTask, 'id' | 'tenantId' | 'organizationId' | 'timeProjectId'>,
|
|
19
|
+
ticket: Pick<StaffSupportTicket, 'id'>,
|
|
20
|
+
kind: SupportAttachmentActivityKind,
|
|
21
|
+
attachmentId: string,
|
|
22
|
+
): void {
|
|
23
|
+
void emitStaffEvent(
|
|
24
|
+
'staff.support_ticket.activity_recorded',
|
|
25
|
+
{
|
|
26
|
+
id: ticket.id,
|
|
27
|
+
tenantId: task.tenantId,
|
|
28
|
+
organizationId: task.organizationId,
|
|
29
|
+
taskId: task.id,
|
|
30
|
+
projectId: task.timeProjectId,
|
|
31
|
+
kind,
|
|
32
|
+
attachmentId,
|
|
33
|
+
},
|
|
34
|
+
{ persistent: true },
|
|
35
|
+
).catch((err) => {
|
|
36
|
+
logger.error('staff support attachment activity emit failed', { err, taskId: task.id })
|
|
37
|
+
getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.support_activity_emit_failed' })
|
|
38
|
+
})
|
|
39
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
|
+
import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
|
|
6
|
+
|
|
7
|
+
export function PortalSupportNotLinked({ size = 'lg' }: { size?: 'sm' | 'lg' }) {
|
|
8
|
+
const t = useT()
|
|
9
|
+
return (
|
|
10
|
+
<EmptyState
|
|
11
|
+
variant="subtle"
|
|
12
|
+
size={size}
|
|
13
|
+
title={t('staff.portal.support.notLinked.title', 'Support tickets are not available for your account')}
|
|
14
|
+
description={t(
|
|
15
|
+
'staff.portal.support.notLinked.description',
|
|
16
|
+
'Your portal account is not linked to a company, so there are no tickets to show. Ask your account administrator to link it.',
|
|
17
|
+
)}
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
20
|
+
}
|