@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
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_ASSIGNMENT_GRACE_DAYS,
|
|
3
3
|
MAX_ASSIGNMENT_GRACE_DAYS,
|
|
4
|
+
TIME_TRACKING_SUPPORT_GROUP,
|
|
4
5
|
buildDefaultTimeTrackingSettings,
|
|
5
6
|
listTimeTrackingSettingKeys,
|
|
6
7
|
normalizeTimeTrackingSettingsRecord,
|
|
7
8
|
registerTimeTrackingSettingKey,
|
|
9
|
+
supportReferencePrefixSchema,
|
|
8
10
|
timeTrackingSettingKeyIds
|
|
9
11
|
} from "./settingKeys.js";
|
|
10
12
|
const STAFF_TIME_TRACKING_MODULE_ID = "staff.time_tracking";
|
|
@@ -16,6 +18,7 @@ const TIME_TRACKING_TARGETS_DAILY_HOURS_KEY = "targets.dailyHours";
|
|
|
16
18
|
const TIME_TRACKING_WARNINGS_OVERLAP_KEY = "warnings.overlap";
|
|
17
19
|
const TIME_TRACKING_WARNINGS_RUNNING_TIMER_KEY = "warnings.runningTimer";
|
|
18
20
|
const TIME_TRACKING_ACCESS_ASSIGNMENT_GRACE_DAYS_KEY = "access.assignmentGraceDays";
|
|
21
|
+
const TIME_TRACKING_SUPPORT_REFERENCE_PREFIX_KEY = "support.referencePrefix";
|
|
19
22
|
const TIME_TRACKING_SETTING_KEYS = [
|
|
20
23
|
TIME_TRACKING_ROUNDING_UNIT_MINUTES_KEY,
|
|
21
24
|
TIME_TRACKING_ROUNDING_DIRECTION_KEY,
|
|
@@ -24,7 +27,8 @@ const TIME_TRACKING_SETTING_KEYS = [
|
|
|
24
27
|
TIME_TRACKING_TARGETS_DAILY_HOURS_KEY,
|
|
25
28
|
TIME_TRACKING_WARNINGS_OVERLAP_KEY,
|
|
26
29
|
TIME_TRACKING_WARNINGS_RUNNING_TIMER_KEY,
|
|
27
|
-
TIME_TRACKING_ACCESS_ASSIGNMENT_GRACE_DAYS_KEY
|
|
30
|
+
TIME_TRACKING_ACCESS_ASSIGNMENT_GRACE_DAYS_KEY,
|
|
31
|
+
TIME_TRACKING_SUPPORT_REFERENCE_PREFIX_KEY
|
|
28
32
|
];
|
|
29
33
|
const DEFAULT_TIME_TRACKING_SETTINGS = buildDefaultTimeTrackingSettings();
|
|
30
34
|
function normalizeTimeTrackingSettings(input) {
|
|
@@ -62,6 +66,27 @@ async function writeTimeTrackingSettings(configService, scope, input) {
|
|
|
62
66
|
}
|
|
63
67
|
return settings;
|
|
64
68
|
}
|
|
69
|
+
function readSettingsGroup(input, group) {
|
|
70
|
+
const value = input[group];
|
|
71
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
async function preserveOmittedSupportSettings(configService, scope, input) {
|
|
75
|
+
const supportGroup = readSettingsGroup(input, TIME_TRACKING_SUPPORT_GROUP);
|
|
76
|
+
if (supportGroup && supportGroup.referencePrefix !== void 0) return input;
|
|
77
|
+
const record = await configService.getRecord(
|
|
78
|
+
STAFF_TIME_TRACKING_MODULE_ID,
|
|
79
|
+
TIME_TRACKING_SUPPORT_REFERENCE_PREFIX_KEY,
|
|
80
|
+
{ tenantId: scope.tenantId }
|
|
81
|
+
);
|
|
82
|
+
if (!record) return input;
|
|
83
|
+
const storedPrefix = supportReferencePrefixSchema.safeParse(record.value);
|
|
84
|
+
if (!storedPrefix.success) return input;
|
|
85
|
+
return {
|
|
86
|
+
...input,
|
|
87
|
+
[TIME_TRACKING_SUPPORT_GROUP]: { ...supportGroup ?? {}, referencePrefix: storedPrefix.data }
|
|
88
|
+
};
|
|
89
|
+
}
|
|
65
90
|
export {
|
|
66
91
|
DEFAULT_ASSIGNMENT_GRACE_DAYS,
|
|
67
92
|
DEFAULT_TIME_TRACKING_SETTINGS,
|
|
@@ -73,11 +98,13 @@ export {
|
|
|
73
98
|
TIME_TRACKING_ROUNDING_DIRECTION_KEY,
|
|
74
99
|
TIME_TRACKING_ROUNDING_UNIT_MINUTES_KEY,
|
|
75
100
|
TIME_TRACKING_SETTING_KEYS,
|
|
101
|
+
TIME_TRACKING_SUPPORT_REFERENCE_PREFIX_KEY,
|
|
76
102
|
TIME_TRACKING_TARGETS_DAILY_HOURS_KEY,
|
|
77
103
|
TIME_TRACKING_WARNINGS_OVERLAP_KEY,
|
|
78
104
|
TIME_TRACKING_WARNINGS_RUNNING_TIMER_KEY,
|
|
79
105
|
listTimeTrackingSettingKeys,
|
|
80
106
|
normalizeTimeTrackingSettings,
|
|
107
|
+
preserveOmittedSupportSettings,
|
|
81
108
|
readTimeTrackingSettingValue,
|
|
82
109
|
readTimeTrackingSettings,
|
|
83
110
|
registerTimeTrackingSettingKey,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/staff/lib/time-tracking/settings.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport type { StaffTimeTrackingSettingsInput } from '../../data/validators'\nimport {\n type RoundingDirection,\n type RoundingSettings,\n type RoundingUnitMinutes,\n} from './rounding'\nimport {\n DEFAULT_ASSIGNMENT_GRACE_DAYS,\n MAX_ASSIGNMENT_GRACE_DAYS,\n buildDefaultTimeTrackingSettings,\n listTimeTrackingSettingKeys,\n normalizeTimeTrackingSettingsRecord,\n registerTimeTrackingSettingKey,\n timeTrackingSettingKeyIds,\n type TimeTrackingSettingKeyDefinition,\n type TimeTrackingSettingKeyInput,\n type TimeTrackingSettingsRecord,\n} from './settingKeys'\n\nexport type { RoundingDirection, RoundingSettings, RoundingUnitMinutes }\nexport type { TimeTrackingSettingKeyDefinition, TimeTrackingSettingKeyInput, TimeTrackingSettingsRecord }\nexport { DEFAULT_ASSIGNMENT_GRACE_DAYS, MAX_ASSIGNMENT_GRACE_DAYS, registerTimeTrackingSettingKey }\n\nexport const STAFF_TIME_TRACKING_MODULE_ID = 'staff.time_tracking'\n\nexport const TIME_TRACKING_ROUNDING_UNIT_MINUTES_KEY = 'rounding.unitMinutes'\nexport const TIME_TRACKING_ROUNDING_DIRECTION_KEY = 'rounding.direction'\nexport const TIME_TRACKING_DEFAULTS_BILLABLE_KEY = 'defaults.billable'\nexport const TIME_TRACKING_DEFAULTS_CHAIN_START_KEY = 'defaults.chainStartFromPreviousEnd'\nexport const TIME_TRACKING_TARGETS_DAILY_HOURS_KEY = 'targets.dailyHours'\nexport const TIME_TRACKING_WARNINGS_OVERLAP_KEY = 'warnings.overlap'\nexport const TIME_TRACKING_WARNINGS_RUNNING_TIMER_KEY = 'warnings.runningTimer'\nexport const TIME_TRACKING_ACCESS_ASSIGNMENT_GRACE_DAYS_KEY = 'access.assignmentGraceDays'\n\n/**\n * The
|
|
5
|
-
"mappings": "AAOA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAMA,MAAM,gCAAgC;AAEtC,MAAM,0CAA0C;AAChD,MAAM,uCAAuC;AAC7C,MAAM,sCAAsC;AAC5C,MAAM,yCAAyC;AAC/C,MAAM,wCAAwC;AAC9C,MAAM,qCAAqC;AAC3C,MAAM,2CAA2C;AACjD,MAAM,iDAAiD;
|
|
4
|
+
"sourcesContent": ["import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport type { StaffTimeTrackingSettingsInput } from '../../data/validators'\nimport {\n type RoundingDirection,\n type RoundingSettings,\n type RoundingUnitMinutes,\n} from './rounding'\nimport {\n DEFAULT_ASSIGNMENT_GRACE_DAYS,\n MAX_ASSIGNMENT_GRACE_DAYS,\n TIME_TRACKING_SUPPORT_GROUP,\n buildDefaultTimeTrackingSettings,\n listTimeTrackingSettingKeys,\n normalizeTimeTrackingSettingsRecord,\n registerTimeTrackingSettingKey,\n supportReferencePrefixSchema,\n timeTrackingSettingKeyIds,\n type TimeTrackingSettingKeyDefinition,\n type TimeTrackingSettingKeyInput,\n type TimeTrackingSettingsRecord,\n} from './settingKeys'\n\nexport type { RoundingDirection, RoundingSettings, RoundingUnitMinutes }\nexport type { TimeTrackingSettingKeyDefinition, TimeTrackingSettingKeyInput, TimeTrackingSettingsRecord }\nexport { DEFAULT_ASSIGNMENT_GRACE_DAYS, MAX_ASSIGNMENT_GRACE_DAYS, registerTimeTrackingSettingKey }\n\nexport const STAFF_TIME_TRACKING_MODULE_ID = 'staff.time_tracking'\n\nexport const TIME_TRACKING_ROUNDING_UNIT_MINUTES_KEY = 'rounding.unitMinutes'\nexport const TIME_TRACKING_ROUNDING_DIRECTION_KEY = 'rounding.direction'\nexport const TIME_TRACKING_DEFAULTS_BILLABLE_KEY = 'defaults.billable'\nexport const TIME_TRACKING_DEFAULTS_CHAIN_START_KEY = 'defaults.chainStartFromPreviousEnd'\nexport const TIME_TRACKING_TARGETS_DAILY_HOURS_KEY = 'targets.dailyHours'\nexport const TIME_TRACKING_WARNINGS_OVERLAP_KEY = 'warnings.overlap'\nexport const TIME_TRACKING_WARNINGS_RUNNING_TIMER_KEY = 'warnings.runningTimer'\nexport const TIME_TRACKING_ACCESS_ASSIGNMENT_GRACE_DAYS_KEY = 'access.assignmentGraceDays'\nexport const TIME_TRACKING_SUPPORT_REFERENCE_PREFIX_KEY = 'support.referencePrefix'\n\n/**\n * The built-in keys the module ships. FROZEN \u2014 a stored config row is named after one\n * of these, so none may be renamed or removed. The list is no longer the whole truth:\n * `timeTrackingSettingKeyIds()` returns these plus every contributed key (EP-42).\n */\nexport const TIME_TRACKING_SETTING_KEYS = [\n TIME_TRACKING_ROUNDING_UNIT_MINUTES_KEY,\n TIME_TRACKING_ROUNDING_DIRECTION_KEY,\n TIME_TRACKING_DEFAULTS_BILLABLE_KEY,\n TIME_TRACKING_DEFAULTS_CHAIN_START_KEY,\n TIME_TRACKING_TARGETS_DAILY_HOURS_KEY,\n TIME_TRACKING_WARNINGS_OVERLAP_KEY,\n TIME_TRACKING_WARNINGS_RUNNING_TIMER_KEY,\n TIME_TRACKING_ACCESS_ASSIGNMENT_GRACE_DAYS_KEY,\n TIME_TRACKING_SUPPORT_REFERENCE_PREFIX_KEY,\n] as const\n\nexport type TimeTrackingSettingKey = (typeof TIME_TRACKING_SETTING_KEYS)[number]\n\nexport type TimeTrackingEntryDefaults = {\n billable: boolean\n chainStartFromPreviousEnd: boolean\n}\n\nexport type TimeTrackingTargets = {\n dailyHours: number | null\n}\n\nexport type TimeTrackingWarnings = {\n overlap: boolean\n runningTimer: boolean\n}\n\nexport type TimeTrackingAccess = {\n assignmentGraceDays: number\n}\n\nexport type TimeTrackingSupport = {\n referencePrefix: string\n}\n\nexport type TimeTrackingBuiltInSettings = {\n rounding: RoundingSettings\n defaults: TimeTrackingEntryDefaults\n targets: TimeTrackingTargets\n warnings: TimeTrackingWarnings\n access: TimeTrackingAccess\n /**\n * Optional in the type so code that builds a settings object without the support\n * group keeps compiling; a normalized read always carries it.\n */\n support?: TimeTrackingSupport\n}\n\n/**\n * The five built-in groups stay statically typed; a contributed group or key reaches\n * a caller through the index signature and is read with `readTimeTrackingSettingValue`.\n */\nexport type TimeTrackingSettings = TimeTrackingBuiltInSettings & TimeTrackingSettingsRecord\n\n/**\n * Settings are global per tenant (spec \u00A710 fixes them there \u2014 no per-project or\n * per-customer override), so the scope carries the tenant only and MUST be\n * derived from the authenticated context, never from request input. A contributed\n * key inherits exactly that scope.\n */\nexport type TimeTrackingSettingsScope = {\n tenantId: string\n}\n\nexport const DEFAULT_TIME_TRACKING_SETTINGS: TimeTrackingSettings =\n buildDefaultTimeTrackingSettings() as TimeTrackingSettings\n\n/**\n * Coerce an arbitrary (possibly partial or malformed) settings shape into the\n * canonical one, filling every absent or invalid value from the registered defaults.\n */\nexport function normalizeTimeTrackingSettings(\n input: Partial<TimeTrackingBuiltInSettings> | TimeTrackingSettingsRecord | StaffTimeTrackingSettingsInput | null | undefined,\n): TimeTrackingSettings {\n return normalizeTimeTrackingSettingsRecord(input) as TimeTrackingSettings\n}\n\n/** Reads one registered key out of a normalized settings object by its `<group>.<key>` id. */\nexport function readTimeTrackingSettingValue(\n settings: TimeTrackingSettings,\n id: string,\n): unknown {\n const separator = id.indexOf('.')\n if (separator <= 0) return undefined\n const group = settings[id.slice(0, separator)]\n if (!group || typeof group !== 'object') return undefined\n return (group as Record<string, unknown>)[id.slice(separator + 1)]\n}\n\nexport { listTimeTrackingSettingKeys, timeTrackingSettingKeyIds }\n\n/**\n * Resolve the tenant's time tracking settings. Absent keys (and keys holding a\n * value the schema no longer accepts) fall back to the registered default, so a\n * tenant that never opened the settings screen still reads a complete record.\n */\nexport async function readTimeTrackingSettings(\n configService: ModuleConfigService,\n scope: TimeTrackingSettingsScope,\n): Promise<TimeTrackingSettings> {\n const configScope = { tenantId: scope.tenantId }\n // `getRecord` rather than `getValue` so an explicitly stored `null`\n // (`targets.dailyHours` = \"no daily target\") stays distinguishable from an\n // absent row, which must fall back to the default.\n const stored: TimeTrackingSettingsRecord = {}\n for (const entry of listTimeTrackingSettingKeys()) {\n const record = await configService.getRecord(STAFF_TIME_TRACKING_MODULE_ID, entry.id, configScope)\n if (!record) continue\n const group = stored[entry.group] ?? {}\n group[entry.key] = record.value\n stored[entry.group] = group\n }\n return normalizeTimeTrackingSettings(stored)\n}\n\n/**\n * Persist the tenant's time tracking settings and return the stored shape.\n */\nexport async function writeTimeTrackingSettings(\n configService: ModuleConfigService,\n scope: TimeTrackingSettingsScope,\n input: Partial<TimeTrackingBuiltInSettings> | TimeTrackingSettingsRecord | StaffTimeTrackingSettingsInput,\n): Promise<TimeTrackingSettings> {\n const settings = normalizeTimeTrackingSettings(input)\n const configScope = { tenantId: scope.tenantId }\n\n for (const entry of listTimeTrackingSettingKeys()) {\n await configService.setValue(\n STAFF_TIME_TRACKING_MODULE_ID,\n entry.id,\n readTimeTrackingSettingValue(settings, entry.id),\n configScope,\n )\n }\n\n return settings\n}\n\nfunction readSettingsGroup(input: Record<string, unknown>, group: string): Record<string, unknown> | null {\n const value = input[group]\n if (!value || typeof value !== 'object' || Array.isArray(value)) return null\n return value as Record<string, unknown>\n}\n\n/**\n * A settings payload from a client that predates the support group omits it. That\n * omission keeps the tenant's stored reference prefix instead of resetting it to the\n * default, so an older settings screen cannot silently rename future ticket references.\n */\nexport async function preserveOmittedSupportSettings(\n configService: ModuleConfigService,\n scope: TimeTrackingSettingsScope,\n input: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const supportGroup = readSettingsGroup(input, TIME_TRACKING_SUPPORT_GROUP)\n if (supportGroup && supportGroup.referencePrefix !== undefined) return input\n const record = await configService.getRecord(\n STAFF_TIME_TRACKING_MODULE_ID,\n TIME_TRACKING_SUPPORT_REFERENCE_PREFIX_KEY,\n { tenantId: scope.tenantId },\n )\n if (!record) return input\n const storedPrefix = supportReferencePrefixSchema.safeParse(record.value)\n if (!storedPrefix.success) return input\n return {\n ...input,\n [TIME_TRACKING_SUPPORT_GROUP]: { ...(supportGroup ?? {}), referencePrefix: storedPrefix.data },\n }\n}\n"],
|
|
5
|
+
"mappings": "AAOA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAMA,MAAM,gCAAgC;AAEtC,MAAM,0CAA0C;AAChD,MAAM,uCAAuC;AAC7C,MAAM,sCAAsC;AAC5C,MAAM,yCAAyC;AAC/C,MAAM,wCAAwC;AAC9C,MAAM,qCAAqC;AAC3C,MAAM,2CAA2C;AACjD,MAAM,iDAAiD;AACvD,MAAM,6CAA6C;AAOnD,MAAM,6BAA6B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAuDO,MAAM,iCACX,iCAAiC;AAM5B,SAAS,8BACd,OACsB;AACtB,SAAO,oCAAoC,KAAK;AAClD;AAGO,SAAS,6BACd,UACA,IACS;AACT,QAAM,YAAY,GAAG,QAAQ,GAAG;AAChC,MAAI,aAAa,EAAG,QAAO;AAC3B,QAAM,QAAQ,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;AAC7C,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,SAAQ,MAAkC,GAAG,MAAM,YAAY,CAAC,CAAC;AACnE;AASA,eAAsB,yBACpB,eACA,OAC+B;AAC/B,QAAM,cAAc,EAAE,UAAU,MAAM,SAAS;AAI/C,QAAM,SAAqC,CAAC;AAC5C,aAAW,SAAS,4BAA4B,GAAG;AACjD,UAAM,SAAS,MAAM,cAAc,UAAU,+BAA+B,MAAM,IAAI,WAAW;AACjG,QAAI,CAAC,OAAQ;AACb,UAAM,QAAQ,OAAO,MAAM,KAAK,KAAK,CAAC;AACtC,UAAM,MAAM,GAAG,IAAI,OAAO;AAC1B,WAAO,MAAM,KAAK,IAAI;AAAA,EACxB;AACA,SAAO,8BAA8B,MAAM;AAC7C;AAKA,eAAsB,0BACpB,eACA,OACA,OAC+B;AAC/B,QAAM,WAAW,8BAA8B,KAAK;AACpD,QAAM,cAAc,EAAE,UAAU,MAAM,SAAS;AAE/C,aAAW,SAAS,4BAA4B,GAAG;AACjD,UAAM,cAAc;AAAA,MAClB;AAAA,MACA,MAAM;AAAA,MACN,6BAA6B,UAAU,MAAM,EAAE;AAAA,MAC/C;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,kBAAkB,OAAgC,OAA+C;AACxG,QAAM,QAAQ,MAAM,KAAK;AACzB,MAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,EAAG,QAAO;AACxE,SAAO;AACT;AAOA,eAAsB,+BACpB,eACA,OACA,OACkC;AAClC,QAAM,eAAe,kBAAkB,OAAO,2BAA2B;AACzE,MAAI,gBAAgB,aAAa,oBAAoB,OAAW,QAAO;AACvE,QAAM,SAAS,MAAM,cAAc;AAAA,IACjC;AAAA,IACA;AAAA,IACA,EAAE,UAAU,MAAM,SAAS;AAAA,EAC7B;AACA,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,eAAe,6BAA6B,UAAU,OAAO,KAAK;AACxE,MAAI,CAAC,aAAa,QAAS,QAAO;AAClC,SAAO;AAAA,IACL,GAAG;AAAA,IACH,CAAC,2BAA2B,GAAG,EAAE,GAAI,gBAAgB,CAAC,GAAI,iBAAiB,aAAa,KAAK;AAAA,EAC/F;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
4
|
+
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
5
|
+
import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
|
|
6
|
+
import { CrudHttpError, forbidden, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
7
|
+
import { runRouteMutationGuards } from "@open-mercato/shared/lib/crud/route-mutation-guard";
|
|
8
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
9
|
+
import { serializeOperationMetadata } from "@open-mercato/shared/lib/commands/operationMetadata";
|
|
10
|
+
import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
|
|
11
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
12
|
+
import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
|
|
13
|
+
import { enforceCommandOptimisticLockWithGuards } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
|
|
14
|
+
import { StaffTimeTask } from "../../data/entities.js";
|
|
15
|
+
import { STAFF_TIME_TRACKING_RESOURCE_KINDS } from "../../api/guards.js";
|
|
16
|
+
import {
|
|
17
|
+
staffTimeTaskAssignmentCommandIds
|
|
18
|
+
} from "../../commands/timesheets-task-assignment.js";
|
|
19
|
+
import { resolveFeatureAccess } from "./featureAccess.js";
|
|
20
|
+
import { assertProjectAccess } from "./access.js";
|
|
21
|
+
import { resolveListProjectAccess } from "./listProjectAccess.js";
|
|
22
|
+
const TASK_ASSIGNMENT_VIEW_FEATURE = "staff.timesheets.tasks.view";
|
|
23
|
+
const FAILURE_CODES = {
|
|
24
|
+
claim: "staff.task_claim_failed",
|
|
25
|
+
release: "staff.task_release_failed"
|
|
26
|
+
};
|
|
27
|
+
const taskAssignmentResponseSchema = z.object({
|
|
28
|
+
id: z.string().uuid(),
|
|
29
|
+
timeProjectId: z.string().uuid(),
|
|
30
|
+
assigneeStaffMemberId: z.string().uuid().nullable(),
|
|
31
|
+
updatedAt: z.string().nullable()
|
|
32
|
+
});
|
|
33
|
+
async function resolveTaskId(req, action, context) {
|
|
34
|
+
const params = context?.params ? await context.params : null;
|
|
35
|
+
const fromParams = params?.id;
|
|
36
|
+
if (typeof fromParams === "string" && fromParams.length > 0) return fromParams;
|
|
37
|
+
try {
|
|
38
|
+
const match = new URL(req.url).pathname.match(new RegExp(`/tasks/([^/]+)/${action}`));
|
|
39
|
+
return match?.[1] ?? null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function createTaskAssignmentPostHandler(action) {
|
|
45
|
+
const logger = createLogger("staff").child({ component: `api/timesheets/tasks/${action}` });
|
|
46
|
+
return async function POST(req, context) {
|
|
47
|
+
try {
|
|
48
|
+
const container = await createRequestContainer();
|
|
49
|
+
const auth = await getAuthFromRequest(req);
|
|
50
|
+
const { translate } = await resolveTranslations();
|
|
51
|
+
if (!auth) throw new CrudHttpError(401, { error: translate("staff.errors.unauthorized", "Unauthorized") });
|
|
52
|
+
const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req });
|
|
53
|
+
const tenantId = scope?.tenantId ?? auth.tenantId ?? null;
|
|
54
|
+
const organizationId = scope?.selectedId ?? auth.orgId ?? null;
|
|
55
|
+
if (!tenantId || !organizationId) {
|
|
56
|
+
throw new CrudHttpError(400, { error: translate("staff.errors.missingScope", "Missing tenant or organization scope.") });
|
|
57
|
+
}
|
|
58
|
+
const taskId = await resolveTaskId(req, action, context);
|
|
59
|
+
if (!taskId || !z.string().uuid().safeParse(taskId).success) {
|
|
60
|
+
throw new CrudHttpError(400, { error: translate("staff.time_tracking.tasks.errors.idRequired", "Task id is required.") });
|
|
61
|
+
}
|
|
62
|
+
const actorId = auth.sub ?? "";
|
|
63
|
+
const viewAccess = await resolveFeatureAccess(container, actorId, [TASK_ASSIGNMENT_VIEW_FEATURE], { tenantId, organizationId });
|
|
64
|
+
if (!viewAccess.allowed) throw forbidden(translate("staff.errors.forbidden", "Forbidden"));
|
|
65
|
+
const routeCtx = {
|
|
66
|
+
container,
|
|
67
|
+
auth,
|
|
68
|
+
organizationScope: scope,
|
|
69
|
+
selectedOrganizationId: organizationId,
|
|
70
|
+
organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),
|
|
71
|
+
request: req
|
|
72
|
+
};
|
|
73
|
+
const em = container.resolve("em").fork();
|
|
74
|
+
const task = await em.findOne(StaffTimeTask, { id: taskId, tenantId, organizationId, deletedAt: null });
|
|
75
|
+
const access = task ? await resolveListProjectAccess(routeCtx) : null;
|
|
76
|
+
if (!task || !access || !assertProjectAccess(access, task.timeProjectId)) {
|
|
77
|
+
throw new CrudHttpError(404, {
|
|
78
|
+
error: translate("staff.time_tracking.tasks.errors.notFound", "Task not found or not accessible.")
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
82
|
+
resourceKind: STAFF_TIME_TRACKING_RESOURCE_KINDS.timeTask,
|
|
83
|
+
resourceId: task.id,
|
|
84
|
+
current: task.updatedAt,
|
|
85
|
+
request: req
|
|
86
|
+
});
|
|
87
|
+
const input = { id: taskId };
|
|
88
|
+
const guardResult = await runRouteMutationGuards({
|
|
89
|
+
container,
|
|
90
|
+
req,
|
|
91
|
+
auth: { userId: actorId, tenantId, organizationId, userFeatures: viewAccess.grantedFeatures },
|
|
92
|
+
input: {
|
|
93
|
+
resourceKind: STAFF_TIME_TRACKING_RESOURCE_KINDS.timeTask,
|
|
94
|
+
resourceId: taskId,
|
|
95
|
+
operation: "update",
|
|
96
|
+
mutationPayload: input
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
if (!guardResult.ok) return guardResult.response;
|
|
100
|
+
const commandBus = container.resolve("commandBus");
|
|
101
|
+
const { result, logEntry } = await commandBus.execute(
|
|
102
|
+
staffTimeTaskAssignmentCommandIds[action],
|
|
103
|
+
{ input, ctx: routeCtx }
|
|
104
|
+
);
|
|
105
|
+
await guardResult.runAfterSuccess();
|
|
106
|
+
const response = NextResponse.json({
|
|
107
|
+
id: result?.taskId ?? taskId,
|
|
108
|
+
timeProjectId: result?.timeProjectId ?? task.timeProjectId,
|
|
109
|
+
assigneeStaffMemberId: result?.assigneeStaffMemberId ?? null,
|
|
110
|
+
updatedAt: result?.updatedAt ?? null
|
|
111
|
+
});
|
|
112
|
+
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
113
|
+
response.headers.set(
|
|
114
|
+
"x-om-operation",
|
|
115
|
+
serializeOperationMetadata({
|
|
116
|
+
id: logEntry.id,
|
|
117
|
+
undoToken: logEntry.undoToken,
|
|
118
|
+
commandId: logEntry.commandId,
|
|
119
|
+
actionLabel: logEntry.actionLabel ?? null,
|
|
120
|
+
resourceKind: logEntry.resourceKind ?? STAFF_TIME_TRACKING_RESOURCE_KINDS.timeTask,
|
|
121
|
+
resourceId: logEntry.resourceId ?? taskId,
|
|
122
|
+
executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : void 0
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return response;
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status });
|
|
129
|
+
const interceptorRejection = getCommandInterceptorHttpRejection(err);
|
|
130
|
+
if (interceptorRejection) return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status });
|
|
131
|
+
const { translate } = await resolveTranslations();
|
|
132
|
+
logger.error(`staff.timesheets.tasks.${action} POST failed`, { err });
|
|
133
|
+
getTelemetryRuntime()?.reportError(err, { module: "staff", code: FAILURE_CODES[action] });
|
|
134
|
+
return NextResponse.json({ error: translate("staff.errors.internal", "Internal server error") }, { status: 500 });
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export {
|
|
139
|
+
TASK_ASSIGNMENT_VIEW_FEATURE,
|
|
140
|
+
createTaskAssignmentPostHandler,
|
|
141
|
+
taskAssignmentResponseSchema
|
|
142
|
+
};
|
|
143
|
+
//# sourceMappingURL=taskAssignmentRoute.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/staff/lib/time-tracking/taskAssignmentRoute.ts"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { CrudHttpError, forbidden, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { StaffTimeTask } from '../../data/entities'\nimport { STAFF_TIME_TRACKING_RESOURCE_KINDS } from '../../api/guards'\nimport {\n staffTimeTaskAssignmentCommandIds,\n type StaffTimeTaskAssignmentInput,\n type StaffTimeTaskAssignmentResult,\n} from '../../commands/timesheets-task-assignment'\nimport { resolveFeatureAccess } from './featureAccess'\nimport { assertProjectAccess } from './access'\nimport { resolveListProjectAccess } from './listProjectAccess'\n\nexport type TaskAssignmentAction = keyof typeof staffTimeTaskAssignmentCommandIds\n\nexport const TASK_ASSIGNMENT_VIEW_FEATURE = 'staff.timesheets.tasks.view'\n\nconst FAILURE_CODES: Record<TaskAssignmentAction, string> = {\n claim: 'staff.task_claim_failed',\n release: 'staff.task_release_failed',\n}\n\nexport type TaskAssignmentRouteContext = {\n params?: Promise<Record<string, string | string[]>> | Record<string, string | string[]>\n}\n\nexport const taskAssignmentResponseSchema = z.object({\n id: z.string().uuid(),\n timeProjectId: z.string().uuid(),\n assigneeStaffMemberId: z.string().uuid().nullable(),\n updatedAt: z.string().nullable(),\n})\n\nasync function resolveTaskId(req: Request, action: TaskAssignmentAction, context?: TaskAssignmentRouteContext): Promise<string | null> {\n const params = context?.params ? await context.params : null\n const fromParams = params?.id\n if (typeof fromParams === 'string' && fromParams.length > 0) return fromParams\n try {\n const match = new URL(req.url).pathname.match(new RegExp(`/tasks/([^/]+)/${action}`))\n return match?.[1] ?? null\n } catch {\n return null\n }\n}\n\n/**\n * The `POST /api/staff/timesheets/tasks/{id}/claim|release` handler. Both actions\n * share every step \u2014 scope, project access (the tasks list rule), optimistic lock,\n * mutation guards, the command and the undo header \u2014 and differ only in the command.\n */\nexport function createTaskAssignmentPostHandler(action: TaskAssignmentAction) {\n const logger = createLogger('staff').child({ component: `api/timesheets/tasks/${action}` })\n return async function POST(req: Request, context?: TaskAssignmentRouteContext): Promise<Response> {\n try {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth) throw new CrudHttpError(401, { error: translate('staff.errors.unauthorized', 'Unauthorized') })\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const tenantId = scope?.tenantId ?? auth.tenantId ?? null\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!tenantId || !organizationId) {\n throw new CrudHttpError(400, { error: translate('staff.errors.missingScope', 'Missing tenant or organization scope.') })\n }\n const taskId = await resolveTaskId(req, action, context)\n if (!taskId || !z.string().uuid().safeParse(taskId).success) {\n throw new CrudHttpError(400, { error: translate('staff.time_tracking.tasks.errors.idRequired', 'Task id is required.') })\n }\n\n const actorId = auth.sub ?? ''\n const viewAccess = await resolveFeatureAccess(container, actorId, [TASK_ASSIGNMENT_VIEW_FEATURE], { tenantId, organizationId })\n if (!viewAccess.allowed) throw forbidden(translate('staff.errors.forbidden', 'Forbidden'))\n\n const routeCtx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n const em = (container.resolve('em') as EntityManager).fork()\n const task = await em.findOne(StaffTimeTask, { id: taskId, tenantId, organizationId, deletedAt: null })\n const access = task ? await resolveListProjectAccess(routeCtx) : null\n if (!task || !access || !assertProjectAccess(access, task.timeProjectId)) {\n throw new CrudHttpError(404, {\n error: translate('staff.time_tracking.tasks.errors.notFound', 'Task not found or not accessible.'),\n })\n }\n\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: STAFF_TIME_TRACKING_RESOURCE_KINDS.timeTask,\n resourceId: task.id,\n current: task.updatedAt,\n request: req,\n })\n\n const input: StaffTimeTaskAssignmentInput = { id: taskId }\n const guardResult = await runRouteMutationGuards({\n container,\n req,\n auth: { userId: actorId, tenantId, organizationId, userFeatures: viewAccess.grantedFeatures },\n input: {\n resourceKind: STAFF_TIME_TRACKING_RESOURCE_KINDS.timeTask,\n resourceId: taskId,\n operation: 'update',\n mutationPayload: input,\n },\n })\n if (!guardResult.ok) return guardResult.response\n\n const commandBus = container.resolve('commandBus') as CommandBus\n const { result, logEntry } = await commandBus.execute<StaffTimeTaskAssignmentInput, StaffTimeTaskAssignmentResult>(\n staffTimeTaskAssignmentCommandIds[action],\n { input, ctx: routeCtx },\n )\n await guardResult.runAfterSuccess()\n\n const response = NextResponse.json({\n id: result?.taskId ?? taskId,\n timeProjectId: result?.timeProjectId ?? task.timeProjectId,\n assigneeStaffMemberId: result?.assigneeStaffMemberId ?? null,\n updatedAt: result?.updatedAt ?? null,\n })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? STAFF_TIME_TRACKING_RESOURCE_KINDS.timeTask,\n resourceId: logEntry.resourceId ?? taskId,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n }),\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n const { translate } = await resolveTranslations()\n logger.error(`staff.timesheets.tasks.${action} POST failed`, { err })\n getTelemetryRuntime()?.reportError(err, { module: 'staff', code: FAILURE_CODES[action] })\n return NextResponse.json({ error: translate('staff.errors.internal', 'Internal server error') }, { status: 500 })\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
2
|
+
import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
|
|
3
|
+
import { resolveNotificationService } from "../../../notifications/lib/notificationService.js";
|
|
4
|
+
import {
|
|
5
|
+
buildBatchNotificationFromType,
|
|
6
|
+
buildFeatureNotificationFromType
|
|
7
|
+
} from "../../../notifications/lib/notificationBuilder.js";
|
|
8
|
+
import { StaffTimeProject } from "../../data/entities.js";
|
|
9
|
+
import { loadActiveProjectMembers } from "./projectAssignment.js";
|
|
10
|
+
const logger = createLogger("staff").child({ component: "lib/time-tracking/workRecipients" });
|
|
11
|
+
const PROJECT_MANAGERS_FEATURE = "staff.timesheets.projects.manage";
|
|
12
|
+
function uniqueExcluding(ids, actorUserId) {
|
|
13
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14
|
+
for (const id of ids) {
|
|
15
|
+
if (typeof id !== "string" || id.length === 0) continue;
|
|
16
|
+
if (actorUserId && id === actorUserId) continue;
|
|
17
|
+
seen.add(id);
|
|
18
|
+
}
|
|
19
|
+
return [...seen];
|
|
20
|
+
}
|
|
21
|
+
async function resolveProjectWorkRecipients(em, scope, input) {
|
|
22
|
+
const actorUserId = input.actorUserId ?? null;
|
|
23
|
+
const audience = uniqueExcluding(input.audienceUserIds ?? [], actorUserId);
|
|
24
|
+
if (audience.length > 0) return { step: 1, userIds: audience };
|
|
25
|
+
const assigners = await loadActiveProjectMembers(em, scope, input.timeProjectId, {
|
|
26
|
+
graceDays: input.graceDays,
|
|
27
|
+
now: input.now,
|
|
28
|
+
assignersOnly: true
|
|
29
|
+
});
|
|
30
|
+
const assignerUserIds = uniqueExcluding(assigners.map((member) => member.userId), actorUserId);
|
|
31
|
+
if (assignerUserIds.length > 0) return { step: 2, userIds: assignerUserIds };
|
|
32
|
+
const project = await em.findOne(
|
|
33
|
+
StaffTimeProject,
|
|
34
|
+
{ id: input.timeProjectId, tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
35
|
+
{ fields: ["id", "ownerUserId"] }
|
|
36
|
+
);
|
|
37
|
+
const owner = uniqueExcluding([project?.ownerUserId], actorUserId);
|
|
38
|
+
if (owner.length > 0) return { step: 3, userIds: owner };
|
|
39
|
+
return { step: 4, userIds: [] };
|
|
40
|
+
}
|
|
41
|
+
async function loadActiveAssignerUserIds(em, scope, input) {
|
|
42
|
+
const assigners = await loadActiveProjectMembers(em, scope, input.timeProjectId, {
|
|
43
|
+
graceDays: input.graceDays,
|
|
44
|
+
now: input.now,
|
|
45
|
+
assignersOnly: true
|
|
46
|
+
});
|
|
47
|
+
return uniqueExcluding(assigners.map((member) => member.userId), null);
|
|
48
|
+
}
|
|
49
|
+
async function projectReachesRecipientBeforeManagers(em, scope, project, options = {}) {
|
|
50
|
+
if (typeof project.ownerUserId === "string" && project.ownerUserId.length > 0) return true;
|
|
51
|
+
const members = await loadActiveProjectMembers(em, scope, project.id, options);
|
|
52
|
+
const hasUser = (member) => typeof member.userId === "string" && member.userId.length > 0;
|
|
53
|
+
if (members.some((member) => member.isAssigner && hasUser(member))) return true;
|
|
54
|
+
const defaultAssigneeId = project.defaultAssigneeStaffMemberId ?? null;
|
|
55
|
+
if (!defaultAssigneeId) return false;
|
|
56
|
+
return members.some((member) => member.staffMemberId === defaultAssigneeId && hasUser(member));
|
|
57
|
+
}
|
|
58
|
+
async function notifyProjectWork(container, scope, options) {
|
|
59
|
+
const notificationService = resolveNotificationService(container);
|
|
60
|
+
const context = { tenantId: scope.tenantId, organizationId: scope.organizationId };
|
|
61
|
+
const { typeDef, recipients, build } = options;
|
|
62
|
+
if (recipients.step === 4) {
|
|
63
|
+
const created = await notificationService.createForFeature(
|
|
64
|
+
{
|
|
65
|
+
...buildFeatureNotificationFromType(typeDef, { ...build, requiredFeature: PROJECT_MANAGERS_FEATURE }),
|
|
66
|
+
restrictRecipientsToOrganization: true
|
|
67
|
+
},
|
|
68
|
+
context
|
|
69
|
+
);
|
|
70
|
+
return { step: 4, notifiedCount: created.length };
|
|
71
|
+
}
|
|
72
|
+
if (recipients.userIds.length === 0) return { step: recipients.step, notifiedCount: 0 };
|
|
73
|
+
try {
|
|
74
|
+
const created = await notificationService.createBatch(
|
|
75
|
+
buildBatchNotificationFromType(typeDef, { ...build, recipientUserIds: recipients.userIds }),
|
|
76
|
+
context
|
|
77
|
+
);
|
|
78
|
+
return { step: recipients.step, notifiedCount: created.length };
|
|
79
|
+
} catch (err) {
|
|
80
|
+
logger.error("staff project-work notification failed", { err, type: typeDef.type, step: recipients.step });
|
|
81
|
+
getTelemetryRuntime()?.reportError(err, { module: "staff", code: "staff.project_work_notification_failed" });
|
|
82
|
+
return { step: recipients.step, notifiedCount: 0 };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
PROJECT_MANAGERS_FEATURE,
|
|
87
|
+
loadActiveAssignerUserIds,
|
|
88
|
+
notifyProjectWork,
|
|
89
|
+
projectReachesRecipientBeforeManagers,
|
|
90
|
+
resolveProjectWorkRecipients
|
|
91
|
+
};
|
|
92
|
+
//# sourceMappingURL=workRecipients.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/staff/lib/time-tracking/workRecipients.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { NotificationTypeDefinition } from '@open-mercato/shared/modules/notifications/types'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { resolveNotificationService } from '../../../notifications/lib/notificationService'\nimport {\n buildBatchNotificationFromType,\n buildFeatureNotificationFromType,\n} from '../../../notifications/lib/notificationBuilder'\nimport { StaffTimeProject } from '../../data/entities'\nimport { loadActiveProjectMembers, type ProjectAssignmentScope } from './projectAssignment'\n\nconst logger = createLogger('staff').child({ component: 'lib/time-tracking/workRecipients' })\n\nexport const PROJECT_MANAGERS_FEATURE = 'staff.timesheets.projects.manage'\n\nexport type WorkRecipientStep = 1 | 2 | 3 | 4\n\nexport type ResolvedWorkRecipients =\n | { step: 1 | 2 | 3; userIds: string[] }\n | { step: 4; userIds: [] }\n\nexport type WorkRecipientsInput = {\n timeProjectId: string\n /** Step 1: the type's own audience. Empty or omitted starts the chain at step 2. */\n audienceUserIds?: readonly (string | null | undefined)[]\n /** Excluded from steps 1\u20133. Step 4 hands resolution to `createForFeature`, which cannot exclude a user. */\n actorUserId?: string | null\n graceDays?: number | null\n now?: Date\n}\n\nfunction uniqueExcluding(ids: readonly (string | null | undefined)[], actorUserId: string | null): string[] {\n const seen = new Set<string>()\n for (const id of ids) {\n if (typeof id !== 'string' || id.length === 0) continue\n if (actorUserId && id === actorUserId) continue\n seen.add(id)\n }\n return [...seen]\n}\n\n/**\n * The escalation chain shared by every project-work notification: the type's own\n * audience, then the project's active assigners, then the project owner, then the\n * organization's project managers. Each step is used only when the one before it\n * is empty. Steps 1\u20133 are resolved here; step 4 is returned as a marker for\n * `notifyProjectWork`, which sends it through `createForFeature`.\n */\nexport async function resolveProjectWorkRecipients(\n em: EntityManager,\n scope: ProjectAssignmentScope,\n input: WorkRecipientsInput,\n): Promise<ResolvedWorkRecipients> {\n const actorUserId = input.actorUserId ?? null\n const audience = uniqueExcluding(input.audienceUserIds ?? [], actorUserId)\n if (audience.length > 0) return { step: 1, userIds: audience }\n\n const assigners = await loadActiveProjectMembers(em, scope, input.timeProjectId, {\n graceDays: input.graceDays,\n now: input.now,\n assignersOnly: true,\n })\n const assignerUserIds = uniqueExcluding(assigners.map((member) => member.userId), actorUserId)\n if (assignerUserIds.length > 0) return { step: 2, userIds: assignerUserIds }\n\n const project = await em.findOne(\n StaffTimeProject,\n { id: input.timeProjectId, tenantId: scope.tenantId, organizationId: scope.organizationId },\n { fields: ['id', 'ownerUserId'] },\n )\n const owner = uniqueExcluding([project?.ownerUserId], actorUserId)\n if (owner.length > 0) return { step: 3, userIds: owner }\n\n return { step: 4, userIds: [] }\n}\n\nexport type ActiveAssignerUserIdsInput = {\n timeProjectId: string\n graceDays?: number | null\n now?: Date\n}\n\nexport async function loadActiveAssignerUserIds(\n em: EntityManager,\n scope: ProjectAssignmentScope,\n input: ActiveAssignerUserIdsInput,\n): Promise<string[]> {\n const assigners = await loadActiveProjectMembers(em, scope, input.timeProjectId, {\n graceDays: input.graceDays,\n now: input.now,\n assignersOnly: true,\n })\n return uniqueExcluding(assigners.map((member) => member.userId), null)\n}\n\nexport type ProjectRecipientProbe = {\n id: string\n ownerUserId?: string | null\n defaultAssigneeStaffMemberId?: string | null\n}\n\n/**\n * Whether new project work reaches somebody before the chain falls through to the\n * organization's project managers (step 4): an active assigner with a user account,\n * a usable default assignee with a user account (the ticket's step-1 audience), or\n * the project owner. `false` is the state the support routing settings warn about.\n */\nexport async function projectReachesRecipientBeforeManagers(\n em: EntityManager,\n scope: ProjectAssignmentScope,\n project: ProjectRecipientProbe,\n options: { graceDays?: number | null; now?: Date } = {},\n): Promise<boolean> {\n if (typeof project.ownerUserId === 'string' && project.ownerUserId.length > 0) return true\n const members = await loadActiveProjectMembers(em, scope, project.id, options)\n const hasUser = (member: { userId: string | null }) => typeof member.userId === 'string' && member.userId.length > 0\n if (members.some((member) => member.isAssigner && hasUser(member))) return true\n const defaultAssigneeId = project.defaultAssigneeStaffMemberId ?? null\n if (!defaultAssigneeId) return false\n return members.some((member) => member.staffMemberId === defaultAssigneeId && hasUser(member))\n}\n\ntype ContainerLike = { resolve: <T = unknown>(name: string) => T }\n\nexport type NotifyProjectWorkOptions = {\n typeDef: NotificationTypeDefinition\n recipients: ResolvedWorkRecipients\n build: {\n titleVariables?: Record<string, string>\n bodyVariables?: Record<string, string>\n sourceEntityType?: string\n sourceEntityId?: string\n linkHref?: string\n groupKey?: string\n }\n}\n\nexport type NotifyProjectWorkResult = {\n step: WorkRecipientStep\n notifiedCount: number\n}\n\n/**\n * Sends one notification to the resolved recipients. Step 4 always passes\n * `restrictRecipientsToOrganization: true`: without it `createForFeature` notifies\n * every holder of the feature in the whole tenant. With the flag on, the service\n * returns no recipients (and logs) when the organization is missing, more than 200\n * tenant-wide candidates hold the feature, the container is unavailable, the RBAC\n * check throws, or nobody in the organization holds it; `notifiedCount` is then 0\n * and the caller decides what \"nobody notified\" means for its record.\n */\nexport async function notifyProjectWork(\n container: ContainerLike,\n scope: ProjectAssignmentScope,\n options: NotifyProjectWorkOptions,\n): Promise<NotifyProjectWorkResult> {\n const notificationService = resolveNotificationService(container)\n const context = { tenantId: scope.tenantId, organizationId: scope.organizationId }\n const { typeDef, recipients, build } = options\n if (recipients.step === 4) {\n const created = await notificationService.createForFeature(\n {\n ...buildFeatureNotificationFromType(typeDef, { ...build, requiredFeature: PROJECT_MANAGERS_FEATURE }),\n restrictRecipientsToOrganization: true,\n },\n context,\n )\n return { step: 4, notifiedCount: created.length }\n }\n if (recipients.userIds.length === 0) return { step: recipients.step, notifiedCount: 0 }\n try {\n const created = await notificationService.createBatch(\n buildBatchNotificationFromType(typeDef, { ...build, recipientUserIds: recipients.userIds }),\n context,\n )\n return { step: recipients.step, notifiedCount: created.length }\n } catch (err) {\n logger.error('staff project-work notification failed', { err, type: typeDef.type, step: recipients.step })\n getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.project_work_notification_failed' })\n return { step: recipients.step, notifiedCount: 0 }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -10,12 +10,14 @@ import {
|
|
|
10
10
|
useAssigneeNames,
|
|
11
11
|
useStaffMemberDirectory,
|
|
12
12
|
useTagDirectory,
|
|
13
|
-
useTagLabels
|
|
13
|
+
useTagLabels,
|
|
14
|
+
useUnassignedTaskCount
|
|
14
15
|
} from "./boardDirectory.js";
|
|
15
16
|
import {
|
|
16
17
|
countActiveBoardFilters,
|
|
17
18
|
withAssigneeFilter,
|
|
18
19
|
withStatusFilter,
|
|
20
|
+
withUnassignedFilter,
|
|
19
21
|
withTagFilter,
|
|
20
22
|
withoutTagFilter
|
|
21
23
|
} from "./boardFilters.js";
|
|
@@ -78,6 +80,7 @@ function AddBoardFilterPopover({
|
|
|
78
80
|
onChange,
|
|
79
81
|
self,
|
|
80
82
|
statuses,
|
|
83
|
+
timeProjectId,
|
|
81
84
|
trigger,
|
|
82
85
|
testId = "board-filter-add"
|
|
83
86
|
}) {
|
|
@@ -91,6 +94,8 @@ function AddBoardFilterPopover({
|
|
|
91
94
|
[memberDirectory.data, self?.id]
|
|
92
95
|
);
|
|
93
96
|
const assignedToMeLabel = t("staff.time_tracking.board.filters.assignedToMe", "Assigned to me");
|
|
97
|
+
const unassignedCount = useUnassignedTaskCount(timeProjectId);
|
|
98
|
+
const unassignedLabel = typeof unassignedCount.data === "number" ? t("staff.time_tracking.board.filters.unassignedWithCount", "Unassigned ({count})", { count: unassignedCount.data }) : t("staff.time_tracking.board.filters.unassigned", "Unassigned");
|
|
94
99
|
return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
|
|
95
100
|
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: trigger }),
|
|
96
101
|
/* @__PURE__ */ jsx(PopoverContent, { align: "start", className: "w-72 p-0", "data-testid": `${testId}-popover`, children: /* @__PURE__ */ jsxs("div", { className: "max-h-96 overflow-y-auto p-2", children: [
|
|
@@ -112,6 +117,18 @@ function AddBoardFilterPopover({
|
|
|
112
117
|
}
|
|
113
118
|
}
|
|
114
119
|
) : null,
|
|
120
|
+
/* @__PURE__ */ jsx(
|
|
121
|
+
PickerRow,
|
|
122
|
+
{
|
|
123
|
+
label: unassignedLabel,
|
|
124
|
+
selected: filters.unassigned,
|
|
125
|
+
testId: "board-filter-unassigned",
|
|
126
|
+
onSelect: () => {
|
|
127
|
+
onChange(withUnassignedFilter(filters, !filters.unassigned));
|
|
128
|
+
setOpen(false);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
),
|
|
115
132
|
otherMembers.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-col gap-0.5", children: [
|
|
116
133
|
/* @__PURE__ */ jsx("span", { className: "px-2.5 py-1 text-xs font-semibold uppercase leading-normal tracking-wide text-muted-foreground", children: t("staff.time_tracking.board.filters.assignee", "Assignee") }),
|
|
117
134
|
otherMembers.map((member) => /* @__PURE__ */ jsx(
|
|
@@ -184,9 +201,11 @@ function BoardFilterChips({
|
|
|
184
201
|
filters,
|
|
185
202
|
onChange,
|
|
186
203
|
self,
|
|
187
|
-
statuses
|
|
204
|
+
statuses,
|
|
205
|
+
timeProjectId
|
|
188
206
|
}) {
|
|
189
207
|
const t = useT();
|
|
208
|
+
const unassignedCount = useUnassignedTaskCount(filters.unassigned ? timeProjectId : null);
|
|
190
209
|
const assigneeIds = React.useMemo(
|
|
191
210
|
() => filters.assigneeStaffMemberId && filters.assigneeStaffMemberId !== self?.id ? [filters.assigneeStaffMemberId] : [],
|
|
192
211
|
[filters.assigneeStaffMemberId, self?.id]
|
|
@@ -215,6 +234,15 @@ function BoardFilterChips({
|
|
|
215
234
|
onRemove: () => onChange(withAssigneeFilter(filters, null))
|
|
216
235
|
}
|
|
217
236
|
) : null,
|
|
237
|
+
filters.unassigned ? /* @__PURE__ */ jsx(
|
|
238
|
+
FilterChip,
|
|
239
|
+
{
|
|
240
|
+
label: typeof unassignedCount.data === "number" ? t("staff.time_tracking.board.filters.unassignedWithCount", "Unassigned ({count})", { count: unassignedCount.data }) : t("staff.time_tracking.board.filters.unassigned", "Unassigned"),
|
|
241
|
+
removeLabel: removeLabel(t("staff.time_tracking.board.filters.unassigned", "Unassigned")),
|
|
242
|
+
testId: "board-filter-chip-unassigned",
|
|
243
|
+
onRemove: () => onChange(withUnassignedFilter(filters, false))
|
|
244
|
+
}
|
|
245
|
+
) : null,
|
|
218
246
|
activeStatus ? /* @__PURE__ */ jsx(
|
|
219
247
|
FilterChip,
|
|
220
248
|
{
|
|
@@ -244,6 +272,7 @@ function BoardFilterChips({
|
|
|
244
272
|
onChange,
|
|
245
273
|
self,
|
|
246
274
|
statuses,
|
|
275
|
+
timeProjectId,
|
|
247
276
|
trigger: /* @__PURE__ */ jsxs(
|
|
248
277
|
Button,
|
|
249
278
|
{
|
|
@@ -266,7 +295,7 @@ function BoardFilterChips({
|
|
|
266
295
|
type: "button",
|
|
267
296
|
variant: "link",
|
|
268
297
|
size: "2xs",
|
|
269
|
-
onClick: () => onChange({ assigneeStaffMemberId: null, taskStatusId: null, tagIds: [] }),
|
|
298
|
+
onClick: () => onChange({ assigneeStaffMemberId: null, taskStatusId: null, tagIds: [], unassigned: false }),
|
|
270
299
|
"data-testid": "board-filter-clear",
|
|
271
300
|
className: "h-auto px-1 text-xs leading-normal text-muted-foreground hover:text-foreground",
|
|
272
301
|
children: t("staff.time_tracking.board.filters.clear", "Clear filters")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/staff/lib/time-tracking-ui/BoardFilterChips.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Check, Plus, X } from 'lucide-react'\nimport { Avatar } from '@open-mercato/ui/primitives/avatar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n useAssigneeNames,\n useStaffMemberDirectory,\n useTagDirectory,\n useTagLabels,\n type BoardSelf,\n} from './boardDirectory'\nimport {\n countActiveBoardFilters,\n withAssigneeFilter,\n withStatusFilter,\n withTagFilter,\n withoutTagFilter,\n type BoardFilters,\n} from './boardFilters'\nimport type { BoardStatus } from './kanbanBoardData'\n\n/**\n * The `dt-chips` row of screen 6 and the picker behind it.\n *\n * Modelled on the deals pipeline filter surfaces (`customers/.../pipeline/components`):\n * a popover-backed picker, chips that carry their own removal, one idiom for the whole\n * product. The difference the mockup asks for is the *shape* of the row \u2014 the pipeline\n * shows one chip per filter kind with its current value, screen 6 shows one chip per\n * active filter plus a dashed \"Dodaj filtr\" \u2014 so the chrome is shared in spirit rather\n * than by importing a component out of another module's page folder.\n *\n * \"Przypisane do mnie\" is the first entry in the picker and one click: it is the filter\n * an operator reaches for far more often than any other, so it does not sit behind a\n * person search.\n */\n\ntype BoardFilterSurfaceProps = {\n filters: BoardFilters\n onChange: (next: BoardFilters) => void\n self: BoardSelf | null\n statuses: readonly BoardStatus[]\n}\n\nfunction chipClassName(dashed: boolean): string {\n const base =\n 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs leading-normal text-foreground'\n return dashed\n ? `${base} border border-dashed border-muted-foreground/60 bg-transparent text-muted-foreground hover:bg-muted/60`\n : `${base} border border-border bg-card`\n}\n\nfunction FilterChip({\n label,\n avatarLabel,\n removeLabel,\n onRemove,\n testId,\n}: {\n label: string\n avatarLabel?: string | null\n removeLabel: string\n onRemove: () => void\n testId: string\n}): React.ReactElement {\n return (\n <span className={chipClassName(false)} data-testid={testId}>\n {avatarLabel ? <Avatar size=\"xs\" label={avatarLabel} variant=\"monochrome\" /> : null}\n <span className=\"max-w-48 truncate font-semibold\">{label}</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"2xs\"\n onClick={onRemove}\n aria-label={removeLabel}\n data-testid={`${testId}-remove`}\n className=\"size-4 rounded-full p-0 text-muted-foreground hover:text-foreground\"\n >\n <X className=\"size-3\" aria-hidden=\"true\" />\n </Button>\n </span>\n )\n}\n\nfunction PickerRow({\n label,\n selected,\n avatarLabel,\n onSelect,\n testId,\n}: {\n label: string\n selected: boolean\n avatarLabel?: string | null\n onSelect: () => void\n testId: string\n}): React.ReactElement {\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n role=\"checkbox\"\n aria-checked={selected}\n onClick={onSelect}\n data-testid={testId}\n className={`h-auto w-full justify-start gap-2.5 rounded-md px-2.5 py-2 text-left text-sm font-normal ${\n selected ? 'bg-muted/60' : 'bg-transparent'\n }`}\n >\n {avatarLabel ? <Avatar size=\"xs\" label={avatarLabel} variant=\"monochrome\" /> : null}\n <span className=\"truncate text-foreground\">{label}</span>\n {selected ? <Check className=\"ml-auto size-3.5 text-muted-foreground\" aria-hidden=\"true\" /> : null}\n </Button>\n )\n}\n\nexport function AddBoardFilterPopover({\n filters,\n onChange,\n self,\n statuses,\n trigger,\n testId = 'board-filter-add',\n}: BoardFilterSurfaceProps & {\n trigger: React.ReactNode\n testId?: string\n}): React.ReactElement {\n const t = useT()\n const [open, setOpen] = React.useState(false)\n const tagDirectory = useTagDirectory(open)\n const memberDirectory = useStaffMemberDirectory(open)\n\n const tags = tagDirectory.data ?? []\n const otherMembers = React.useMemo(\n () => (memberDirectory.data ?? []).filter((member) => member.id !== self?.id),\n [memberDirectory.data, self?.id],\n )\n\n const assignedToMeLabel = t('staff.time_tracking.board.filters.assignedToMe', 'Assigned to me')\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-72 p-0\" data-testid={`${testId}-popover`}>\n <div className=\"max-h-96 overflow-y-auto p-2\">\n {self ? (\n <PickerRow\n label={assignedToMeLabel}\n avatarLabel={self.displayName ?? assignedToMeLabel}\n selected={filters.assigneeStaffMemberId === self.id}\n testId=\"board-filter-assigned-to-me\"\n onSelect={() => {\n onChange(\n withAssigneeFilter(\n filters,\n filters.assigneeStaffMemberId === self.id ? null : self.id,\n ),\n )\n setOpen(false)\n }}\n />\n ) : null}\n\n {otherMembers.length > 0 ? (\n <div className=\"mt-2 flex flex-col gap-0.5\">\n <span className=\"px-2.5 py-1 text-xs font-semibold uppercase leading-normal tracking-wide text-muted-foreground\">\n {t('staff.time_tracking.board.filters.assignee', 'Assignee')}\n </span>\n {otherMembers.map((member) => (\n <PickerRow\n key={member.id}\n label={member.displayName}\n avatarLabel={member.displayName}\n selected={filters.assigneeStaffMemberId === member.id}\n testId={`board-filter-assignee-${member.id}`}\n onSelect={() => {\n onChange(\n withAssigneeFilter(\n filters,\n filters.assigneeStaffMemberId === member.id ? null : member.id,\n ),\n )\n setOpen(false)\n }}\n />\n ))}\n </div>\n ) : null}\n\n {statuses.length > 0 ? (\n <div className=\"mt-2 flex flex-col gap-0.5\">\n <span className=\"px-2.5 py-1 text-xs font-semibold uppercase leading-normal tracking-wide text-muted-foreground\">\n {t('staff.time_tracking.board.filters.status', 'Status')}\n </span>\n {statuses.map((status) => (\n <PickerRow\n key={status.id}\n label={status.name}\n selected={filters.taskStatusId === status.id}\n testId={`board-filter-status-${status.id}`}\n onSelect={() => {\n onChange(\n withStatusFilter(\n filters,\n filters.taskStatusId === status.id ? null : status.id,\n ),\n )\n setOpen(false)\n }}\n />\n ))}\n </div>\n ) : null}\n\n {tags.length > 0 ? (\n <div className=\"mt-2 flex flex-col gap-0.5\">\n <span className=\"px-2.5 py-1 text-xs font-semibold uppercase leading-normal tracking-wide text-muted-foreground\">\n {t('staff.time_tracking.board.filters.tag', 'Tag')}\n </span>\n {tags.map((tag) => {\n const selected = filters.tagIds.includes(tag.id)\n return (\n <PickerRow\n key={tag.id}\n label={tag.label}\n selected={selected}\n testId={`board-filter-tag-${tag.id}`}\n onSelect={() => {\n onChange(\n selected ? withoutTagFilter(filters, tag.id) : withTagFilter(filters, tag.id),\n )\n setOpen(false)\n }}\n />\n )\n })}\n </div>\n ) : null}\n\n {!self && otherMembers.length === 0 && statuses.length === 0 && tags.length === 0 ? (\n <p className=\"px-2.5 py-6 text-center text-xs leading-normal text-muted-foreground\">\n {t('staff.time_tracking.board.filters.noOptions', 'Nothing to filter by yet.')}\n </p>\n ) : null}\n </div>\n </PopoverContent>\n </Popover>\n )\n}\n\nexport function BoardFilterChips({\n filters,\n onChange,\n self,\n statuses,\n}: BoardFilterSurfaceProps): React.ReactElement {\n const t = useT()\n\n const assigneeIds = React.useMemo(\n () =>\n filters.assigneeStaffMemberId && filters.assigneeStaffMemberId !== self?.id\n ? [filters.assigneeStaffMemberId]\n : [],\n [filters.assigneeStaffMemberId, self?.id],\n )\n const assigneeNames = useAssigneeNames(assigneeIds)\n const tagLabels = useTagLabels(filters.tagIds)\n\n const activeStatus = React.useMemo(\n () => statuses.find((status) => status.id === filters.taskStatusId) ?? null,\n [filters.taskStatusId, statuses],\n )\n\n const assignedToMeLabel = t('staff.time_tracking.board.filters.assignedToMe', 'Assigned to me')\n const isSelf = !!filters.assigneeStaffMemberId && filters.assigneeStaffMemberId === self?.id\n const assigneeLabel = isSelf\n ? assignedToMeLabel\n : filters.assigneeStaffMemberId\n ? assigneeNames.data?.get(filters.assigneeStaffMemberId) ?? filters.assigneeStaffMemberId\n : null\n\n const removeLabel = React.useCallback(\n (label: string) =>\n t('staff.time_tracking.board.filters.remove', 'Remove filter: {label}', { label }),\n [t],\n )\n\n return (\n <div className=\"flex flex-wrap items-center gap-2\" data-testid=\"board-filter-chips\">\n {assigneeLabel ? (\n <FilterChip\n label={assigneeLabel}\n avatarLabel={isSelf ? self?.displayName ?? assignedToMeLabel : assigneeLabel}\n removeLabel={removeLabel(assigneeLabel)}\n testId=\"board-filter-chip-assignee\"\n onRemove={() => onChange(withAssigneeFilter(filters, null))}\n />\n ) : null}\n\n {activeStatus ? (\n <FilterChip\n label={activeStatus.name}\n removeLabel={removeLabel(activeStatus.name)}\n testId=\"board-filter-chip-status\"\n onRemove={() => onChange(withStatusFilter(filters, null))}\n />\n ) : null}\n\n {filters.tagIds.map((tagId) => {\n const label = tagLabels.data?.get(tagId) ?? tagId\n return (\n <FilterChip\n key={tagId}\n label={label}\n removeLabel={removeLabel(label)}\n testId={`board-filter-chip-tag-${tagId}`}\n onRemove={() => onChange(withoutTagFilter(filters, tagId))}\n />\n )\n })}\n\n <AddBoardFilterPopover\n filters={filters}\n onChange={onChange}\n self={self}\n statuses={statuses}\n trigger={\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n data-testid=\"board-filter-add-chip\"\n className={`h-auto ${chipClassName(true)}`}\n >\n <Plus className=\"size-3\" aria-hidden=\"true\" />\n {t('staff.time_tracking.board.filters.add', 'Add filter')}\n </Button>\n }\n />\n\n {countActiveBoardFilters(filters) > 1 ? (\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"2xs\"\n onClick={() => onChange({ assigneeStaffMemberId: null, taskStatusId: null, tagIds: [] })}\n data-testid=\"board-filter-clear\"\n className=\"h-auto px-1 text-xs leading-normal text-muted-foreground hover:text-foreground\"\n >\n {t('staff.time_tracking.board.filters.clear', 'Clear filters')}\n </Button>\n ) : null}\n </div>\n )\n}\n\nexport default BoardFilterChips\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Check, Plus, X } from 'lucide-react'\nimport { Avatar } from '@open-mercato/ui/primitives/avatar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n useAssigneeNames,\n useStaffMemberDirectory,\n useTagDirectory,\n useTagLabels,\n useUnassignedTaskCount,\n type BoardSelf,\n} from './boardDirectory'\nimport {\n countActiveBoardFilters,\n withAssigneeFilter,\n withStatusFilter,\n withUnassignedFilter,\n withTagFilter,\n withoutTagFilter,\n type BoardFilters,\n} from './boardFilters'\nimport type { BoardStatus } from './kanbanBoardData'\n\n/**\n * The `dt-chips` row of screen 6 and the picker behind it.\n *\n * Modelled on the deals pipeline filter surfaces (`customers/.../pipeline/components`):\n * a popover-backed picker, chips that carry their own removal, one idiom for the whole\n * product. The difference the mockup asks for is the *shape* of the row \u2014 the pipeline\n * shows one chip per filter kind with its current value, screen 6 shows one chip per\n * active filter plus a dashed \"Dodaj filtr\" \u2014 so the chrome is shared in spirit rather\n * than by importing a component out of another module's page folder.\n *\n * \"Przypisane do mnie\" is the first entry in the picker and one click: it is the filter\n * an operator reaches for far more often than any other, so it does not sit behind a\n * person search.\n */\n\ntype BoardFilterSurfaceProps = {\n filters: BoardFilters\n onChange: (next: BoardFilters) => void\n self: BoardSelf | null\n statuses: readonly BoardStatus[]\n timeProjectId?: string | null\n}\n\nfunction chipClassName(dashed: boolean): string {\n const base =\n 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs leading-normal text-foreground'\n return dashed\n ? `${base} border border-dashed border-muted-foreground/60 bg-transparent text-muted-foreground hover:bg-muted/60`\n : `${base} border border-border bg-card`\n}\n\nfunction FilterChip({\n label,\n avatarLabel,\n removeLabel,\n onRemove,\n testId,\n}: {\n label: string\n avatarLabel?: string | null\n removeLabel: string\n onRemove: () => void\n testId: string\n}): React.ReactElement {\n return (\n <span className={chipClassName(false)} data-testid={testId}>\n {avatarLabel ? <Avatar size=\"xs\" label={avatarLabel} variant=\"monochrome\" /> : null}\n <span className=\"max-w-48 truncate font-semibold\">{label}</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"2xs\"\n onClick={onRemove}\n aria-label={removeLabel}\n data-testid={`${testId}-remove`}\n className=\"size-4 rounded-full p-0 text-muted-foreground hover:text-foreground\"\n >\n <X className=\"size-3\" aria-hidden=\"true\" />\n </Button>\n </span>\n )\n}\n\nfunction PickerRow({\n label,\n selected,\n avatarLabel,\n onSelect,\n testId,\n}: {\n label: string\n selected: boolean\n avatarLabel?: string | null\n onSelect: () => void\n testId: string\n}): React.ReactElement {\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n role=\"checkbox\"\n aria-checked={selected}\n onClick={onSelect}\n data-testid={testId}\n className={`h-auto w-full justify-start gap-2.5 rounded-md px-2.5 py-2 text-left text-sm font-normal ${\n selected ? 'bg-muted/60' : 'bg-transparent'\n }`}\n >\n {avatarLabel ? <Avatar size=\"xs\" label={avatarLabel} variant=\"monochrome\" /> : null}\n <span className=\"truncate text-foreground\">{label}</span>\n {selected ? <Check className=\"ml-auto size-3.5 text-muted-foreground\" aria-hidden=\"true\" /> : null}\n </Button>\n )\n}\n\nexport function AddBoardFilterPopover({\n filters,\n onChange,\n self,\n statuses,\n timeProjectId,\n trigger,\n testId = 'board-filter-add',\n}: BoardFilterSurfaceProps & {\n trigger: React.ReactNode\n testId?: string\n}): React.ReactElement {\n const t = useT()\n const [open, setOpen] = React.useState(false)\n const tagDirectory = useTagDirectory(open)\n const memberDirectory = useStaffMemberDirectory(open)\n\n const tags = tagDirectory.data ?? []\n const otherMembers = React.useMemo(\n () => (memberDirectory.data ?? []).filter((member) => member.id !== self?.id),\n [memberDirectory.data, self?.id],\n )\n\n const assignedToMeLabel = t('staff.time_tracking.board.filters.assignedToMe', 'Assigned to me')\n const unassignedCount = useUnassignedTaskCount(timeProjectId)\n const unassignedLabel =\n typeof unassignedCount.data === 'number'\n ? t('staff.time_tracking.board.filters.unassignedWithCount', 'Unassigned ({count})', { count: unassignedCount.data })\n : t('staff.time_tracking.board.filters.unassigned', 'Unassigned')\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-72 p-0\" data-testid={`${testId}-popover`}>\n <div className=\"max-h-96 overflow-y-auto p-2\">\n {self ? (\n <PickerRow\n label={assignedToMeLabel}\n avatarLabel={self.displayName ?? assignedToMeLabel}\n selected={filters.assigneeStaffMemberId === self.id}\n testId=\"board-filter-assigned-to-me\"\n onSelect={() => {\n onChange(\n withAssigneeFilter(\n filters,\n filters.assigneeStaffMemberId === self.id ? null : self.id,\n ),\n )\n setOpen(false)\n }}\n />\n ) : null}\n\n <PickerRow\n label={unassignedLabel}\n selected={filters.unassigned}\n testId=\"board-filter-unassigned\"\n onSelect={() => {\n onChange(withUnassignedFilter(filters, !filters.unassigned))\n setOpen(false)\n }}\n />\n\n {otherMembers.length > 0 ? (\n <div className=\"mt-2 flex flex-col gap-0.5\">\n <span className=\"px-2.5 py-1 text-xs font-semibold uppercase leading-normal tracking-wide text-muted-foreground\">\n {t('staff.time_tracking.board.filters.assignee', 'Assignee')}\n </span>\n {otherMembers.map((member) => (\n <PickerRow\n key={member.id}\n label={member.displayName}\n avatarLabel={member.displayName}\n selected={filters.assigneeStaffMemberId === member.id}\n testId={`board-filter-assignee-${member.id}`}\n onSelect={() => {\n onChange(\n withAssigneeFilter(\n filters,\n filters.assigneeStaffMemberId === member.id ? null : member.id,\n ),\n )\n setOpen(false)\n }}\n />\n ))}\n </div>\n ) : null}\n\n {statuses.length > 0 ? (\n <div className=\"mt-2 flex flex-col gap-0.5\">\n <span className=\"px-2.5 py-1 text-xs font-semibold uppercase leading-normal tracking-wide text-muted-foreground\">\n {t('staff.time_tracking.board.filters.status', 'Status')}\n </span>\n {statuses.map((status) => (\n <PickerRow\n key={status.id}\n label={status.name}\n selected={filters.taskStatusId === status.id}\n testId={`board-filter-status-${status.id}`}\n onSelect={() => {\n onChange(\n withStatusFilter(\n filters,\n filters.taskStatusId === status.id ? null : status.id,\n ),\n )\n setOpen(false)\n }}\n />\n ))}\n </div>\n ) : null}\n\n {tags.length > 0 ? (\n <div className=\"mt-2 flex flex-col gap-0.5\">\n <span className=\"px-2.5 py-1 text-xs font-semibold uppercase leading-normal tracking-wide text-muted-foreground\">\n {t('staff.time_tracking.board.filters.tag', 'Tag')}\n </span>\n {tags.map((tag) => {\n const selected = filters.tagIds.includes(tag.id)\n return (\n <PickerRow\n key={tag.id}\n label={tag.label}\n selected={selected}\n testId={`board-filter-tag-${tag.id}`}\n onSelect={() => {\n onChange(\n selected ? withoutTagFilter(filters, tag.id) : withTagFilter(filters, tag.id),\n )\n setOpen(false)\n }}\n />\n )\n })}\n </div>\n ) : null}\n\n {!self && otherMembers.length === 0 && statuses.length === 0 && tags.length === 0 ? (\n <p className=\"px-2.5 py-6 text-center text-xs leading-normal text-muted-foreground\">\n {t('staff.time_tracking.board.filters.noOptions', 'Nothing to filter by yet.')}\n </p>\n ) : null}\n </div>\n </PopoverContent>\n </Popover>\n )\n}\n\nexport function BoardFilterChips({\n filters,\n onChange,\n self,\n statuses,\n timeProjectId,\n}: BoardFilterSurfaceProps): React.ReactElement {\n const t = useT()\n const unassignedCount = useUnassignedTaskCount(filters.unassigned ? timeProjectId : null)\n\n const assigneeIds = React.useMemo(\n () =>\n filters.assigneeStaffMemberId && filters.assigneeStaffMemberId !== self?.id\n ? [filters.assigneeStaffMemberId]\n : [],\n [filters.assigneeStaffMemberId, self?.id],\n )\n const assigneeNames = useAssigneeNames(assigneeIds)\n const tagLabels = useTagLabels(filters.tagIds)\n\n const activeStatus = React.useMemo(\n () => statuses.find((status) => status.id === filters.taskStatusId) ?? null,\n [filters.taskStatusId, statuses],\n )\n\n const assignedToMeLabel = t('staff.time_tracking.board.filters.assignedToMe', 'Assigned to me')\n const isSelf = !!filters.assigneeStaffMemberId && filters.assigneeStaffMemberId === self?.id\n const assigneeLabel = isSelf\n ? assignedToMeLabel\n : filters.assigneeStaffMemberId\n ? assigneeNames.data?.get(filters.assigneeStaffMemberId) ?? filters.assigneeStaffMemberId\n : null\n\n const removeLabel = React.useCallback(\n (label: string) =>\n t('staff.time_tracking.board.filters.remove', 'Remove filter: {label}', { label }),\n [t],\n )\n\n return (\n <div className=\"flex flex-wrap items-center gap-2\" data-testid=\"board-filter-chips\">\n {assigneeLabel ? (\n <FilterChip\n label={assigneeLabel}\n avatarLabel={isSelf ? self?.displayName ?? assignedToMeLabel : assigneeLabel}\n removeLabel={removeLabel(assigneeLabel)}\n testId=\"board-filter-chip-assignee\"\n onRemove={() => onChange(withAssigneeFilter(filters, null))}\n />\n ) : null}\n\n {filters.unassigned ? (\n <FilterChip\n label={\n typeof unassignedCount.data === 'number'\n ? t('staff.time_tracking.board.filters.unassignedWithCount', 'Unassigned ({count})', { count: unassignedCount.data })\n : t('staff.time_tracking.board.filters.unassigned', 'Unassigned')\n }\n removeLabel={removeLabel(t('staff.time_tracking.board.filters.unassigned', 'Unassigned'))}\n testId=\"board-filter-chip-unassigned\"\n onRemove={() => onChange(withUnassignedFilter(filters, false))}\n />\n ) : null}\n\n {activeStatus ? (\n <FilterChip\n label={activeStatus.name}\n removeLabel={removeLabel(activeStatus.name)}\n testId=\"board-filter-chip-status\"\n onRemove={() => onChange(withStatusFilter(filters, null))}\n />\n ) : null}\n\n {filters.tagIds.map((tagId) => {\n const label = tagLabels.data?.get(tagId) ?? tagId\n return (\n <FilterChip\n key={tagId}\n label={label}\n removeLabel={removeLabel(label)}\n testId={`board-filter-chip-tag-${tagId}`}\n onRemove={() => onChange(withoutTagFilter(filters, tagId))}\n />\n )\n })}\n\n <AddBoardFilterPopover\n filters={filters}\n onChange={onChange}\n self={self}\n statuses={statuses}\n timeProjectId={timeProjectId}\n trigger={\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n data-testid=\"board-filter-add-chip\"\n className={`h-auto ${chipClassName(true)}`}\n >\n <Plus className=\"size-3\" aria-hidden=\"true\" />\n {t('staff.time_tracking.board.filters.add', 'Add filter')}\n </Button>\n }\n />\n\n {countActiveBoardFilters(filters) > 1 ? (\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"2xs\"\n onClick={() => onChange({ assigneeStaffMemberId: null, taskStatusId: null, tagIds: [], unassigned: false })}\n data-testid=\"board-filter-clear\"\n className=\"h-auto px-1 text-xs leading-normal text-muted-foreground hover:text-foreground\"\n >\n {t('staff.time_tracking.board.filters.clear', 'Clear filters')}\n </Button>\n ) : null}\n </div>\n )\n}\n\nexport default BoardFilterChips\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|