@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,172 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
3
|
+
import { StaffTeamMember, StaffTimeProjectMember } from '../../data/entities'
|
|
4
|
+
import { DEFAULT_ASSIGNMENT_GRACE_DAYS, MAX_ASSIGNMENT_GRACE_DAYS } from './settingKeys'
|
|
5
|
+
|
|
6
|
+
const MS_PER_DAY = 86_400_000
|
|
7
|
+
const ISO_DATE_PREFIX = /^(\d{4})-(\d{2})-(\d{2})/
|
|
8
|
+
|
|
9
|
+
export function normalizeAssignmentGraceDays(value: unknown): number {
|
|
10
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) return DEFAULT_ASSIGNMENT_GRACE_DAYS
|
|
11
|
+
if (value < 0 || value > MAX_ASSIGNMENT_GRACE_DAYS) return DEFAULT_ASSIGNMENT_GRACE_DAYS
|
|
12
|
+
return value
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `assigned_start_date` / `assigned_end_date` are `date` columns, so both bounds
|
|
17
|
+
* are compared as whole days rather than timestamps. Date instances are read
|
|
18
|
+
* through their local calendar fields because the postgres driver materialises a
|
|
19
|
+
* `date` as local midnight, and "today" likewise means the server's local day.
|
|
20
|
+
*/
|
|
21
|
+
export function toDayIndex(value: unknown): number | null {
|
|
22
|
+
if (value == null) return null
|
|
23
|
+
if (value instanceof Date) {
|
|
24
|
+
if (Number.isNaN(value.getTime())) return null
|
|
25
|
+
return Date.UTC(value.getFullYear(), value.getMonth(), value.getDate()) / MS_PER_DAY
|
|
26
|
+
}
|
|
27
|
+
if (typeof value === 'string') {
|
|
28
|
+
const match = ISO_DATE_PREFIX.exec(value.trim())
|
|
29
|
+
if (!match) return null
|
|
30
|
+
return Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])) / MS_PER_DAY
|
|
31
|
+
}
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* An entry date is a calendar day the client sent as `YYYY-MM-DD`, which
|
|
37
|
+
* `z.coerce.date()` turns into UTC midnight — so a Date is read through its UTC
|
|
38
|
+
* fields here, the same way the grid addresses a cell (D-8).
|
|
39
|
+
*/
|
|
40
|
+
export function toEntryDayIndex(value: unknown): number | null {
|
|
41
|
+
if (value instanceof Date) {
|
|
42
|
+
if (Number.isNaN(value.getTime())) return null
|
|
43
|
+
return Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()) / MS_PER_DAY
|
|
44
|
+
}
|
|
45
|
+
return toDayIndex(value)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A membership only grants access inside its assignment window: from
|
|
50
|
+
* `assigned_start_date` until `assigned_end_date` plus the tenant's grace days.
|
|
51
|
+
* A null end date is open-ended and never expires.
|
|
52
|
+
*/
|
|
53
|
+
export function isWithinAssignmentWindow(
|
|
54
|
+
membership: Pick<StaffTimeProjectMember, 'assignedStartDate' | 'assignedEndDate'>,
|
|
55
|
+
todayIndex: number,
|
|
56
|
+
graceDays: number,
|
|
57
|
+
): boolean {
|
|
58
|
+
if (membership.assignedStartDate != null) {
|
|
59
|
+
const startIndex = toDayIndex(membership.assignedStartDate)
|
|
60
|
+
if (startIndex === null || todayIndex < startIndex) return false
|
|
61
|
+
}
|
|
62
|
+
if (membership.assignedEndDate == null) return true
|
|
63
|
+
const endIndex = toDayIndex(membership.assignedEndDate)
|
|
64
|
+
if (endIndex === null) return false
|
|
65
|
+
return todayIndex <= endIndex + graceDays
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
export type ProjectAssignmentScope = {
|
|
70
|
+
tenantId: string
|
|
71
|
+
organizationId: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type ActiveProjectMember = {
|
|
75
|
+
membershipId: string
|
|
76
|
+
staffMemberId: string
|
|
77
|
+
userId: string | null
|
|
78
|
+
isAssigner: boolean
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type MembershipLike = Pick<
|
|
82
|
+
StaffTimeProjectMember,
|
|
83
|
+
'status' | 'deletedAt' | 'assignedStartDate' | 'assignedEndDate'
|
|
84
|
+
>
|
|
85
|
+
|
|
86
|
+
export function isActiveAssignment(membership: MembershipLike, now: Date, graceDays: number): boolean {
|
|
87
|
+
if (membership.status !== 'active' || membership.deletedAt) return false
|
|
88
|
+
const todayIndex = toDayIndex(now)
|
|
89
|
+
if (todayIndex === null) return false
|
|
90
|
+
return isWithinAssignmentWindow(membership, todayIndex, normalizeAssignmentGraceDays(graceDays))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function loadActiveProjectMembers(
|
|
94
|
+
em: EntityManager,
|
|
95
|
+
scope: ProjectAssignmentScope,
|
|
96
|
+
timeProjectId: string,
|
|
97
|
+
options: { graceDays?: number | null; now?: Date; assignersOnly?: boolean } = {},
|
|
98
|
+
): Promise<ActiveProjectMember[]> {
|
|
99
|
+
const where: Record<string, unknown> = {
|
|
100
|
+
timeProjectId,
|
|
101
|
+
tenantId: scope.tenantId,
|
|
102
|
+
organizationId: scope.organizationId,
|
|
103
|
+
status: 'active',
|
|
104
|
+
deletedAt: null,
|
|
105
|
+
}
|
|
106
|
+
if (options.assignersOnly) where.isAssigner = true
|
|
107
|
+
const memberships = await em.find(StaffTimeProjectMember, where)
|
|
108
|
+
const now = options.now ?? new Date()
|
|
109
|
+
const graceDays = normalizeAssignmentGraceDays(options.graceDays)
|
|
110
|
+
const active = memberships.filter((membership) => isActiveAssignment(membership, now, graceDays))
|
|
111
|
+
if (active.length === 0) return []
|
|
112
|
+
const staffMembers = await findWithDecryption(
|
|
113
|
+
em,
|
|
114
|
+
StaffTeamMember,
|
|
115
|
+
{
|
|
116
|
+
id: { $in: Array.from(new Set(active.map((membership) => membership.staffMemberId))) },
|
|
117
|
+
tenantId: scope.tenantId,
|
|
118
|
+
organizationId: scope.organizationId,
|
|
119
|
+
deletedAt: null,
|
|
120
|
+
},
|
|
121
|
+
{},
|
|
122
|
+
scope,
|
|
123
|
+
)
|
|
124
|
+
const userByMember = new Map(staffMembers.map((member) => [member.id, member.userId ?? null]))
|
|
125
|
+
return active
|
|
126
|
+
.filter((membership) => userByMember.has(membership.staffMemberId))
|
|
127
|
+
.map((membership) => ({
|
|
128
|
+
membershipId: membership.id,
|
|
129
|
+
staffMemberId: membership.staffMemberId,
|
|
130
|
+
userId: userByMember.get(membership.staffMemberId) ?? null,
|
|
131
|
+
isAssigner: membership.isAssigner === true,
|
|
132
|
+
}))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function resolveUsableDefaultAssignee(
|
|
136
|
+
em: EntityManager,
|
|
137
|
+
scope: ProjectAssignmentScope,
|
|
138
|
+
project: { id: string; defaultAssigneeStaffMemberId?: string | null },
|
|
139
|
+
options: { graceDays?: number | null; now?: Date } = {},
|
|
140
|
+
): Promise<string | null> {
|
|
141
|
+
const candidate = project.defaultAssigneeStaffMemberId ?? null
|
|
142
|
+
if (!candidate) return null
|
|
143
|
+
const members = await loadActiveProjectMembers(em, scope, project.id, options)
|
|
144
|
+
return members.some((member) => member.staffMemberId === candidate) ? candidate : null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function isActiveProjectAssigner(
|
|
148
|
+
em: EntityManager,
|
|
149
|
+
scope: ProjectAssignmentScope,
|
|
150
|
+
timeProjectId: string,
|
|
151
|
+
staffMemberId: string,
|
|
152
|
+
options: { graceDays?: number | null; now?: Date } = {},
|
|
153
|
+
): Promise<boolean> {
|
|
154
|
+
const assigners = await loadActiveProjectMembers(em, scope, timeProjectId, { ...options, assignersOnly: true })
|
|
155
|
+
return assigners.some((member) => member.staffMemberId === staffMemberId)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
type ConfigContainer = { resolve: (name: string) => unknown }
|
|
159
|
+
|
|
160
|
+
export async function resolveAssignmentGraceDaysFromContainer(
|
|
161
|
+
container: ConfigContainer,
|
|
162
|
+
tenantId: string,
|
|
163
|
+
): Promise<number | null> {
|
|
164
|
+
try {
|
|
165
|
+
const { readTimeTrackingSettings } = await import('./settings')
|
|
166
|
+
const configService = container.resolve('moduleConfigService') as Parameters<typeof readTimeTrackingSettings>[0]
|
|
167
|
+
const settings = await readTimeTrackingSettings(configService, { tenantId })
|
|
168
|
+
return settings.access.assignmentGraceDays
|
|
169
|
+
} catch {
|
|
170
|
+
return null
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -26,6 +26,9 @@ export const TIME_TRACKING_DEFAULTS_GROUP = 'defaults'
|
|
|
26
26
|
export const TIME_TRACKING_TARGETS_GROUP = 'targets'
|
|
27
27
|
export const TIME_TRACKING_WARNINGS_GROUP = 'warnings'
|
|
28
28
|
export const TIME_TRACKING_ACCESS_GROUP = 'access'
|
|
29
|
+
export const TIME_TRACKING_SUPPORT_GROUP = 'support'
|
|
30
|
+
export const DEFAULT_SUPPORT_REFERENCE_PREFIX = 'SUP'
|
|
31
|
+
export const supportReferencePrefixSchema = z.string().regex(/^[A-Z][A-Z0-9]{1,9}$/)
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
34
|
* Days past `assigned_end_date` that project access survives (spec D-12). The
|
|
@@ -198,6 +201,14 @@ const BUILT_IN_SETTING_KEYS: readonly TimeTrackingSettingKeyInput[] = [
|
|
|
198
201
|
labelKey: 'staff.time_tracking.settings.access.graceDays',
|
|
199
202
|
priority: BUILT_IN_STRATEGY_PRIORITY,
|
|
200
203
|
},
|
|
204
|
+
{
|
|
205
|
+
group: TIME_TRACKING_SUPPORT_GROUP,
|
|
206
|
+
key: 'referencePrefix',
|
|
207
|
+
schema: supportReferencePrefixSchema,
|
|
208
|
+
default: DEFAULT_SUPPORT_REFERENCE_PREFIX,
|
|
209
|
+
labelKey: 'staff.support.settings.referencePrefix',
|
|
210
|
+
priority: BUILT_IN_STRATEGY_PRIORITY,
|
|
211
|
+
},
|
|
201
212
|
]
|
|
202
213
|
|
|
203
214
|
for (const input of BUILT_IN_SETTING_KEYS) {
|
|
@@ -8,10 +8,12 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_ASSIGNMENT_GRACE_DAYS,
|
|
10
10
|
MAX_ASSIGNMENT_GRACE_DAYS,
|
|
11
|
+
TIME_TRACKING_SUPPORT_GROUP,
|
|
11
12
|
buildDefaultTimeTrackingSettings,
|
|
12
13
|
listTimeTrackingSettingKeys,
|
|
13
14
|
normalizeTimeTrackingSettingsRecord,
|
|
14
15
|
registerTimeTrackingSettingKey,
|
|
16
|
+
supportReferencePrefixSchema,
|
|
15
17
|
timeTrackingSettingKeyIds,
|
|
16
18
|
type TimeTrackingSettingKeyDefinition,
|
|
17
19
|
type TimeTrackingSettingKeyInput,
|
|
@@ -32,9 +34,10 @@ export const TIME_TRACKING_TARGETS_DAILY_HOURS_KEY = 'targets.dailyHours'
|
|
|
32
34
|
export const TIME_TRACKING_WARNINGS_OVERLAP_KEY = 'warnings.overlap'
|
|
33
35
|
export const TIME_TRACKING_WARNINGS_RUNNING_TIMER_KEY = 'warnings.runningTimer'
|
|
34
36
|
export const TIME_TRACKING_ACCESS_ASSIGNMENT_GRACE_DAYS_KEY = 'access.assignmentGraceDays'
|
|
37
|
+
export const TIME_TRACKING_SUPPORT_REFERENCE_PREFIX_KEY = 'support.referencePrefix'
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
|
-
* The
|
|
40
|
+
* The built-in keys the module ships. FROZEN — a stored config row is named after one
|
|
38
41
|
* of these, so none may be renamed or removed. The list is no longer the whole truth:
|
|
39
42
|
* `timeTrackingSettingKeyIds()` returns these plus every contributed key (EP-42).
|
|
40
43
|
*/
|
|
@@ -47,6 +50,7 @@ export const TIME_TRACKING_SETTING_KEYS = [
|
|
|
47
50
|
TIME_TRACKING_WARNINGS_OVERLAP_KEY,
|
|
48
51
|
TIME_TRACKING_WARNINGS_RUNNING_TIMER_KEY,
|
|
49
52
|
TIME_TRACKING_ACCESS_ASSIGNMENT_GRACE_DAYS_KEY,
|
|
53
|
+
TIME_TRACKING_SUPPORT_REFERENCE_PREFIX_KEY,
|
|
50
54
|
] as const
|
|
51
55
|
|
|
52
56
|
export type TimeTrackingSettingKey = (typeof TIME_TRACKING_SETTING_KEYS)[number]
|
|
@@ -69,12 +73,21 @@ export type TimeTrackingAccess = {
|
|
|
69
73
|
assignmentGraceDays: number
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
export type TimeTrackingSupport = {
|
|
77
|
+
referencePrefix: string
|
|
78
|
+
}
|
|
79
|
+
|
|
72
80
|
export type TimeTrackingBuiltInSettings = {
|
|
73
81
|
rounding: RoundingSettings
|
|
74
82
|
defaults: TimeTrackingEntryDefaults
|
|
75
83
|
targets: TimeTrackingTargets
|
|
76
84
|
warnings: TimeTrackingWarnings
|
|
77
85
|
access: TimeTrackingAccess
|
|
86
|
+
/**
|
|
87
|
+
* Optional in the type so code that builds a settings object without the support
|
|
88
|
+
* group keeps compiling; a normalized read always carries it.
|
|
89
|
+
*/
|
|
90
|
+
support?: TimeTrackingSupport
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
/**
|
|
@@ -166,3 +179,35 @@ export async function writeTimeTrackingSettings(
|
|
|
166
179
|
|
|
167
180
|
return settings
|
|
168
181
|
}
|
|
182
|
+
|
|
183
|
+
function readSettingsGroup(input: Record<string, unknown>, group: string): Record<string, unknown> | null {
|
|
184
|
+
const value = input[group]
|
|
185
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
|
|
186
|
+
return value as Record<string, unknown>
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* A settings payload from a client that predates the support group omits it. That
|
|
191
|
+
* omission keeps the tenant's stored reference prefix instead of resetting it to the
|
|
192
|
+
* default, so an older settings screen cannot silently rename future ticket references.
|
|
193
|
+
*/
|
|
194
|
+
export async function preserveOmittedSupportSettings(
|
|
195
|
+
configService: ModuleConfigService,
|
|
196
|
+
scope: TimeTrackingSettingsScope,
|
|
197
|
+
input: Record<string, unknown>,
|
|
198
|
+
): Promise<Record<string, unknown>> {
|
|
199
|
+
const supportGroup = readSettingsGroup(input, TIME_TRACKING_SUPPORT_GROUP)
|
|
200
|
+
if (supportGroup && supportGroup.referencePrefix !== undefined) return input
|
|
201
|
+
const record = await configService.getRecord(
|
|
202
|
+
STAFF_TIME_TRACKING_MODULE_ID,
|
|
203
|
+
TIME_TRACKING_SUPPORT_REFERENCE_PREFIX_KEY,
|
|
204
|
+
{ tenantId: scope.tenantId },
|
|
205
|
+
)
|
|
206
|
+
if (!record) return input
|
|
207
|
+
const storedPrefix = supportReferencePrefixSchema.safeParse(record.value)
|
|
208
|
+
if (!storedPrefix.success) return input
|
|
209
|
+
return {
|
|
210
|
+
...input,
|
|
211
|
+
[TIME_TRACKING_SUPPORT_GROUP]: { ...(supportGroup ?? {}), referencePrefix: storedPrefix.data },
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
|
+
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
6
|
+
import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
7
|
+
import { CrudHttpError, forbidden, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
8
|
+
import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
|
|
9
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
10
|
+
import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
|
|
11
|
+
import type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
12
|
+
import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
|
|
13
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
14
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
15
|
+
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
16
|
+
import { StaffTimeTask } from '../../data/entities'
|
|
17
|
+
import { STAFF_TIME_TRACKING_RESOURCE_KINDS } from '../../api/guards'
|
|
18
|
+
import {
|
|
19
|
+
staffTimeTaskAssignmentCommandIds,
|
|
20
|
+
type StaffTimeTaskAssignmentInput,
|
|
21
|
+
type StaffTimeTaskAssignmentResult,
|
|
22
|
+
} from '../../commands/timesheets-task-assignment'
|
|
23
|
+
import { resolveFeatureAccess } from './featureAccess'
|
|
24
|
+
import { assertProjectAccess } from './access'
|
|
25
|
+
import { resolveListProjectAccess } from './listProjectAccess'
|
|
26
|
+
|
|
27
|
+
export type TaskAssignmentAction = keyof typeof staffTimeTaskAssignmentCommandIds
|
|
28
|
+
|
|
29
|
+
export const TASK_ASSIGNMENT_VIEW_FEATURE = 'staff.timesheets.tasks.view'
|
|
30
|
+
|
|
31
|
+
const FAILURE_CODES: Record<TaskAssignmentAction, string> = {
|
|
32
|
+
claim: 'staff.task_claim_failed',
|
|
33
|
+
release: 'staff.task_release_failed',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type TaskAssignmentRouteContext = {
|
|
37
|
+
params?: Promise<Record<string, string | string[]>> | Record<string, string | string[]>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const taskAssignmentResponseSchema = z.object({
|
|
41
|
+
id: z.string().uuid(),
|
|
42
|
+
timeProjectId: z.string().uuid(),
|
|
43
|
+
assigneeStaffMemberId: z.string().uuid().nullable(),
|
|
44
|
+
updatedAt: z.string().nullable(),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
async function resolveTaskId(req: Request, action: TaskAssignmentAction, context?: TaskAssignmentRouteContext): Promise<string | null> {
|
|
48
|
+
const params = context?.params ? await context.params : null
|
|
49
|
+
const fromParams = params?.id
|
|
50
|
+
if (typeof fromParams === 'string' && fromParams.length > 0) return fromParams
|
|
51
|
+
try {
|
|
52
|
+
const match = new URL(req.url).pathname.match(new RegExp(`/tasks/([^/]+)/${action}`))
|
|
53
|
+
return match?.[1] ?? null
|
|
54
|
+
} catch {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The `POST /api/staff/timesheets/tasks/{id}/claim|release` handler. Both actions
|
|
61
|
+
* share every step — scope, project access (the tasks list rule), optimistic lock,
|
|
62
|
+
* mutation guards, the command and the undo header — and differ only in the command.
|
|
63
|
+
*/
|
|
64
|
+
export function createTaskAssignmentPostHandler(action: TaskAssignmentAction) {
|
|
65
|
+
const logger = createLogger('staff').child({ component: `api/timesheets/tasks/${action}` })
|
|
66
|
+
return async function POST(req: Request, context?: TaskAssignmentRouteContext): Promise<Response> {
|
|
67
|
+
try {
|
|
68
|
+
const container = await createRequestContainer()
|
|
69
|
+
const auth = await getAuthFromRequest(req)
|
|
70
|
+
const { translate } = await resolveTranslations()
|
|
71
|
+
if (!auth) throw new CrudHttpError(401, { error: translate('staff.errors.unauthorized', 'Unauthorized') })
|
|
72
|
+
|
|
73
|
+
const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
|
|
74
|
+
const tenantId = scope?.tenantId ?? auth.tenantId ?? null
|
|
75
|
+
const organizationId = scope?.selectedId ?? auth.orgId ?? null
|
|
76
|
+
if (!tenantId || !organizationId) {
|
|
77
|
+
throw new CrudHttpError(400, { error: translate('staff.errors.missingScope', 'Missing tenant or organization scope.') })
|
|
78
|
+
}
|
|
79
|
+
const taskId = await resolveTaskId(req, action, context)
|
|
80
|
+
if (!taskId || !z.string().uuid().safeParse(taskId).success) {
|
|
81
|
+
throw new CrudHttpError(400, { error: translate('staff.time_tracking.tasks.errors.idRequired', 'Task id is required.') })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const actorId = auth.sub ?? ''
|
|
85
|
+
const viewAccess = await resolveFeatureAccess(container, actorId, [TASK_ASSIGNMENT_VIEW_FEATURE], { tenantId, organizationId })
|
|
86
|
+
if (!viewAccess.allowed) throw forbidden(translate('staff.errors.forbidden', 'Forbidden'))
|
|
87
|
+
|
|
88
|
+
const routeCtx: CommandRuntimeContext = {
|
|
89
|
+
container,
|
|
90
|
+
auth,
|
|
91
|
+
organizationScope: scope,
|
|
92
|
+
selectedOrganizationId: organizationId,
|
|
93
|
+
organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),
|
|
94
|
+
request: req,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const em = (container.resolve('em') as EntityManager).fork()
|
|
98
|
+
const task = await em.findOne(StaffTimeTask, { id: taskId, tenantId, organizationId, deletedAt: null })
|
|
99
|
+
const access = task ? await resolveListProjectAccess(routeCtx) : null
|
|
100
|
+
if (!task || !access || !assertProjectAccess(access, task.timeProjectId)) {
|
|
101
|
+
throw new CrudHttpError(404, {
|
|
102
|
+
error: translate('staff.time_tracking.tasks.errors.notFound', 'Task not found or not accessible.'),
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
107
|
+
resourceKind: STAFF_TIME_TRACKING_RESOURCE_KINDS.timeTask,
|
|
108
|
+
resourceId: task.id,
|
|
109
|
+
current: task.updatedAt,
|
|
110
|
+
request: req,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const input: StaffTimeTaskAssignmentInput = { id: taskId }
|
|
114
|
+
const guardResult = await runRouteMutationGuards({
|
|
115
|
+
container,
|
|
116
|
+
req,
|
|
117
|
+
auth: { userId: actorId, tenantId, organizationId, userFeatures: viewAccess.grantedFeatures },
|
|
118
|
+
input: {
|
|
119
|
+
resourceKind: STAFF_TIME_TRACKING_RESOURCE_KINDS.timeTask,
|
|
120
|
+
resourceId: taskId,
|
|
121
|
+
operation: 'update',
|
|
122
|
+
mutationPayload: input,
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
if (!guardResult.ok) return guardResult.response
|
|
126
|
+
|
|
127
|
+
const commandBus = container.resolve('commandBus') as CommandBus
|
|
128
|
+
const { result, logEntry } = await commandBus.execute<StaffTimeTaskAssignmentInput, StaffTimeTaskAssignmentResult>(
|
|
129
|
+
staffTimeTaskAssignmentCommandIds[action],
|
|
130
|
+
{ input, ctx: routeCtx },
|
|
131
|
+
)
|
|
132
|
+
await guardResult.runAfterSuccess()
|
|
133
|
+
|
|
134
|
+
const response = NextResponse.json({
|
|
135
|
+
id: result?.taskId ?? taskId,
|
|
136
|
+
timeProjectId: result?.timeProjectId ?? task.timeProjectId,
|
|
137
|
+
assigneeStaffMemberId: result?.assigneeStaffMemberId ?? null,
|
|
138
|
+
updatedAt: result?.updatedAt ?? null,
|
|
139
|
+
})
|
|
140
|
+
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
141
|
+
response.headers.set(
|
|
142
|
+
'x-om-operation',
|
|
143
|
+
serializeOperationMetadata({
|
|
144
|
+
id: logEntry.id,
|
|
145
|
+
undoToken: logEntry.undoToken,
|
|
146
|
+
commandId: logEntry.commandId,
|
|
147
|
+
actionLabel: logEntry.actionLabel ?? null,
|
|
148
|
+
resourceKind: logEntry.resourceKind ?? STAFF_TIME_TRACKING_RESOURCE_KINDS.timeTask,
|
|
149
|
+
resourceId: logEntry.resourceId ?? taskId,
|
|
150
|
+
executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,
|
|
151
|
+
}),
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
return response
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })
|
|
157
|
+
const interceptorRejection = getCommandInterceptorHttpRejection(err)
|
|
158
|
+
if (interceptorRejection) return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })
|
|
159
|
+
const { translate } = await resolveTranslations()
|
|
160
|
+
logger.error(`staff.timesheets.tasks.${action} POST failed`, { err })
|
|
161
|
+
getTelemetryRuntime()?.reportError(err, { module: 'staff', code: FAILURE_CODES[action] })
|
|
162
|
+
return NextResponse.json({ error: translate('staff.errors.internal', 'Internal server error') }, { status: 500 })
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import type { NotificationTypeDefinition } from '@open-mercato/shared/modules/notifications/types'
|
|
3
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
4
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
5
|
+
import { resolveNotificationService } from '../../../notifications/lib/notificationService'
|
|
6
|
+
import {
|
|
7
|
+
buildBatchNotificationFromType,
|
|
8
|
+
buildFeatureNotificationFromType,
|
|
9
|
+
} from '../../../notifications/lib/notificationBuilder'
|
|
10
|
+
import { StaffTimeProject } from '../../data/entities'
|
|
11
|
+
import { loadActiveProjectMembers, type ProjectAssignmentScope } from './projectAssignment'
|
|
12
|
+
|
|
13
|
+
const logger = createLogger('staff').child({ component: 'lib/time-tracking/workRecipients' })
|
|
14
|
+
|
|
15
|
+
export const PROJECT_MANAGERS_FEATURE = 'staff.timesheets.projects.manage'
|
|
16
|
+
|
|
17
|
+
export type WorkRecipientStep = 1 | 2 | 3 | 4
|
|
18
|
+
|
|
19
|
+
export type ResolvedWorkRecipients =
|
|
20
|
+
| { step: 1 | 2 | 3; userIds: string[] }
|
|
21
|
+
| { step: 4; userIds: [] }
|
|
22
|
+
|
|
23
|
+
export type WorkRecipientsInput = {
|
|
24
|
+
timeProjectId: string
|
|
25
|
+
/** Step 1: the type's own audience. Empty or omitted starts the chain at step 2. */
|
|
26
|
+
audienceUserIds?: readonly (string | null | undefined)[]
|
|
27
|
+
/** Excluded from steps 1–3. Step 4 hands resolution to `createForFeature`, which cannot exclude a user. */
|
|
28
|
+
actorUserId?: string | null
|
|
29
|
+
graceDays?: number | null
|
|
30
|
+
now?: Date
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function uniqueExcluding(ids: readonly (string | null | undefined)[], actorUserId: string | null): string[] {
|
|
34
|
+
const seen = new Set<string>()
|
|
35
|
+
for (const id of ids) {
|
|
36
|
+
if (typeof id !== 'string' || id.length === 0) continue
|
|
37
|
+
if (actorUserId && id === actorUserId) continue
|
|
38
|
+
seen.add(id)
|
|
39
|
+
}
|
|
40
|
+
return [...seen]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The escalation chain shared by every project-work notification: the type's own
|
|
45
|
+
* audience, then the project's active assigners, then the project owner, then the
|
|
46
|
+
* organization's project managers. Each step is used only when the one before it
|
|
47
|
+
* is empty. Steps 1–3 are resolved here; step 4 is returned as a marker for
|
|
48
|
+
* `notifyProjectWork`, which sends it through `createForFeature`.
|
|
49
|
+
*/
|
|
50
|
+
export async function resolveProjectWorkRecipients(
|
|
51
|
+
em: EntityManager,
|
|
52
|
+
scope: ProjectAssignmentScope,
|
|
53
|
+
input: WorkRecipientsInput,
|
|
54
|
+
): Promise<ResolvedWorkRecipients> {
|
|
55
|
+
const actorUserId = input.actorUserId ?? null
|
|
56
|
+
const audience = uniqueExcluding(input.audienceUserIds ?? [], actorUserId)
|
|
57
|
+
if (audience.length > 0) return { step: 1, userIds: audience }
|
|
58
|
+
|
|
59
|
+
const assigners = await loadActiveProjectMembers(em, scope, input.timeProjectId, {
|
|
60
|
+
graceDays: input.graceDays,
|
|
61
|
+
now: input.now,
|
|
62
|
+
assignersOnly: true,
|
|
63
|
+
})
|
|
64
|
+
const assignerUserIds = uniqueExcluding(assigners.map((member) => member.userId), actorUserId)
|
|
65
|
+
if (assignerUserIds.length > 0) return { step: 2, userIds: assignerUserIds }
|
|
66
|
+
|
|
67
|
+
const project = await em.findOne(
|
|
68
|
+
StaffTimeProject,
|
|
69
|
+
{ id: input.timeProjectId, tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
70
|
+
{ fields: ['id', 'ownerUserId'] },
|
|
71
|
+
)
|
|
72
|
+
const owner = uniqueExcluding([project?.ownerUserId], actorUserId)
|
|
73
|
+
if (owner.length > 0) return { step: 3, userIds: owner }
|
|
74
|
+
|
|
75
|
+
return { step: 4, userIds: [] }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type ActiveAssignerUserIdsInput = {
|
|
79
|
+
timeProjectId: string
|
|
80
|
+
graceDays?: number | null
|
|
81
|
+
now?: Date
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function loadActiveAssignerUserIds(
|
|
85
|
+
em: EntityManager,
|
|
86
|
+
scope: ProjectAssignmentScope,
|
|
87
|
+
input: ActiveAssignerUserIdsInput,
|
|
88
|
+
): Promise<string[]> {
|
|
89
|
+
const assigners = await loadActiveProjectMembers(em, scope, input.timeProjectId, {
|
|
90
|
+
graceDays: input.graceDays,
|
|
91
|
+
now: input.now,
|
|
92
|
+
assignersOnly: true,
|
|
93
|
+
})
|
|
94
|
+
return uniqueExcluding(assigners.map((member) => member.userId), null)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type ProjectRecipientProbe = {
|
|
98
|
+
id: string
|
|
99
|
+
ownerUserId?: string | null
|
|
100
|
+
defaultAssigneeStaffMemberId?: string | null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Whether new project work reaches somebody before the chain falls through to the
|
|
105
|
+
* organization's project managers (step 4): an active assigner with a user account,
|
|
106
|
+
* a usable default assignee with a user account (the ticket's step-1 audience), or
|
|
107
|
+
* the project owner. `false` is the state the support routing settings warn about.
|
|
108
|
+
*/
|
|
109
|
+
export async function projectReachesRecipientBeforeManagers(
|
|
110
|
+
em: EntityManager,
|
|
111
|
+
scope: ProjectAssignmentScope,
|
|
112
|
+
project: ProjectRecipientProbe,
|
|
113
|
+
options: { graceDays?: number | null; now?: Date } = {},
|
|
114
|
+
): Promise<boolean> {
|
|
115
|
+
if (typeof project.ownerUserId === 'string' && project.ownerUserId.length > 0) return true
|
|
116
|
+
const members = await loadActiveProjectMembers(em, scope, project.id, options)
|
|
117
|
+
const hasUser = (member: { userId: string | null }) => typeof member.userId === 'string' && member.userId.length > 0
|
|
118
|
+
if (members.some((member) => member.isAssigner && hasUser(member))) return true
|
|
119
|
+
const defaultAssigneeId = project.defaultAssigneeStaffMemberId ?? null
|
|
120
|
+
if (!defaultAssigneeId) return false
|
|
121
|
+
return members.some((member) => member.staffMemberId === defaultAssigneeId && hasUser(member))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
type ContainerLike = { resolve: <T = unknown>(name: string) => T }
|
|
125
|
+
|
|
126
|
+
export type NotifyProjectWorkOptions = {
|
|
127
|
+
typeDef: NotificationTypeDefinition
|
|
128
|
+
recipients: ResolvedWorkRecipients
|
|
129
|
+
build: {
|
|
130
|
+
titleVariables?: Record<string, string>
|
|
131
|
+
bodyVariables?: Record<string, string>
|
|
132
|
+
sourceEntityType?: string
|
|
133
|
+
sourceEntityId?: string
|
|
134
|
+
linkHref?: string
|
|
135
|
+
groupKey?: string
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export type NotifyProjectWorkResult = {
|
|
140
|
+
step: WorkRecipientStep
|
|
141
|
+
notifiedCount: number
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Sends one notification to the resolved recipients. Step 4 always passes
|
|
146
|
+
* `restrictRecipientsToOrganization: true`: without it `createForFeature` notifies
|
|
147
|
+
* every holder of the feature in the whole tenant. With the flag on, the service
|
|
148
|
+
* returns no recipients (and logs) when the organization is missing, more than 200
|
|
149
|
+
* tenant-wide candidates hold the feature, the container is unavailable, the RBAC
|
|
150
|
+
* check throws, or nobody in the organization holds it; `notifiedCount` is then 0
|
|
151
|
+
* and the caller decides what "nobody notified" means for its record.
|
|
152
|
+
*/
|
|
153
|
+
export async function notifyProjectWork(
|
|
154
|
+
container: ContainerLike,
|
|
155
|
+
scope: ProjectAssignmentScope,
|
|
156
|
+
options: NotifyProjectWorkOptions,
|
|
157
|
+
): Promise<NotifyProjectWorkResult> {
|
|
158
|
+
const notificationService = resolveNotificationService(container)
|
|
159
|
+
const context = { tenantId: scope.tenantId, organizationId: scope.organizationId }
|
|
160
|
+
const { typeDef, recipients, build } = options
|
|
161
|
+
if (recipients.step === 4) {
|
|
162
|
+
const created = await notificationService.createForFeature(
|
|
163
|
+
{
|
|
164
|
+
...buildFeatureNotificationFromType(typeDef, { ...build, requiredFeature: PROJECT_MANAGERS_FEATURE }),
|
|
165
|
+
restrictRecipientsToOrganization: true,
|
|
166
|
+
},
|
|
167
|
+
context,
|
|
168
|
+
)
|
|
169
|
+
return { step: 4, notifiedCount: created.length }
|
|
170
|
+
}
|
|
171
|
+
if (recipients.userIds.length === 0) return { step: recipients.step, notifiedCount: 0 }
|
|
172
|
+
try {
|
|
173
|
+
const created = await notificationService.createBatch(
|
|
174
|
+
buildBatchNotificationFromType(typeDef, { ...build, recipientUserIds: recipients.userIds }),
|
|
175
|
+
context,
|
|
176
|
+
)
|
|
177
|
+
return { step: recipients.step, notifiedCount: created.length }
|
|
178
|
+
} catch (err) {
|
|
179
|
+
logger.error('staff project-work notification failed', { err, type: typeDef.type, step: recipients.step })
|
|
180
|
+
getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.project_work_notification_failed' })
|
|
181
|
+
return { step: recipients.step, notifiedCount: 0 }
|
|
182
|
+
}
|
|
183
|
+
}
|