@open-mercato/core 0.10.1-develop.7717.1.2befe9c634 → 0.10.1-develop.7721.1.b5c4a00a68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/staff_support_routing_rule/index.js +21 -0
- package/dist/generated/entities/staff_support_routing_rule/index.js.map +7 -0
- package/dist/generated/entities/staff_support_ticket/index.js +37 -0
- package/dist/generated/entities/staff_support_ticket/index.js.map +7 -0
- package/dist/generated/entities/staff_time_project/index.js +2 -0
- package/dist/generated/entities/staff_time_project/index.js.map +2 -2
- package/dist/generated/entities/staff_time_project_member/index.js +2 -0
- package/dist/generated/entities/staff_time_project_member/index.js.map +2 -2
- package/dist/generated/entities/staff_time_task/index.js +4 -0
- package/dist/generated/entities/staff_time_task/index.js.map +2 -2
- package/dist/generated/entities/staff_time_task_comment/index.js +9 -1
- package/dist/generated/entities/staff_time_task_comment/index.js.map +2 -2
- package/dist/generated/entities.ids.generated.js +2 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +38 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/auth.js +1 -0
- package/dist/helpers/integration/auth.js.map +2 -2
- package/dist/helpers/integration/communicationChannelsFixtures.js +20 -0
- package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
- package/dist/modules/catalog/api/bulk-delete/route.js +12 -2
- package/dist/modules/catalog/api/bulk-delete/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/test-seed/route.js +17 -3
- package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
- package/dist/modules/communication_channels/lib/test-seed.js +38 -5
- package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
- package/dist/modules/messages/commands/messages.js +2 -3
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
- package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js +299 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js +91 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/route.js +146 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/options/route.js +60 -0
- package/dist/modules/staff/api/portal/support-tickets/options/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/route.js +220 -0
- package/dist/modules/staff/api/portal/support-tickets/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/settings/route.js +7 -1
- package/dist/modules/staff/api/timesheets/settings/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js +122 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js +180 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/route.js +147 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/support-tickets/options/route.js +133 -0
- package/dist/modules/staff/api/timesheets/support-tickets/options/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js +33 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js +25 -7
- package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js +32 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/route.js +94 -47
- package/dist/modules/staff/api/timesheets/tasks/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +3 -0
- package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/route.js +3 -0
- package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +1 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +54 -4
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js +21 -5
- package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/settings/page.js +319 -254
- package/dist/modules/staff/backend/staff/time-tracking/settings/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/support/page.js +363 -0
- package/dist/modules/staff/backend/staff/time-tracking/support/page.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js +18 -0
- package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js.map +7 -0
- package/dist/modules/staff/cli.js +27 -0
- package/dist/modules/staff/cli.js.map +2 -2
- package/dist/modules/staff/commands/index.js +3 -0
- package/dist/modules/staff/commands/index.js.map +2 -2
- package/dist/modules/staff/commands/shared.js +10 -1
- package/dist/modules/staff/commands/shared.js.map +2 -2
- package/dist/modules/staff/commands/support-routing-rules.js +505 -0
- package/dist/modules/staff/commands/support-routing-rules.js.map +7 -0
- package/dist/modules/staff/commands/support-tickets.js +513 -0
- package/dist/modules/staff/commands/support-tickets.js.map +7 -0
- package/dist/modules/staff/commands/timesheets-projects.js +72 -14
- package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-task-assignment.js +270 -0
- package/dist/modules/staff/commands/timesheets-task-assignment.js.map +7 -0
- package/dist/modules/staff/commands/timesheets-task-comments.js +58 -12
- package/dist/modules/staff/commands/timesheets-task-comments.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-tasks.js +91 -9
- package/dist/modules/staff/commands/timesheets-tasks.js.map +3 -3
- package/dist/modules/staff/data/enrichers.js +141 -0
- package/dist/modules/staff/data/enrichers.js.map +2 -2
- package/dist/modules/staff/data/entities.js +143 -2
- package/dist/modules/staff/data/entities.js.map +2 -2
- package/dist/modules/staff/data/extensions.js +8 -0
- package/dist/modules/staff/data/extensions.js.map +2 -2
- package/dist/modules/staff/data/validators.js +34 -5
- package/dist/modules/staff/data/validators.js.map +2 -2
- package/dist/modules/staff/encryption.js +6 -0
- package/dist/modules/staff/encryption.js.map +2 -2
- package/dist/modules/staff/events.js +25 -0
- package/dist/modules/staff/events.js.map +2 -2
- package/dist/modules/staff/events.payloads.js +54 -0
- package/dist/modules/staff/events.payloads.js.map +2 -2
- package/dist/modules/staff/extension-points.js +1 -0
- package/dist/modules/staff/extension-points.js.map +2 -2
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js +468 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js +12 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js +316 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js +12 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js +190 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js +19 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js.map +7 -0
- package/dist/modules/staff/index.js +1 -1
- package/dist/modules/staff/index.js.map +2 -2
- package/dist/modules/staff/lib/crud.js +2 -0
- package/dist/modules/staff/lib/crud.js.map +2 -2
- package/dist/modules/staff/lib/support/attachments.js +36 -0
- package/dist/modules/staff/lib/support/attachments.js.map +7 -0
- package/dist/modules/staff/lib/support/dictionaries.js +87 -0
- package/dist/modules/staff/lib/support/dictionaries.js.map +7 -0
- package/dist/modules/staff/lib/support/notifications.js +111 -0
- package/dist/modules/staff/lib/support/notifications.js.map +7 -0
- package/dist/modules/staff/lib/support/portalAttachments.js +100 -0
- package/dist/modules/staff/lib/support/portalAttachments.js.map +7 -0
- package/dist/modules/staff/lib/support/portalBroadcast.js +117 -0
- package/dist/modules/staff/lib/support/portalBroadcast.js.map +7 -0
- package/dist/modules/staff/lib/support/portalRoute.js +99 -0
- package/dist/modules/staff/lib/support/portalRoute.js.map +7 -0
- package/dist/modules/staff/lib/support/portalStatus.js +12 -0
- package/dist/modules/staff/lib/support/portalStatus.js.map +7 -0
- package/dist/modules/staff/lib/support/portalTickets.js +148 -0
- package/dist/modules/staff/lib/support/portalTickets.js.map +7 -0
- package/dist/modules/staff/lib/support/routing.js +81 -0
- package/dist/modules/staff/lib/support/routing.js.map +7 -0
- package/dist/modules/staff/lib/support/routingRuleDraft.js +25 -0
- package/dist/modules/staff/lib/support/routingRuleDraft.js.map +7 -0
- package/dist/modules/staff/lib/support/seedDefaults.js +114 -0
- package/dist/modules/staff/lib/support/seedDefaults.js.map +7 -0
- package/dist/modules/staff/lib/support/supportActivity.js +26 -0
- package/dist/modules/staff/lib/support/supportActivity.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js +23 -0
- package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js +112 -0
- package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/portalPendingUploads.js +45 -0
- package/dist/modules/staff/lib/support-ui/portalPendingUploads.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/portalSupport.js +51 -0
- package/dist/modules/staff/lib/support-ui/portalSupport.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/access.js +3 -39
- package/dist/modules/staff/lib/time-tracking/access.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/listProjectAccess.js +54 -0
- package/dist/modules/staff/lib/time-tracking/listProjectAccess.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/portalRecipients.js +92 -2
- package/dist/modules/staff/lib/time-tracking/portalRecipients.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/projectAssignment.js +112 -0
- package/dist/modules/staff/lib/time-tracking/projectAssignment.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/settingKeys.js +14 -0
- package/dist/modules/staff/lib/time-tracking/settingKeys.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/settings.js +28 -1
- package/dist/modules/staff/lib/time-tracking/settings.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js +143 -0
- package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/workRecipients.js +92 -0
- package/dist/modules/staff/lib/time-tracking/workRecipients.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js +32 -3
- package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js +29 -0
- package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js +4 -0
- package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js +141 -1
- package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js +153 -0
- package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js +67 -0
- package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js +3 -1
- package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js +80 -17
- package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js +230 -41
- package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js +21 -1
- package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js +16 -4
- package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js +3 -1
- package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js +68 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js +50 -0
- package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js +13 -1
- package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js +36 -2
- package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js.map +2 -2
- package/dist/modules/staff/migrations/Migration20261009180543_staff.js +42 -0
- package/dist/modules/staff/migrations/Migration20261009180543_staff.js.map +7 -0
- package/dist/modules/staff/notifications.js +92 -0
- package/dist/modules/staff/notifications.js.map +2 -2
- package/dist/modules/staff/setup.js +6 -2
- package/dist/modules/staff/setup.js.map +2 -2
- package/dist/modules/staff/subscribers/support-portal-comment-created.js +26 -0
- package/dist/modules/staff/subscribers/support-portal-comment-created.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-comment-deleted.js +26 -0
- package/dist/modules/staff/subscribers/support-portal-comment-deleted.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-comment-updated.js +26 -0
- package/dist/modules/staff/subscribers/support-portal-comment-updated.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-status-column-updated.js +24 -0
- package/dist/modules/staff/subscribers/support-portal-status-column-updated.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-task-status-changed.js +25 -0
- package/dist/modules/staff/subscribers/support-portal-task-status-changed.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-task-updated.js +25 -0
- package/dist/modules/staff/subscribers/support-portal-task-updated.js.map +7 -0
- package/dist/modules/staff/subscribers/support-ticket-created-notification.js +25 -0
- package/dist/modules/staff/subscribers/support-ticket-created-notification.js.map +7 -0
- package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js +25 -0
- package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js.map +7 -0
- package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js +26 -0
- package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js.map +7 -0
- package/dist/modules/staff/subscribers/time-task-assignee-notification.js +83 -0
- package/dist/modules/staff/subscribers/time-task-assignee-notification.js.map +7 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js +112 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js.map +7 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js +17 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js.map +7 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js +870 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js.map +7 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js +17 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js.map +7 -0
- package/dist/modules/staff/widgets/injection-table.js +16 -1
- package/dist/modules/staff/widgets/injection-table.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +4 -18
- package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLotDetailPage.js +4 -18
- package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +4 -18
- package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +2 -2
- package/dist/modules/wms/lib/inventoryCsvExport.js +19 -0
- package/dist/modules/wms/lib/inventoryCsvExport.js.map +7 -0
- package/generated/entities/staff_support_routing_rule/index.ts +9 -0
- package/generated/entities/staff_support_ticket/index.ts +17 -0
- package/generated/entities/staff_time_project/index.ts +1 -0
- package/generated/entities/staff_time_project_member/index.ts +1 -0
- package/generated/entities/staff_time_task/index.ts +2 -0
- package/generated/entities/staff_time_task_comment/index.ts +4 -0
- package/generated/entities.ids.generated.ts +2 -0
- package/generated/entity-fields-registry.ts +38 -0
- package/package.json +8 -8
- package/src/helpers/integration/auth.ts +1 -1
- package/src/helpers/integration/communicationChannelsFixtures.ts +60 -2
- package/src/modules/catalog/api/bulk-delete/route.ts +12 -2
- package/src/modules/communication_channels/api/post/test-seed/route.ts +29 -12
- package/src/modules/communication_channels/lib/test-seed.ts +49 -10
- package/src/modules/messages/commands/messages.ts +8 -10
- package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +2 -2
- package/src/modules/staff/AGENTS.md +22 -6
- package/src/modules/staff/api/portal/support-tickets/[id]/attachments/route.ts +327 -0
- package/src/modules/staff/api/portal/support-tickets/[id]/messages/route.ts +94 -0
- package/src/modules/staff/api/portal/support-tickets/[id]/route.ts +160 -0
- package/src/modules/staff/api/portal/support-tickets/options/route.ts +60 -0
- package/src/modules/staff/api/portal/support-tickets/route.ts +253 -0
- package/src/modules/staff/api/timesheets/settings/route.ts +7 -1
- package/src/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.ts +131 -0
- package/src/modules/staff/api/timesheets/support-routing-rules/preview/route.ts +248 -0
- package/src/modules/staff/api/timesheets/support-routing-rules/route.ts +150 -0
- package/src/modules/staff/api/timesheets/support-tickets/options/route.ts +181 -0
- package/src/modules/staff/api/timesheets/tasks/[id]/claim/route.ts +31 -0
- package/src/modules/staff/api/timesheets/tasks/[id]/comments/route.ts +25 -7
- package/src/modules/staff/api/timesheets/tasks/[id]/release/route.ts +30 -0
- package/src/modules/staff/api/timesheets/tasks/route.ts +119 -67
- package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +3 -0
- package/src/modules/staff/api/timesheets/time-projects/route.ts +3 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +1 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +67 -2
- package/src/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.ts +22 -3
- package/src/modules/staff/backend/staff/time-tracking/settings/page.tsx +405 -308
- package/src/modules/staff/backend/staff/time-tracking/support/page.meta.ts +14 -0
- package/src/modules/staff/backend/staff/time-tracking/support/page.tsx +428 -0
- package/src/modules/staff/cli.ts +28 -0
- package/src/modules/staff/commands/index.ts +3 -0
- package/src/modules/staff/commands/shared.ts +22 -1
- package/src/modules/staff/commands/support-routing-rules.ts +595 -0
- package/src/modules/staff/commands/support-tickets.ts +580 -0
- package/src/modules/staff/commands/timesheets-projects.ts +84 -13
- package/src/modules/staff/commands/timesheets-task-assignment.ts +319 -0
- package/src/modules/staff/commands/timesheets-task-comments.ts +70 -15
- package/src/modules/staff/commands/timesheets-tasks.ts +106 -15
- package/src/modules/staff/data/enrichers.ts +203 -0
- package/src/modules/staff/data/entities.ts +133 -2
- package/src/modules/staff/data/extensions.ts +9 -1
- package/src/modules/staff/data/validators.ts +36 -0
- package/src/modules/staff/encryption.ts +6 -0
- package/src/modules/staff/events.payloads.ts +87 -1
- package/src/modules/staff/events.ts +26 -0
- package/src/modules/staff/extension-points.ts +1 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.ts +10 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.tsx +553 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.ts +10 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.tsx +346 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/page.meta.ts +17 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/page.tsx +253 -0
- package/src/modules/staff/i18n/de.json +291 -0
- package/src/modules/staff/i18n/en.json +291 -0
- package/src/modules/staff/i18n/es.json +291 -0
- package/src/modules/staff/i18n/ko.json +291 -0
- package/src/modules/staff/i18n/pl.json +291 -0
- package/src/modules/staff/index.ts +1 -1
- package/src/modules/staff/lib/crud.ts +2 -1
- package/src/modules/staff/lib/support/attachments.ts +43 -0
- package/src/modules/staff/lib/support/dictionaries.ts +120 -0
- package/src/modules/staff/lib/support/notifications.ts +138 -0
- package/src/modules/staff/lib/support/portalAttachments.ts +140 -0
- package/src/modules/staff/lib/support/portalBroadcast.ts +161 -0
- package/src/modules/staff/lib/support/portalRoute.ts +108 -0
- package/src/modules/staff/lib/support/portalStatus.ts +15 -0
- package/src/modules/staff/lib/support/portalTickets.ts +233 -0
- package/src/modules/staff/lib/support/routing.ts +117 -0
- package/src/modules/staff/lib/support/routingRuleDraft.ts +45 -0
- package/src/modules/staff/lib/support/seedDefaults.ts +151 -0
- package/src/modules/staff/lib/support/supportActivity.ts +39 -0
- package/src/modules/staff/lib/support-ui/PortalSupportNotLinked.tsx +20 -0
- package/src/modules/staff/lib/support-ui/portalAttachmentValidation.ts +125 -0
- package/src/modules/staff/lib/support-ui/portalPendingUploads.ts +49 -0
- package/src/modules/staff/lib/support-ui/portalSupport.ts +116 -0
- package/src/modules/staff/lib/time-tracking/access.ts +2 -62
- package/src/modules/staff/lib/time-tracking/listProjectAccess.ts +78 -0
- package/src/modules/staff/lib/time-tracking/portalRecipients.ts +173 -5
- package/src/modules/staff/lib/time-tracking/projectAssignment.ts +172 -0
- package/src/modules/staff/lib/time-tracking/settingKeys.ts +11 -0
- package/src/modules/staff/lib/time-tracking/settings.ts +46 -1
- package/src/modules/staff/lib/time-tracking/taskAssignmentRoute.ts +165 -0
- package/src/modules/staff/lib/time-tracking/workRecipients.ts +183 -0
- package/src/modules/staff/lib/time-tracking-ui/BoardFilterChips.tsx +36 -1
- package/src/modules/staff/lib/time-tracking-ui/KanbanBoard.tsx +30 -0
- package/src/modules/staff/lib/time-tracking-ui/KanbanCard.tsx +9 -0
- package/src/modules/staff/lib/time-tracking-ui/ProjectFormSections.tsx +185 -6
- package/src/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.tsx +188 -0
- package/src/modules/staff/lib/time-tracking-ui/SupportRequestPanel.tsx +102 -0
- package/src/modules/staff/lib/time-tracking-ui/TaskBoardScreen.tsx +2 -0
- package/src/modules/staff/lib/time-tracking-ui/TaskCommentThread.tsx +105 -22
- package/src/modules/staff/lib/time-tracking-ui/TaskDrawer.tsx +278 -43
- package/src/modules/staff/lib/time-tracking-ui/boardDirectory.ts +21 -0
- package/src/modules/staff/lib/time-tracking-ui/boardFilters.ts +16 -2
- package/src/modules/staff/lib/time-tracking-ui/kanbanBoardData.ts +4 -0
- package/src/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.ts +117 -0
- package/src/modules/staff/lib/time-tracking-ui/settingsTabs.ts +17 -0
- package/src/modules/staff/lib/time-tracking-ui/supportTicketData.ts +71 -0
- package/src/modules/staff/lib/time-tracking-ui/taskDrawerData.ts +15 -0
- package/src/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.ts +48 -1
- package/src/modules/staff/migrations/.snapshot-open-mercato.json +714 -0
- package/src/modules/staff/migrations/Migration20261009180543_staff.ts +50 -0
- package/src/modules/staff/notifications.ts +92 -0
- package/src/modules/staff/setup.ts +6 -2
- package/src/modules/staff/subscribers/support-portal-comment-created.ts +35 -0
- package/src/modules/staff/subscribers/support-portal-comment-deleted.ts +35 -0
- package/src/modules/staff/subscribers/support-portal-comment-updated.ts +35 -0
- package/src/modules/staff/subscribers/support-portal-status-column-updated.ts +36 -0
- package/src/modules/staff/subscribers/support-portal-task-status-changed.ts +34 -0
- package/src/modules/staff/subscribers/support-portal-task-updated.ts +34 -0
- package/src/modules/staff/subscribers/support-ticket-created-notification.ts +27 -0
- package/src/modules/staff/subscribers/support-ticket-customer-replied-notification.ts +27 -0
- package/src/modules/staff/subscribers/support-ticket-unnotified-clear.ts +29 -0
- package/src/modules/staff/subscribers/time-task-assignee-notification.ts +102 -0
- package/src/modules/staff/widgets/injection/portal-support-tickets/widget.client.tsx +151 -0
- package/src/modules/staff/widgets/injection/portal-support-tickets/widget.ts +16 -0
- package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.tsx +1058 -0
- package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.ts +17 -0
- package/src/modules/staff/widgets/injection-table.ts +15 -0
- package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +4 -20
- package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +4 -20
- package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +4 -19
- package/src/modules/wms/lib/inventoryCsvExport.ts +18 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_ATTACHMENT_MAX_UPLOAD_BYTES = 25 * 1024 * 1024
|
|
4
|
+
|
|
5
|
+
export const ATTACHMENT_ACCEPT_TYPES = [
|
|
6
|
+
'.avif',
|
|
7
|
+
'.bmp',
|
|
8
|
+
'.csv',
|
|
9
|
+
'.docx',
|
|
10
|
+
'.gif',
|
|
11
|
+
'.jpeg',
|
|
12
|
+
'.jpg',
|
|
13
|
+
'.json',
|
|
14
|
+
'.md',
|
|
15
|
+
'.pdf',
|
|
16
|
+
'.png',
|
|
17
|
+
'.pptx',
|
|
18
|
+
'.txt',
|
|
19
|
+
'.webp',
|
|
20
|
+
'.xlsx',
|
|
21
|
+
'.zip',
|
|
22
|
+
'application/json',
|
|
23
|
+
'application/pdf',
|
|
24
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
25
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
26
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
27
|
+
'application/zip',
|
|
28
|
+
'image/avif',
|
|
29
|
+
'image/bmp',
|
|
30
|
+
'image/gif',
|
|
31
|
+
'image/jpeg',
|
|
32
|
+
'image/png',
|
|
33
|
+
'image/webp',
|
|
34
|
+
'text/csv',
|
|
35
|
+
'text/markdown',
|
|
36
|
+
'text/plain',
|
|
37
|
+
].join(',')
|
|
38
|
+
|
|
39
|
+
const BLOCKED_ATTACHMENT_MIME_TYPES = new Set([
|
|
40
|
+
'application/xhtml+xml',
|
|
41
|
+
'application/xml',
|
|
42
|
+
'image/svg+xml',
|
|
43
|
+
'text/html',
|
|
44
|
+
'text/xml',
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
const ACTIVE_CONTENT_ATTACHMENT_EXTENSIONS = new Set([
|
|
48
|
+
'htm',
|
|
49
|
+
'html',
|
|
50
|
+
'svg',
|
|
51
|
+
'xhtml',
|
|
52
|
+
'xml',
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
const DANGEROUS_EXECUTABLE_EXTENSIONS = new Set([
|
|
56
|
+
'app',
|
|
57
|
+
'apk',
|
|
58
|
+
'bat',
|
|
59
|
+
'cmd',
|
|
60
|
+
'com',
|
|
61
|
+
'dll',
|
|
62
|
+
'exe',
|
|
63
|
+
'hta',
|
|
64
|
+
'htm',
|
|
65
|
+
'html',
|
|
66
|
+
'jar',
|
|
67
|
+
'js',
|
|
68
|
+
'jse',
|
|
69
|
+
'lnk',
|
|
70
|
+
'msi',
|
|
71
|
+
'pif',
|
|
72
|
+
'ps1',
|
|
73
|
+
'psm1',
|
|
74
|
+
'reg',
|
|
75
|
+
'scr',
|
|
76
|
+
'sh',
|
|
77
|
+
'vbe',
|
|
78
|
+
'vbs',
|
|
79
|
+
'wsf',
|
|
80
|
+
'wsh',
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
function getFileExtensionSegments(fileName: string): string[] {
|
|
84
|
+
const parts = fileName.trim().split('.').filter(Boolean)
|
|
85
|
+
if (parts.length < 2) return []
|
|
86
|
+
return parts.slice(1).map((part) => part.toLowerCase().replace(/[^a-z0-9]/g, '')).filter(Boolean)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function fileLooksLikeActiveContent(file: File): Promise<boolean> {
|
|
90
|
+
try {
|
|
91
|
+
const sniff = (await file.slice(0, 512).text()).trimStart().toLowerCase()
|
|
92
|
+
return sniff.startsWith('<svg') || sniff.startsWith('<?xml') || sniff.startsWith('<!doctype html') || sniff.startsWith('<html')
|
|
93
|
+
} catch {
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function resolvePortalMaxUploadBytes(serverMaxBytes: number | null | undefined): number {
|
|
99
|
+
return typeof serverMaxBytes === 'number' && Number.isFinite(serverMaxBytes) && serverMaxBytes > 0
|
|
100
|
+
? serverMaxBytes
|
|
101
|
+
: DEFAULT_ATTACHMENT_MAX_UPLOAD_BYTES
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function validateAttachmentFile(
|
|
105
|
+
file: File,
|
|
106
|
+
t: TranslateFn,
|
|
107
|
+
maxBytes: number = DEFAULT_ATTACHMENT_MAX_UPLOAD_BYTES,
|
|
108
|
+
): Promise<string | null> {
|
|
109
|
+
if (file.size > maxBytes) {
|
|
110
|
+
return t('attachments.errors.maxUploadSize')
|
|
111
|
+
}
|
|
112
|
+
const extensionSegments = getFileExtensionSegments(file.name)
|
|
113
|
+
const mimeType = file.type.trim().toLowerCase()
|
|
114
|
+
if (extensionSegments.some((extension) => DANGEROUS_EXECUTABLE_EXTENSIONS.has(extension))) {
|
|
115
|
+
return t('attachments.errors.dangerousExecutable')
|
|
116
|
+
}
|
|
117
|
+
if (
|
|
118
|
+
extensionSegments.some((extension) => ACTIVE_CONTENT_ATTACHMENT_EXTENSIONS.has(extension)) ||
|
|
119
|
+
(mimeType && BLOCKED_ATTACHMENT_MIME_TYPES.has(mimeType)) ||
|
|
120
|
+
await fileLooksLikeActiveContent(file)
|
|
121
|
+
) {
|
|
122
|
+
return t('attachments.errors.activeContentBlocked')
|
|
123
|
+
}
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { failedUploadsStorageKey } from './portalSupport'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Files that failed to upload while filing a ticket. The names survive a reload in
|
|
5
|
+
* `sessionStorage`; the `File` objects themselves can only live in memory, so a retry
|
|
6
|
+
* is possible for files picked in this browser session and a re-attach otherwise.
|
|
7
|
+
*/
|
|
8
|
+
const retryableFilesByTicket = new Map<string, Map<string, File>>()
|
|
9
|
+
|
|
10
|
+
export function rememberFailedUploads(ticketId: string, files: readonly File[]): void {
|
|
11
|
+
if (!files.length) return
|
|
12
|
+
const current = retryableFilesByTicket.get(ticketId) ?? new Map<string, File>()
|
|
13
|
+
for (const file of files) current.set(file.name, file)
|
|
14
|
+
retryableFilesByTicket.set(ticketId, current)
|
|
15
|
+
writeFailedUploadNames(ticketId, [...new Set([...readFailedUploadNames(ticketId), ...files.map((file) => file.name)])])
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getRetryableUpload(ticketId: string, fileName: string): File | null {
|
|
19
|
+
return retryableFilesByTicket.get(ticketId)?.get(fileName) ?? null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function forgetFailedUpload(ticketId: string, fileName: string): string[] {
|
|
23
|
+
const files = retryableFilesByTicket.get(ticketId)
|
|
24
|
+
files?.delete(fileName)
|
|
25
|
+
if (files && files.size === 0) retryableFilesByTicket.delete(ticketId)
|
|
26
|
+
const remaining = readFailedUploadNames(ticketId).filter((name) => name !== fileName)
|
|
27
|
+
writeFailedUploadNames(ticketId, remaining)
|
|
28
|
+
return remaining
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function readFailedUploadNames(ticketId: string): string[] {
|
|
32
|
+
try {
|
|
33
|
+
const raw = window.sessionStorage.getItem(failedUploadsStorageKey(ticketId))
|
|
34
|
+
if (!raw) return []
|
|
35
|
+
const parsed: unknown = JSON.parse(raw)
|
|
36
|
+
return Array.isArray(parsed) ? parsed.filter((entry): entry is string => typeof entry === 'string') : []
|
|
37
|
+
} catch {
|
|
38
|
+
return []
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeFailedUploadNames(ticketId: string, names: readonly string[]): void {
|
|
43
|
+
try {
|
|
44
|
+
if (names.length) window.sessionStorage.setItem(failedUploadsStorageKey(ticketId), JSON.stringify(names))
|
|
45
|
+
else window.sessionStorage.removeItem(failedUploadsStorageKey(ticketId))
|
|
46
|
+
} catch {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { StatusBadgeVariant } from '@open-mercato/ui/primitives/status-badge'
|
|
2
|
+
import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
3
|
+
|
|
4
|
+
export type PortalSupportStatus = 'received' | 'in_progress' | 'resolved'
|
|
5
|
+
|
|
6
|
+
export type PortalSupportTicketListItem = {
|
|
7
|
+
id: string
|
|
8
|
+
supportReference: string
|
|
9
|
+
subject: string
|
|
10
|
+
category: { value: string; label: string }
|
|
11
|
+
portalStatus: PortalSupportStatus
|
|
12
|
+
createdAt: string | null
|
|
13
|
+
updatedAt: string | null
|
|
14
|
+
lastActivityAt: string | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type PortalSupportTicketList = {
|
|
18
|
+
items?: PortalSupportTicketListItem[]
|
|
19
|
+
total?: number
|
|
20
|
+
page?: number
|
|
21
|
+
pageSize?: number
|
|
22
|
+
totalPages?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type PortalSupportOptions = {
|
|
26
|
+
categories?: Array<{ value: string; label: string }>
|
|
27
|
+
canCreate?: boolean
|
|
28
|
+
maxUploadBytes?: number | null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type PortalSupportMessage = {
|
|
32
|
+
id: string
|
|
33
|
+
authorKind: 'staff' | 'customer'
|
|
34
|
+
authorName: string | null
|
|
35
|
+
isSelf?: boolean
|
|
36
|
+
body: string
|
|
37
|
+
createdAt: string | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type PortalSupportTicketDetail = {
|
|
41
|
+
id: string
|
|
42
|
+
supportReference: string
|
|
43
|
+
subject: string
|
|
44
|
+
description: string
|
|
45
|
+
category: { value: string; label: string }
|
|
46
|
+
portalStatus: PortalSupportStatus
|
|
47
|
+
createdAt: string | null
|
|
48
|
+
updatedAt: string | null
|
|
49
|
+
canReply: boolean
|
|
50
|
+
requester?: { name: string | null; isSelf: boolean }
|
|
51
|
+
messages: PortalSupportMessage[]
|
|
52
|
+
attachments?: PortalSupportAttachment[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type PortalSupportAttachment = {
|
|
56
|
+
id: string
|
|
57
|
+
fileName: string
|
|
58
|
+
fileSize: number
|
|
59
|
+
mimeType: string | null
|
|
60
|
+
downloadUrl: string
|
|
61
|
+
uploadedByCustomer: boolean
|
|
62
|
+
canDelete: boolean
|
|
63
|
+
createdAt: string | null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const PORTAL_SUPPORT_UPDATED_EVENT = 'staff.support_ticket.portal_updated'
|
|
67
|
+
|
|
68
|
+
export const PORTAL_SUPPORT_NOT_LINKED_CODE = 'customer_account_not_linked'
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A portal account without a company link can never see or file tickets, so the
|
|
72
|
+
* support screens explain that instead of offering a retry that cannot succeed.
|
|
73
|
+
*/
|
|
74
|
+
export function isPortalSupportAccountNotLinked(call: { status: number; result?: unknown }): boolean {
|
|
75
|
+
if (call.status !== 403 || !call.result || typeof call.result !== 'object') return false
|
|
76
|
+
return (call.result as { code?: unknown }).code === PORTAL_SUPPORT_NOT_LINKED_CODE
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const PORTAL_SUPPORT_STATUS_VARIANTS: Record<PortalSupportStatus, StatusBadgeVariant> = {
|
|
80
|
+
received: 'info',
|
|
81
|
+
in_progress: 'warning',
|
|
82
|
+
resolved: 'success',
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function portalSupportStatusLabel(t: TranslateFn, status: PortalSupportStatus): string {
|
|
86
|
+
if (status === 'resolved') return t('staff.portal.support.status.resolved', 'Resolved')
|
|
87
|
+
if (status === 'in_progress') return t('staff.portal.support.status.inProgress', 'In progress')
|
|
88
|
+
return t('staff.portal.support.status.received', 'Received')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function formatPortalDate(value: string | null | undefined, locale?: string): string {
|
|
92
|
+
if (!value) return ''
|
|
93
|
+
const date = new Date(value)
|
|
94
|
+
if (Number.isNaN(date.getTime())) return ''
|
|
95
|
+
return date.toLocaleString(locale, { dateStyle: 'medium', timeStyle: 'short' })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function formatFileSize(t: TranslateFn, bytes: number): string {
|
|
99
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return t('staff.portal.support.fileSize.bytes', '{size} B', { size: 0 })
|
|
100
|
+
if (bytes < 1024) return t('staff.portal.support.fileSize.bytes', '{size} B', { size: bytes })
|
|
101
|
+
if (bytes < 1024 * 1024) {
|
|
102
|
+
return t('staff.portal.support.fileSize.kilobytes', '{size} KB', { size: (bytes / 1024).toFixed(1) })
|
|
103
|
+
}
|
|
104
|
+
return t('staff.portal.support.fileSize.megabytes', '{size} MB', { size: (bytes / (1024 * 1024)).toFixed(1) })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function failedUploadsStorageKey(ticketId: string): string {
|
|
108
|
+
return `om:staff:support:failed-uploads:${ticketId}`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function readPortalTicketIdFromEvent(event: { payload?: Record<string, unknown> } | null | undefined): string | null {
|
|
112
|
+
const payload = event?.payload
|
|
113
|
+
if (!payload || typeof payload !== 'object') return null
|
|
114
|
+
const candidate = payload.ticketId ?? payload.id
|
|
115
|
+
return typeof candidate === 'string' ? candidate : null
|
|
116
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { EntityManager } from '@mikro-orm/postgresql'
|
|
|
2
2
|
import { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'
|
|
3
3
|
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
4
4
|
import { StaffTeamMember, StaffTimeProjectMember } from '../../data/entities'
|
|
5
|
-
import {
|
|
5
|
+
import { isWithinAssignmentWindow, normalizeAssignmentGraceDays, toDayIndex, toEntryDayIndex } from './projectAssignment'
|
|
6
6
|
|
|
7
7
|
export const MANAGE_PROJECTS_FEATURE = 'staff.timesheets.projects.manage'
|
|
8
8
|
|
|
@@ -47,67 +47,7 @@ export type ProjectAccess = {
|
|
|
47
47
|
|
|
48
48
|
const DENIED: ProjectAccess = { canManageAll: false, projectIds: [], staffMemberId: null }
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
const ISO_DATE_PREFIX = /^(\d{4})-(\d{2})-(\d{2})/
|
|
52
|
-
|
|
53
|
-
export function normalizeAssignmentGraceDays(value: unknown): number {
|
|
54
|
-
if (typeof value !== 'number' || !Number.isInteger(value)) return DEFAULT_ASSIGNMENT_GRACE_DAYS
|
|
55
|
-
if (value < 0 || value > MAX_ASSIGNMENT_GRACE_DAYS) return DEFAULT_ASSIGNMENT_GRACE_DAYS
|
|
56
|
-
return value
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* `assigned_start_date` / `assigned_end_date` are `date` columns, so both bounds
|
|
61
|
-
* are compared as whole days rather than timestamps. Date instances are read
|
|
62
|
-
* through their local calendar fields because the postgres driver materialises a
|
|
63
|
-
* `date` as local midnight, and "today" likewise means the server's local day.
|
|
64
|
-
*/
|
|
65
|
-
function toDayIndex(value: unknown): number | null {
|
|
66
|
-
if (value == null) return null
|
|
67
|
-
if (value instanceof Date) {
|
|
68
|
-
if (Number.isNaN(value.getTime())) return null
|
|
69
|
-
return Date.UTC(value.getFullYear(), value.getMonth(), value.getDate()) / MS_PER_DAY
|
|
70
|
-
}
|
|
71
|
-
if (typeof value === 'string') {
|
|
72
|
-
const match = ISO_DATE_PREFIX.exec(value.trim())
|
|
73
|
-
if (!match) return null
|
|
74
|
-
return Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])) / MS_PER_DAY
|
|
75
|
-
}
|
|
76
|
-
return null
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* An entry date is a calendar day the client sent as `YYYY-MM-DD`, which
|
|
81
|
-
* `z.coerce.date()` turns into UTC midnight — so a Date is read through its UTC
|
|
82
|
-
* fields here, the same way the grid addresses a cell (D-8).
|
|
83
|
-
*/
|
|
84
|
-
function toEntryDayIndex(value: unknown): number | null {
|
|
85
|
-
if (value instanceof Date) {
|
|
86
|
-
if (Number.isNaN(value.getTime())) return null
|
|
87
|
-
return Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()) / MS_PER_DAY
|
|
88
|
-
}
|
|
89
|
-
return toDayIndex(value)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* A membership only grants access inside its assignment window: from
|
|
94
|
-
* `assigned_start_date` until `assigned_end_date` plus the tenant's grace days.
|
|
95
|
-
* A null end date is open-ended and never expires.
|
|
96
|
-
*/
|
|
97
|
-
export function isWithinAssignmentWindow(
|
|
98
|
-
membership: Pick<StaffTimeProjectMember, 'assignedStartDate' | 'assignedEndDate'>,
|
|
99
|
-
todayIndex: number,
|
|
100
|
-
graceDays: number,
|
|
101
|
-
): boolean {
|
|
102
|
-
if (membership.assignedStartDate != null) {
|
|
103
|
-
const startIndex = toDayIndex(membership.assignedStartDate)
|
|
104
|
-
if (startIndex === null || todayIndex < startIndex) return false
|
|
105
|
-
}
|
|
106
|
-
if (membership.assignedEndDate == null) return true
|
|
107
|
-
const endIndex = toDayIndex(membership.assignedEndDate)
|
|
108
|
-
if (endIndex === null) return false
|
|
109
|
-
return todayIndex <= endIndex + graceDays
|
|
110
|
-
}
|
|
50
|
+
export { isWithinAssignmentWindow, normalizeAssignmentGraceDays, toDayIndex }
|
|
111
51
|
|
|
112
52
|
export async function resolveProjectAccess(ctx: ProjectAccessContext): Promise<ProjectAccess> {
|
|
113
53
|
const tenantId = ctx.tenantId ?? null
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'
|
|
3
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
4
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
5
|
+
import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'
|
|
6
|
+
import { resolveFeatureAccess } from './featureAccess'
|
|
7
|
+
import { MANAGE_PROJECTS_FEATURE, resolveProjectAccess, type ProjectAccess } from './access'
|
|
8
|
+
import { readTimeTrackingSettings } from './settings'
|
|
9
|
+
|
|
10
|
+
const logger = createLogger('staff').child({ component: 'lib/time-tracking/listProjectAccess' })
|
|
11
|
+
|
|
12
|
+
const DENIED_ACCESS: ProjectAccess = { canManageAll: false, projectIds: [], staffMemberId: null }
|
|
13
|
+
|
|
14
|
+
type ContainerLike = { resolve: (name: string) => unknown }
|
|
15
|
+
|
|
16
|
+
type ListAccessCtx = Pick<CrudCtx, 'container' | 'auth' | 'selectedOrganizationId' | 'request'>
|
|
17
|
+
|
|
18
|
+
/** One access decision per request, shared by every filter that needs it. */
|
|
19
|
+
const accessByRequest = new WeakMap<Request, Promise<ProjectAccess>>()
|
|
20
|
+
|
|
21
|
+
async function resolveAssignmentGraceDays(container: ContainerLike, tenantId: string): Promise<number | null> {
|
|
22
|
+
try {
|
|
23
|
+
const configService = container.resolve('moduleConfigService') as ModuleConfigService
|
|
24
|
+
const settings = await readTimeTrackingSettings(configService, { tenantId })
|
|
25
|
+
return settings.access.assignmentGraceDays
|
|
26
|
+
} catch {
|
|
27
|
+
// Fail safe to the documented default rather than widening the window.
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function loadProjectAccess(
|
|
33
|
+
container: ContainerLike,
|
|
34
|
+
userId: string | null,
|
|
35
|
+
tenantId: string | null,
|
|
36
|
+
organizationId: string | null,
|
|
37
|
+
): Promise<ProjectAccess> {
|
|
38
|
+
if (!tenantId || !organizationId) return { ...DENIED_ACCESS }
|
|
39
|
+
const scope = { tenantId, organizationId }
|
|
40
|
+
try {
|
|
41
|
+
// One lookup, one authority, and a failure that says so. The previous
|
|
42
|
+
// `catch → []` could not tell "denied" from "could not ask", so an RBAC
|
|
43
|
+
// hiccup silently demoted a manager to their own memberships.
|
|
44
|
+
const access = await resolveFeatureAccess(container, userId, [MANAGE_PROJECTS_FEATURE], scope)
|
|
45
|
+
const em = container.resolve('em') as EntityManager
|
|
46
|
+
return await resolveProjectAccess({
|
|
47
|
+
em: em.fork(),
|
|
48
|
+
userId,
|
|
49
|
+
tenantId,
|
|
50
|
+
organizationId,
|
|
51
|
+
canManageAll: access.allowed,
|
|
52
|
+
userFeatures: access.grantedFeatures,
|
|
53
|
+
assignmentGraceDays: await resolveAssignmentGraceDays(container, tenantId),
|
|
54
|
+
})
|
|
55
|
+
} catch (err) {
|
|
56
|
+
logger.error('staff.timesheets.tasks access resolution failed', { err })
|
|
57
|
+
getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.task_access_resolution_failed' })
|
|
58
|
+
return { ...DENIED_ACCESS }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The project-access decision of the tasks list, memoised per request and failing
|
|
64
|
+
* closed. Shared by the tasks list route and the task action routes (status move,
|
|
65
|
+
* claim, release) so a task is only ever reachable through one access rule.
|
|
66
|
+
*/
|
|
67
|
+
export async function resolveListProjectAccess(ctx: ListAccessCtx): Promise<ProjectAccess> {
|
|
68
|
+
const tenantId = ctx.auth?.tenantId ?? null
|
|
69
|
+
const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
|
|
70
|
+
const load = () => loadProjectAccess(ctx.container, ctx.auth?.sub ?? null, tenantId, organizationId)
|
|
71
|
+
const request = ctx.request
|
|
72
|
+
if (!request) return load()
|
|
73
|
+
const cached = accessByRequest.get(request)
|
|
74
|
+
if (cached) return cached
|
|
75
|
+
const pending = load()
|
|
76
|
+
accessByRequest.set(request, pending)
|
|
77
|
+
return pending
|
|
78
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { TableNotFoundException } from '@mikro-orm/core'
|
|
2
3
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
4
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
5
|
+
import { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'
|
|
3
6
|
|
|
4
7
|
const logger = createLogger('staff').child({ component: 'time-tracking/portal-recipients' })
|
|
5
8
|
|
|
@@ -8,12 +11,20 @@ const logger = createLogger('staff').child({ component: 'time-tracking/portal-re
|
|
|
8
11
|
* column **names** rather than imports.
|
|
9
12
|
*
|
|
10
13
|
* Staff is slated for extraction into `@open-mercato/staff`, so it may not take a
|
|
11
|
-
* static dependency on `customer_accounts`; `requires` in `index.ts`
|
|
12
|
-
*
|
|
14
|
+
* static dependency on `customer_accounts`; `requires` in `index.ts` never gains it.
|
|
15
|
+
* A Kysely query against the string
|
|
13
16
|
* `'customer_users'` reads the mapping without an import edge, exactly the way
|
|
14
17
|
* `warranty_claims/commands/claims.ts` does — and if the table is absent because
|
|
15
|
-
* the module is not installed, the query throws
|
|
16
|
-
* which every caller treats as "no portal audience".
|
|
18
|
+
* the module is not installed, the query throws "relation does not exist" and the
|
|
19
|
+
* audience is empty, which every caller treats as "no portal audience".
|
|
20
|
+
*
|
|
21
|
+
* Each helper comes in two flavours. The `...Strict` variant returns `[]` only for
|
|
22
|
+
* an expected empty audience (no rows, or the `customer_accounts` tables absent)
|
|
23
|
+
* and rethrows any other query failure, so a caller that records "the portal was
|
|
24
|
+
* told" can retry instead of recording a delivery that never happened. The lenient
|
|
25
|
+
* variant keeps the historical swallow-and-return-`[]` contract and reports the
|
|
26
|
+
* unexpected failure to telemetry. Both fail closed: neither ever returns an
|
|
27
|
+
* unfiltered audience.
|
|
17
28
|
*
|
|
18
29
|
* `customer_users.customer_entity_id` is the FK into `customers:customer_entity`,
|
|
19
30
|
* which is the same id `staff_time_reports.customer_id` holds (EP-44 declares that
|
|
@@ -42,11 +53,30 @@ export type PortalRecipientScope = {
|
|
|
42
53
|
/** A broadcast fan-out, not a mailing list — a customer with more portal users than this gets a truncated audience rather than an unbounded query. */
|
|
43
54
|
export const MAX_PORTAL_RECIPIENTS = 100
|
|
44
55
|
|
|
56
|
+
const UNDEFINED_TABLE_SQLSTATE = '42P01'
|
|
57
|
+
|
|
45
58
|
function isNonEmpty(value: unknown): value is string {
|
|
46
59
|
return typeof value === 'string' && value.trim().length > 0
|
|
47
60
|
}
|
|
48
61
|
|
|
49
|
-
|
|
62
|
+
/** The `customer_accounts` tables are not installed: an expected "no portal audience", not a failure. */
|
|
63
|
+
export function isPortalTableMissingError(error: unknown): boolean {
|
|
64
|
+
let current: unknown = error
|
|
65
|
+
for (let depth = 0; depth < 4 && typeof current === 'object' && current !== null; depth += 1) {
|
|
66
|
+
if (current instanceof TableNotFoundException) return true
|
|
67
|
+
const candidate = current as { code?: unknown; cause?: unknown }
|
|
68
|
+
if (candidate.code === UNDEFINED_TABLE_SQLSTATE) return true
|
|
69
|
+
current = candidate.cause
|
|
70
|
+
}
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function reportSwallowedRecipientError(err: unknown, code: string): void {
|
|
75
|
+
if (isPortalTableMissingError(err)) return
|
|
76
|
+
getTelemetryRuntime()?.reportError(err, { module: 'staff', code })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function resolvePortalRecipientUserIdsStrict(
|
|
50
80
|
em: EntityManager,
|
|
51
81
|
scope: PortalRecipientScope,
|
|
52
82
|
): Promise<string[]> {
|
|
@@ -66,11 +96,149 @@ export async function resolvePortalRecipientUserIds(
|
|
|
66
96
|
.limit(MAX_PORTAL_RECIPIENTS)
|
|
67
97
|
.execute()
|
|
68
98
|
return rows.map((row) => row.id).filter(isNonEmpty)
|
|
99
|
+
} catch (err) {
|
|
100
|
+
if (isPortalTableMissingError(err)) return []
|
|
101
|
+
throw err
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function resolvePortalRecipientUserIds(
|
|
106
|
+
em: EntityManager,
|
|
107
|
+
scope: PortalRecipientScope,
|
|
108
|
+
): Promise<string[]> {
|
|
109
|
+
try {
|
|
110
|
+
return await resolvePortalRecipientUserIdsStrict(em, scope)
|
|
69
111
|
} catch (err) {
|
|
70
112
|
logger.warn('Could not resolve portal recipients for a time-tracking customer', {
|
|
71
113
|
customerId: scope.customerId,
|
|
72
114
|
err,
|
|
73
115
|
})
|
|
116
|
+
reportSwallowedRecipientError(err, 'staff.portal_recipients_resolve_failed')
|
|
117
|
+
return []
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
type CustomerAclDb = {
|
|
122
|
+
customer_user_acls: {
|
|
123
|
+
user_id: string
|
|
124
|
+
tenant_id: string
|
|
125
|
+
features_json: unknown
|
|
126
|
+
is_portal_admin: boolean
|
|
127
|
+
}
|
|
128
|
+
customer_user_roles: {
|
|
129
|
+
user_id: string
|
|
130
|
+
role_id: string
|
|
131
|
+
deleted_at: Date | null
|
|
132
|
+
}
|
|
133
|
+
customer_roles: {
|
|
134
|
+
id: string
|
|
135
|
+
tenant_id: string
|
|
136
|
+
organization_id: string
|
|
137
|
+
deleted_at: Date | null
|
|
138
|
+
}
|
|
139
|
+
customer_role_acls: {
|
|
140
|
+
role_id: string
|
|
141
|
+
tenant_id: string
|
|
142
|
+
features_json: unknown
|
|
143
|
+
is_portal_admin: boolean
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
type PortalAclGrant = { features: string[]; isPortalAdmin: boolean }
|
|
148
|
+
|
|
149
|
+
function readFeatureList(value: unknown): string[] {
|
|
150
|
+
let parsed: unknown = value
|
|
151
|
+
if (typeof value === 'string') {
|
|
152
|
+
try {
|
|
153
|
+
parsed = JSON.parse(value)
|
|
154
|
+
} catch {
|
|
155
|
+
return []
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return Array.isArray(parsed) ? parsed.filter(isNonEmpty) : []
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function mergeGrant(grants: Map<string, PortalAclGrant>, userId: string, featuresJson: unknown, isPortalAdmin: boolean): void {
|
|
162
|
+
const grant = grants.get(userId) ?? { features: [], isPortalAdmin: false }
|
|
163
|
+
grant.isPortalAdmin = grant.isPortalAdmin || isPortalAdmin === true
|
|
164
|
+
for (const feature of readFeatureList(featuresJson)) {
|
|
165
|
+
if (!grant.features.includes(feature)) grant.features.push(feature)
|
|
166
|
+
}
|
|
167
|
+
grants.set(userId, grant)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Narrows a portal audience to the customer users holding `requiredFeature`, resolved the
|
|
172
|
+
* way `customer_accounts`' `CustomerRbacService.loadAcl` does: a per-user ACL row replaces
|
|
173
|
+
* the user's role ACLs, a portal admin is unrestricted, and grants match wildcard-aware via
|
|
174
|
+
* `authorizeFeatures`. Read by table name for the same decoupling reason as above. The
|
|
175
|
+
* strict variant returns `[]` when the module is absent and rethrows any other failure;
|
|
176
|
+
* neither variant ever returns an unfiltered audience.
|
|
177
|
+
*/
|
|
178
|
+
export async function filterPortalRecipientsByFeatureStrict(
|
|
179
|
+
em: EntityManager,
|
|
180
|
+
userIds: string[],
|
|
181
|
+
scope: { tenantId: string; organizationId: string },
|
|
182
|
+
requiredFeature: string,
|
|
183
|
+
): Promise<string[]> {
|
|
184
|
+
const candidates = Array.from(new Set(userIds.filter(isNonEmpty)))
|
|
185
|
+
if (candidates.length === 0 || !isNonEmpty(scope.tenantId) || !isNonEmpty(scope.organizationId)) return []
|
|
186
|
+
try {
|
|
187
|
+
const db = em.getKysely<CustomerAclDb>()
|
|
188
|
+
const userAclRows = await db
|
|
189
|
+
.selectFrom('customer_user_acls')
|
|
190
|
+
.select(['user_id', 'features_json', 'is_portal_admin'])
|
|
191
|
+
.where('tenant_id', '=', scope.tenantId)
|
|
192
|
+
.where('user_id', 'in', candidates)
|
|
193
|
+
.execute()
|
|
194
|
+
const userGrants = new Map<string, PortalAclGrant>()
|
|
195
|
+
for (const row of userAclRows) mergeGrant(userGrants, row.user_id, row.features_json, row.is_portal_admin)
|
|
196
|
+
const roleCandidates = candidates.filter((userId) => !userGrants.has(userId))
|
|
197
|
+
const roleGrants = new Map<string, PortalAclGrant>()
|
|
198
|
+
if (roleCandidates.length > 0) {
|
|
199
|
+
const roleAclRows = await db
|
|
200
|
+
.selectFrom('customer_user_roles')
|
|
201
|
+
.innerJoin('customer_roles', 'customer_roles.id', 'customer_user_roles.role_id')
|
|
202
|
+
.innerJoin('customer_role_acls', 'customer_role_acls.role_id', 'customer_roles.id')
|
|
203
|
+
.select([
|
|
204
|
+
'customer_user_roles.user_id as user_id',
|
|
205
|
+
'customer_role_acls.features_json as features_json',
|
|
206
|
+
'customer_role_acls.is_portal_admin as is_portal_admin',
|
|
207
|
+
])
|
|
208
|
+
.where('customer_user_roles.user_id', 'in', roleCandidates)
|
|
209
|
+
.where('customer_user_roles.deleted_at', 'is', null)
|
|
210
|
+
.where('customer_roles.tenant_id', '=', scope.tenantId)
|
|
211
|
+
.where('customer_roles.organization_id', '=', scope.organizationId)
|
|
212
|
+
.where('customer_roles.deleted_at', 'is', null)
|
|
213
|
+
.where('customer_role_acls.tenant_id', '=', scope.tenantId)
|
|
214
|
+
.execute()
|
|
215
|
+
for (const row of roleAclRows) mergeGrant(roleGrants, row.user_id, row.features_json, row.is_portal_admin)
|
|
216
|
+
}
|
|
217
|
+
return candidates.filter((userId) => {
|
|
218
|
+
const grant = userGrants.get(userId) ?? roleGrants.get(userId)
|
|
219
|
+
if (!grant) return false
|
|
220
|
+
return authorizeFeatures([requiredFeature], { grantedFeatures: grant.features, unrestricted: grant.isPortalAdmin })
|
|
221
|
+
})
|
|
222
|
+
} catch (err) {
|
|
223
|
+
if (isPortalTableMissingError(err)) return []
|
|
224
|
+
throw err
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function filterPortalRecipientsByFeature(
|
|
229
|
+
em: EntityManager,
|
|
230
|
+
userIds: string[],
|
|
231
|
+
scope: { tenantId: string; organizationId: string },
|
|
232
|
+
requiredFeature: string,
|
|
233
|
+
): Promise<string[]> {
|
|
234
|
+
try {
|
|
235
|
+
return await filterPortalRecipientsByFeatureStrict(em, userIds, scope, requiredFeature)
|
|
236
|
+
} catch (err) {
|
|
237
|
+
logger.warn('Could not resolve portal recipient features; treating the audience as empty', {
|
|
238
|
+
feature: requiredFeature,
|
|
239
|
+
err,
|
|
240
|
+
})
|
|
241
|
+
reportSwallowedRecipientError(err, 'staff.portal_recipients_feature_filter_failed')
|
|
74
242
|
return []
|
|
75
243
|
}
|
|
76
244
|
}
|