@open-mercato/core 0.10.1-develop.7717.1.2befe9c634 → 0.10.1-develop.7721.1.b5c4a00a68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/staff_support_routing_rule/index.js +21 -0
- package/dist/generated/entities/staff_support_routing_rule/index.js.map +7 -0
- package/dist/generated/entities/staff_support_ticket/index.js +37 -0
- package/dist/generated/entities/staff_support_ticket/index.js.map +7 -0
- package/dist/generated/entities/staff_time_project/index.js +2 -0
- package/dist/generated/entities/staff_time_project/index.js.map +2 -2
- package/dist/generated/entities/staff_time_project_member/index.js +2 -0
- package/dist/generated/entities/staff_time_project_member/index.js.map +2 -2
- package/dist/generated/entities/staff_time_task/index.js +4 -0
- package/dist/generated/entities/staff_time_task/index.js.map +2 -2
- package/dist/generated/entities/staff_time_task_comment/index.js +9 -1
- package/dist/generated/entities/staff_time_task_comment/index.js.map +2 -2
- package/dist/generated/entities.ids.generated.js +2 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +38 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/auth.js +1 -0
- package/dist/helpers/integration/auth.js.map +2 -2
- package/dist/helpers/integration/communicationChannelsFixtures.js +20 -0
- package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
- package/dist/modules/catalog/api/bulk-delete/route.js +12 -2
- package/dist/modules/catalog/api/bulk-delete/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/test-seed/route.js +17 -3
- package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
- package/dist/modules/communication_channels/lib/test-seed.js +38 -5
- package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
- package/dist/modules/messages/commands/messages.js +2 -3
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
- package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js +299 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js +91 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/route.js +146 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/options/route.js +60 -0
- package/dist/modules/staff/api/portal/support-tickets/options/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/route.js +220 -0
- package/dist/modules/staff/api/portal/support-tickets/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/settings/route.js +7 -1
- package/dist/modules/staff/api/timesheets/settings/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js +122 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js +180 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/route.js +147 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/support-tickets/options/route.js +133 -0
- package/dist/modules/staff/api/timesheets/support-tickets/options/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js +33 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js +25 -7
- package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js +32 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/route.js +94 -47
- package/dist/modules/staff/api/timesheets/tasks/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +3 -0
- package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/route.js +3 -0
- package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +1 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +54 -4
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js +21 -5
- package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/settings/page.js +319 -254
- package/dist/modules/staff/backend/staff/time-tracking/settings/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/support/page.js +363 -0
- package/dist/modules/staff/backend/staff/time-tracking/support/page.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js +18 -0
- package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js.map +7 -0
- package/dist/modules/staff/cli.js +27 -0
- package/dist/modules/staff/cli.js.map +2 -2
- package/dist/modules/staff/commands/index.js +3 -0
- package/dist/modules/staff/commands/index.js.map +2 -2
- package/dist/modules/staff/commands/shared.js +10 -1
- package/dist/modules/staff/commands/shared.js.map +2 -2
- package/dist/modules/staff/commands/support-routing-rules.js +505 -0
- package/dist/modules/staff/commands/support-routing-rules.js.map +7 -0
- package/dist/modules/staff/commands/support-tickets.js +513 -0
- package/dist/modules/staff/commands/support-tickets.js.map +7 -0
- package/dist/modules/staff/commands/timesheets-projects.js +72 -14
- package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-task-assignment.js +270 -0
- package/dist/modules/staff/commands/timesheets-task-assignment.js.map +7 -0
- package/dist/modules/staff/commands/timesheets-task-comments.js +58 -12
- package/dist/modules/staff/commands/timesheets-task-comments.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-tasks.js +91 -9
- package/dist/modules/staff/commands/timesheets-tasks.js.map +3 -3
- package/dist/modules/staff/data/enrichers.js +141 -0
- package/dist/modules/staff/data/enrichers.js.map +2 -2
- package/dist/modules/staff/data/entities.js +143 -2
- package/dist/modules/staff/data/entities.js.map +2 -2
- package/dist/modules/staff/data/extensions.js +8 -0
- package/dist/modules/staff/data/extensions.js.map +2 -2
- package/dist/modules/staff/data/validators.js +34 -5
- package/dist/modules/staff/data/validators.js.map +2 -2
- package/dist/modules/staff/encryption.js +6 -0
- package/dist/modules/staff/encryption.js.map +2 -2
- package/dist/modules/staff/events.js +25 -0
- package/dist/modules/staff/events.js.map +2 -2
- package/dist/modules/staff/events.payloads.js +54 -0
- package/dist/modules/staff/events.payloads.js.map +2 -2
- package/dist/modules/staff/extension-points.js +1 -0
- package/dist/modules/staff/extension-points.js.map +2 -2
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js +468 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js +12 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js +316 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js +12 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js +190 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js +19 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js.map +7 -0
- package/dist/modules/staff/index.js +1 -1
- package/dist/modules/staff/index.js.map +2 -2
- package/dist/modules/staff/lib/crud.js +2 -0
- package/dist/modules/staff/lib/crud.js.map +2 -2
- package/dist/modules/staff/lib/support/attachments.js +36 -0
- package/dist/modules/staff/lib/support/attachments.js.map +7 -0
- package/dist/modules/staff/lib/support/dictionaries.js +87 -0
- package/dist/modules/staff/lib/support/dictionaries.js.map +7 -0
- package/dist/modules/staff/lib/support/notifications.js +111 -0
- package/dist/modules/staff/lib/support/notifications.js.map +7 -0
- package/dist/modules/staff/lib/support/portalAttachments.js +100 -0
- package/dist/modules/staff/lib/support/portalAttachments.js.map +7 -0
- package/dist/modules/staff/lib/support/portalBroadcast.js +117 -0
- package/dist/modules/staff/lib/support/portalBroadcast.js.map +7 -0
- package/dist/modules/staff/lib/support/portalRoute.js +99 -0
- package/dist/modules/staff/lib/support/portalRoute.js.map +7 -0
- package/dist/modules/staff/lib/support/portalStatus.js +12 -0
- package/dist/modules/staff/lib/support/portalStatus.js.map +7 -0
- package/dist/modules/staff/lib/support/portalTickets.js +148 -0
- package/dist/modules/staff/lib/support/portalTickets.js.map +7 -0
- package/dist/modules/staff/lib/support/routing.js +81 -0
- package/dist/modules/staff/lib/support/routing.js.map +7 -0
- package/dist/modules/staff/lib/support/routingRuleDraft.js +25 -0
- package/dist/modules/staff/lib/support/routingRuleDraft.js.map +7 -0
- package/dist/modules/staff/lib/support/seedDefaults.js +114 -0
- package/dist/modules/staff/lib/support/seedDefaults.js.map +7 -0
- package/dist/modules/staff/lib/support/supportActivity.js +26 -0
- package/dist/modules/staff/lib/support/supportActivity.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js +23 -0
- package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js +112 -0
- package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/portalPendingUploads.js +45 -0
- package/dist/modules/staff/lib/support-ui/portalPendingUploads.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/portalSupport.js +51 -0
- package/dist/modules/staff/lib/support-ui/portalSupport.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/access.js +3 -39
- package/dist/modules/staff/lib/time-tracking/access.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/listProjectAccess.js +54 -0
- package/dist/modules/staff/lib/time-tracking/listProjectAccess.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/portalRecipients.js +92 -2
- package/dist/modules/staff/lib/time-tracking/portalRecipients.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/projectAssignment.js +112 -0
- package/dist/modules/staff/lib/time-tracking/projectAssignment.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/settingKeys.js +14 -0
- package/dist/modules/staff/lib/time-tracking/settingKeys.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/settings.js +28 -1
- package/dist/modules/staff/lib/time-tracking/settings.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js +143 -0
- package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/workRecipients.js +92 -0
- package/dist/modules/staff/lib/time-tracking/workRecipients.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js +32 -3
- package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js +29 -0
- package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js +4 -0
- package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js +141 -1
- package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js +153 -0
- package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js +67 -0
- package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js +3 -1
- package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js +80 -17
- package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js +230 -41
- package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js +21 -1
- package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js +16 -4
- package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js +3 -1
- package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js +68 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js +50 -0
- package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js +13 -1
- package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js +36 -2
- package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js.map +2 -2
- package/dist/modules/staff/migrations/Migration20261009180543_staff.js +42 -0
- package/dist/modules/staff/migrations/Migration20261009180543_staff.js.map +7 -0
- package/dist/modules/staff/notifications.js +92 -0
- package/dist/modules/staff/notifications.js.map +2 -2
- package/dist/modules/staff/setup.js +6 -2
- package/dist/modules/staff/setup.js.map +2 -2
- package/dist/modules/staff/subscribers/support-portal-comment-created.js +26 -0
- package/dist/modules/staff/subscribers/support-portal-comment-created.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-comment-deleted.js +26 -0
- package/dist/modules/staff/subscribers/support-portal-comment-deleted.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-comment-updated.js +26 -0
- package/dist/modules/staff/subscribers/support-portal-comment-updated.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-status-column-updated.js +24 -0
- package/dist/modules/staff/subscribers/support-portal-status-column-updated.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-task-status-changed.js +25 -0
- package/dist/modules/staff/subscribers/support-portal-task-status-changed.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-task-updated.js +25 -0
- package/dist/modules/staff/subscribers/support-portal-task-updated.js.map +7 -0
- package/dist/modules/staff/subscribers/support-ticket-created-notification.js +25 -0
- package/dist/modules/staff/subscribers/support-ticket-created-notification.js.map +7 -0
- package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js +25 -0
- package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js.map +7 -0
- package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js +26 -0
- package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js.map +7 -0
- package/dist/modules/staff/subscribers/time-task-assignee-notification.js +83 -0
- package/dist/modules/staff/subscribers/time-task-assignee-notification.js.map +7 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js +112 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js.map +7 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js +17 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js.map +7 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js +870 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js.map +7 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js +17 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js.map +7 -0
- package/dist/modules/staff/widgets/injection-table.js +16 -1
- package/dist/modules/staff/widgets/injection-table.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +4 -18
- package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLotDetailPage.js +4 -18
- package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +4 -18
- package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +2 -2
- package/dist/modules/wms/lib/inventoryCsvExport.js +19 -0
- package/dist/modules/wms/lib/inventoryCsvExport.js.map +7 -0
- package/generated/entities/staff_support_routing_rule/index.ts +9 -0
- package/generated/entities/staff_support_ticket/index.ts +17 -0
- package/generated/entities/staff_time_project/index.ts +1 -0
- package/generated/entities/staff_time_project_member/index.ts +1 -0
- package/generated/entities/staff_time_task/index.ts +2 -0
- package/generated/entities/staff_time_task_comment/index.ts +4 -0
- package/generated/entities.ids.generated.ts +2 -0
- package/generated/entity-fields-registry.ts +38 -0
- package/package.json +8 -8
- package/src/helpers/integration/auth.ts +1 -1
- package/src/helpers/integration/communicationChannelsFixtures.ts +60 -2
- package/src/modules/catalog/api/bulk-delete/route.ts +12 -2
- package/src/modules/communication_channels/api/post/test-seed/route.ts +29 -12
- package/src/modules/communication_channels/lib/test-seed.ts +49 -10
- package/src/modules/messages/commands/messages.ts +8 -10
- package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +2 -2
- package/src/modules/staff/AGENTS.md +22 -6
- package/src/modules/staff/api/portal/support-tickets/[id]/attachments/route.ts +327 -0
- package/src/modules/staff/api/portal/support-tickets/[id]/messages/route.ts +94 -0
- package/src/modules/staff/api/portal/support-tickets/[id]/route.ts +160 -0
- package/src/modules/staff/api/portal/support-tickets/options/route.ts +60 -0
- package/src/modules/staff/api/portal/support-tickets/route.ts +253 -0
- package/src/modules/staff/api/timesheets/settings/route.ts +7 -1
- package/src/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.ts +131 -0
- package/src/modules/staff/api/timesheets/support-routing-rules/preview/route.ts +248 -0
- package/src/modules/staff/api/timesheets/support-routing-rules/route.ts +150 -0
- package/src/modules/staff/api/timesheets/support-tickets/options/route.ts +181 -0
- package/src/modules/staff/api/timesheets/tasks/[id]/claim/route.ts +31 -0
- package/src/modules/staff/api/timesheets/tasks/[id]/comments/route.ts +25 -7
- package/src/modules/staff/api/timesheets/tasks/[id]/release/route.ts +30 -0
- package/src/modules/staff/api/timesheets/tasks/route.ts +119 -67
- package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +3 -0
- package/src/modules/staff/api/timesheets/time-projects/route.ts +3 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +1 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +67 -2
- package/src/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.ts +22 -3
- package/src/modules/staff/backend/staff/time-tracking/settings/page.tsx +405 -308
- package/src/modules/staff/backend/staff/time-tracking/support/page.meta.ts +14 -0
- package/src/modules/staff/backend/staff/time-tracking/support/page.tsx +428 -0
- package/src/modules/staff/cli.ts +28 -0
- package/src/modules/staff/commands/index.ts +3 -0
- package/src/modules/staff/commands/shared.ts +22 -1
- package/src/modules/staff/commands/support-routing-rules.ts +595 -0
- package/src/modules/staff/commands/support-tickets.ts +580 -0
- package/src/modules/staff/commands/timesheets-projects.ts +84 -13
- package/src/modules/staff/commands/timesheets-task-assignment.ts +319 -0
- package/src/modules/staff/commands/timesheets-task-comments.ts +70 -15
- package/src/modules/staff/commands/timesheets-tasks.ts +106 -15
- package/src/modules/staff/data/enrichers.ts +203 -0
- package/src/modules/staff/data/entities.ts +133 -2
- package/src/modules/staff/data/extensions.ts +9 -1
- package/src/modules/staff/data/validators.ts +36 -0
- package/src/modules/staff/encryption.ts +6 -0
- package/src/modules/staff/events.payloads.ts +87 -1
- package/src/modules/staff/events.ts +26 -0
- package/src/modules/staff/extension-points.ts +1 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.ts +10 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.tsx +553 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.ts +10 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.tsx +346 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/page.meta.ts +17 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/page.tsx +253 -0
- package/src/modules/staff/i18n/de.json +291 -0
- package/src/modules/staff/i18n/en.json +291 -0
- package/src/modules/staff/i18n/es.json +291 -0
- package/src/modules/staff/i18n/ko.json +291 -0
- package/src/modules/staff/i18n/pl.json +291 -0
- package/src/modules/staff/index.ts +1 -1
- package/src/modules/staff/lib/crud.ts +2 -1
- package/src/modules/staff/lib/support/attachments.ts +43 -0
- package/src/modules/staff/lib/support/dictionaries.ts +120 -0
- package/src/modules/staff/lib/support/notifications.ts +138 -0
- package/src/modules/staff/lib/support/portalAttachments.ts +140 -0
- package/src/modules/staff/lib/support/portalBroadcast.ts +161 -0
- package/src/modules/staff/lib/support/portalRoute.ts +108 -0
- package/src/modules/staff/lib/support/portalStatus.ts +15 -0
- package/src/modules/staff/lib/support/portalTickets.ts +233 -0
- package/src/modules/staff/lib/support/routing.ts +117 -0
- package/src/modules/staff/lib/support/routingRuleDraft.ts +45 -0
- package/src/modules/staff/lib/support/seedDefaults.ts +151 -0
- package/src/modules/staff/lib/support/supportActivity.ts +39 -0
- package/src/modules/staff/lib/support-ui/PortalSupportNotLinked.tsx +20 -0
- package/src/modules/staff/lib/support-ui/portalAttachmentValidation.ts +125 -0
- package/src/modules/staff/lib/support-ui/portalPendingUploads.ts +49 -0
- package/src/modules/staff/lib/support-ui/portalSupport.ts +116 -0
- package/src/modules/staff/lib/time-tracking/access.ts +2 -62
- package/src/modules/staff/lib/time-tracking/listProjectAccess.ts +78 -0
- package/src/modules/staff/lib/time-tracking/portalRecipients.ts +173 -5
- package/src/modules/staff/lib/time-tracking/projectAssignment.ts +172 -0
- package/src/modules/staff/lib/time-tracking/settingKeys.ts +11 -0
- package/src/modules/staff/lib/time-tracking/settings.ts +46 -1
- package/src/modules/staff/lib/time-tracking/taskAssignmentRoute.ts +165 -0
- package/src/modules/staff/lib/time-tracking/workRecipients.ts +183 -0
- package/src/modules/staff/lib/time-tracking-ui/BoardFilterChips.tsx +36 -1
- package/src/modules/staff/lib/time-tracking-ui/KanbanBoard.tsx +30 -0
- package/src/modules/staff/lib/time-tracking-ui/KanbanCard.tsx +9 -0
- package/src/modules/staff/lib/time-tracking-ui/ProjectFormSections.tsx +185 -6
- package/src/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.tsx +188 -0
- package/src/modules/staff/lib/time-tracking-ui/SupportRequestPanel.tsx +102 -0
- package/src/modules/staff/lib/time-tracking-ui/TaskBoardScreen.tsx +2 -0
- package/src/modules/staff/lib/time-tracking-ui/TaskCommentThread.tsx +105 -22
- package/src/modules/staff/lib/time-tracking-ui/TaskDrawer.tsx +278 -43
- package/src/modules/staff/lib/time-tracking-ui/boardDirectory.ts +21 -0
- package/src/modules/staff/lib/time-tracking-ui/boardFilters.ts +16 -2
- package/src/modules/staff/lib/time-tracking-ui/kanbanBoardData.ts +4 -0
- package/src/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.ts +117 -0
- package/src/modules/staff/lib/time-tracking-ui/settingsTabs.ts +17 -0
- package/src/modules/staff/lib/time-tracking-ui/supportTicketData.ts +71 -0
- package/src/modules/staff/lib/time-tracking-ui/taskDrawerData.ts +15 -0
- package/src/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.ts +48 -1
- package/src/modules/staff/migrations/.snapshot-open-mercato.json +714 -0
- package/src/modules/staff/migrations/Migration20261009180543_staff.ts +50 -0
- package/src/modules/staff/notifications.ts +92 -0
- package/src/modules/staff/setup.ts +6 -2
- package/src/modules/staff/subscribers/support-portal-comment-created.ts +35 -0
- package/src/modules/staff/subscribers/support-portal-comment-deleted.ts +35 -0
- package/src/modules/staff/subscribers/support-portal-comment-updated.ts +35 -0
- package/src/modules/staff/subscribers/support-portal-status-column-updated.ts +36 -0
- package/src/modules/staff/subscribers/support-portal-task-status-changed.ts +34 -0
- package/src/modules/staff/subscribers/support-portal-task-updated.ts +34 -0
- package/src/modules/staff/subscribers/support-ticket-created-notification.ts +27 -0
- package/src/modules/staff/subscribers/support-ticket-customer-replied-notification.ts +27 -0
- package/src/modules/staff/subscribers/support-ticket-unnotified-clear.ts +29 -0
- package/src/modules/staff/subscribers/time-task-assignee-notification.ts +102 -0
- package/src/modules/staff/widgets/injection/portal-support-tickets/widget.client.tsx +151 -0
- package/src/modules/staff/widgets/injection/portal-support-tickets/widget.ts +16 -0
- package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.tsx +1058 -0
- package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.ts +17 -0
- package/src/modules/staff/widgets/injection-table.ts +15 -0
- package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +4 -20
- package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +4 -20
- package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +4 -19
- package/src/modules/wms/lib/inventoryCsvExport.ts +18 -0
|
@@ -8,9 +8,20 @@ import { Avatar } from "@open-mercato/ui/primitives/avatar";
|
|
|
8
8
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
9
9
|
import { Input } from "@open-mercato/ui/primitives/input";
|
|
10
10
|
import { Label } from "@open-mercato/ui/primitives/label";
|
|
11
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@open-mercato/ui/primitives/select";
|
|
11
12
|
import { SegmentedControl, SegmentedControlItem } from "@open-mercato/ui/primitives/segmented-control";
|
|
12
13
|
import { SwitchField } from "@open-mercato/ui/primitives/switch-field";
|
|
13
14
|
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
15
|
+
import { ErrorMessage } from "@open-mercato/ui/backend/detail";
|
|
16
|
+
import { resolveGraceDays } from "./projectTeamAssignment.js";
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_ASSIGNEE_PAGE_SIZE,
|
|
19
|
+
chunkIds,
|
|
20
|
+
classifyDefaultAssigneeCandidates,
|
|
21
|
+
collectProjectMemberships,
|
|
22
|
+
readProjectMembershipPage,
|
|
23
|
+
resolveSelectedDefaultAssignee
|
|
24
|
+
} from "./projectDefaultAssignee.js";
|
|
14
25
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
15
26
|
const DEFAULT_BUDGET_WARN_AT_PERCENT = 80;
|
|
16
27
|
function ProjectBudgetSection({
|
|
@@ -91,6 +102,7 @@ function ProjectBudgetSection({
|
|
|
91
102
|
] }) : null
|
|
92
103
|
] });
|
|
93
104
|
}
|
|
105
|
+
import { isUsableDefaultAssigneeMembership } from "./projectDefaultAssignee.js";
|
|
94
106
|
function readString(value) {
|
|
95
107
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
96
108
|
}
|
|
@@ -201,12 +213,140 @@ function ProjectRoundingNotice() {
|
|
|
201
213
|
] }) });
|
|
202
214
|
}
|
|
203
215
|
const __testing = { memberLabel };
|
|
216
|
+
const NO_DEFAULT_ASSIGNEE = "__none__";
|
|
217
|
+
function ProjectDefaultAssigneeField({ projectId, value, onChange, disabled }) {
|
|
218
|
+
const t = useT();
|
|
219
|
+
const [options, setOptions] = React.useState([]);
|
|
220
|
+
const [loadState, setLoadState] = React.useState("loading");
|
|
221
|
+
const [reloadToken, setReloadToken] = React.useState(0);
|
|
222
|
+
const [truncated, setTruncated] = React.useState(false);
|
|
223
|
+
const [extraLabels, setExtraLabels] = React.useState(/* @__PURE__ */ new Map());
|
|
224
|
+
const valueRef = React.useRef(value);
|
|
225
|
+
valueRef.current = value;
|
|
226
|
+
React.useEffect(() => {
|
|
227
|
+
if (!projectId) {
|
|
228
|
+
setOptions([]);
|
|
229
|
+
setTruncated(false);
|
|
230
|
+
setExtraLabels(/* @__PURE__ */ new Map());
|
|
231
|
+
setLoadState("loaded");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
let cancelled = false;
|
|
235
|
+
setLoadState("loading");
|
|
236
|
+
async function load() {
|
|
237
|
+
try {
|
|
238
|
+
const [collection, settingsRes] = await Promise.all([
|
|
239
|
+
collectProjectMemberships(async (page) => {
|
|
240
|
+
const membersRes = await apiCall(
|
|
241
|
+
`/api/staff/timesheets/time-projects/${projectId}/employees?page=${page}&pageSize=${DEFAULT_ASSIGNEE_PAGE_SIZE}`
|
|
242
|
+
);
|
|
243
|
+
return readProjectMembershipPage(membersRes);
|
|
244
|
+
}),
|
|
245
|
+
apiCall("/api/staff/timesheets/settings")
|
|
246
|
+
]);
|
|
247
|
+
const graceDays = resolveGraceDays(settingsRes.ok ? settingsRes.result?.access?.assignmentGraceDays : null);
|
|
248
|
+
const candidates = classifyDefaultAssigneeCandidates(collection.rows, graceDays);
|
|
249
|
+
if (!cancelled) setTruncated(collection.truncated);
|
|
250
|
+
const selectedId = valueRef.current;
|
|
251
|
+
const labelIds = Array.from(/* @__PURE__ */ new Set([
|
|
252
|
+
...candidates.map((candidate) => candidate.staffMemberId),
|
|
253
|
+
...selectedId ? [selectedId] : []
|
|
254
|
+
]));
|
|
255
|
+
if (labelIds.length === 0) {
|
|
256
|
+
if (!cancelled) {
|
|
257
|
+
setOptions([]);
|
|
258
|
+
setExtraLabels(/* @__PURE__ */ new Map());
|
|
259
|
+
setLoadState("loaded");
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const staffResults = await Promise.all(
|
|
264
|
+
chunkIds(labelIds).map(
|
|
265
|
+
(ids) => apiCall(
|
|
266
|
+
`/api/staff/team-members?ids=${ids.join(",")}&pageSize=${DEFAULT_ASSIGNEE_PAGE_SIZE}`
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
);
|
|
270
|
+
const staffRows = staffResults.flatMap((staffRes) => Array.isArray(staffRes.result?.items) ? staffRes.result.items : []);
|
|
271
|
+
const labels = new Map(
|
|
272
|
+
staffRows.map((row) => [readString(row.id), memberLabel(row)]).filter((entry) => entry[0] !== null)
|
|
273
|
+
);
|
|
274
|
+
if (!cancelled) {
|
|
275
|
+
setOptions(candidates.map((candidate) => ({
|
|
276
|
+
id: candidate.staffMemberId,
|
|
277
|
+
label: labels.get(candidate.staffMemberId) || candidate.staffMemberId,
|
|
278
|
+
active: candidate.active
|
|
279
|
+
})));
|
|
280
|
+
setExtraLabels(labels);
|
|
281
|
+
setLoadState("loaded");
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
if (!cancelled) setLoadState("failed");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
void load();
|
|
288
|
+
return () => {
|
|
289
|
+
cancelled = true;
|
|
290
|
+
};
|
|
291
|
+
}, [projectId, reloadToken]);
|
|
292
|
+
const retryLoad = React.useCallback(() => setReloadToken((token) => token + 1), []);
|
|
293
|
+
const selection = resolveSelectedDefaultAssignee(
|
|
294
|
+
options.map((option) => ({ staffMemberId: option.id, active: option.active })),
|
|
295
|
+
value,
|
|
296
|
+
truncated
|
|
297
|
+
);
|
|
298
|
+
const selected = value ? options.find((option) => option.id === value) ?? null : null;
|
|
299
|
+
const selectedLabel = selected?.label ?? (value ? extraLabels.get(value) || value : null);
|
|
300
|
+
const loadFailed = loadState === "failed";
|
|
301
|
+
const activeOptions = loadFailed ? [] : options.filter((option) => option.active);
|
|
302
|
+
const showWarning = loadState === "loaded" && selection.unusable;
|
|
303
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", "data-testid": "project-default-assignee", children: [
|
|
304
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "project-default-assignee-select", children: t("staff.time_tracking.assigners.defaultAssignee", "Default assignee for portal tickets") }),
|
|
305
|
+
/* @__PURE__ */ jsxs(
|
|
306
|
+
Select,
|
|
307
|
+
{
|
|
308
|
+
value: value ?? NO_DEFAULT_ASSIGNEE,
|
|
309
|
+
disabled: disabled || !projectId,
|
|
310
|
+
onValueChange: (next) => onChange(next === NO_DEFAULT_ASSIGNEE ? null : next),
|
|
311
|
+
children: [
|
|
312
|
+
/* @__PURE__ */ jsx(SelectTrigger, { id: "project-default-assignee-select", "data-testid": "project-default-assignee-select", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
313
|
+
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
314
|
+
/* @__PURE__ */ jsx(SelectItem, { value: NO_DEFAULT_ASSIGNEE, children: t("staff.time_tracking.assigners.defaultAssigneeNone", "No default assignee") }),
|
|
315
|
+
activeOptions.map((option) => /* @__PURE__ */ jsx(SelectItem, { value: option.id, children: option.label }, option.id)),
|
|
316
|
+
value && (loadFailed || !selected || !selected.active) ? /* @__PURE__ */ jsx(SelectItem, { value, children: selectedLabel ?? value }) : null
|
|
317
|
+
] })
|
|
318
|
+
]
|
|
319
|
+
}
|
|
320
|
+
),
|
|
321
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
|
|
322
|
+
"staff.time_tracking.assigners.defaultAssigneeHelp",
|
|
323
|
+
"New customer portal tickets routed to this project are assigned to this member. Tasks created on the board stay assigned to their creator."
|
|
324
|
+
) }),
|
|
325
|
+
loadFailed ? /* @__PURE__ */ jsx(
|
|
326
|
+
ErrorMessage,
|
|
327
|
+
{
|
|
328
|
+
label: t(
|
|
329
|
+
"staff.time_tracking.assigners.defaultAssigneeLoadFailed",
|
|
330
|
+
"Could not load the project members, so the default assignee cannot be checked right now."
|
|
331
|
+
),
|
|
332
|
+
action: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: retryLoad, "data-testid": "project-default-assignee-retry", children: t("staff.time_tracking.assigners.defaultAssigneeRetry", "Retry") })
|
|
333
|
+
}
|
|
334
|
+
) : null,
|
|
335
|
+
showWarning ? /* @__PURE__ */ jsx(Alert, { status: "warning", children: /* @__PURE__ */ jsx(AlertDescription, { children: t(
|
|
336
|
+
"staff.time_tracking.assigners.defaultAssigneeInactive",
|
|
337
|
+
"This member is no longer an active project member. New tickets will be left unassigned and the assigners notified."
|
|
338
|
+
) }) }) : null,
|
|
339
|
+
!projectId ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("staff.time_tracking.assigners.defaultAssigneeAfterCreate", "Save the project and add members to choose a default assignee.") }) : null
|
|
340
|
+
] });
|
|
341
|
+
}
|
|
204
342
|
export {
|
|
205
343
|
DEFAULT_BUDGET_WARN_AT_PERCENT,
|
|
206
344
|
ProjectBudgetSection,
|
|
345
|
+
ProjectDefaultAssigneeField,
|
|
207
346
|
ProjectRoundingNotice,
|
|
208
347
|
ProjectTeamSection,
|
|
209
348
|
__testing,
|
|
210
|
-
describeRounding
|
|
349
|
+
describeRounding,
|
|
350
|
+
isUsableDefaultAssigneeMembership
|
|
211
351
|
};
|
|
212
352
|
//# sourceMappingURL=ProjectFormSections.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/staff/lib/time-tracking-ui/ProjectFormSections.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Users } from 'lucide-react'\nimport { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'\nimport { Avatar } from '@open-mercato/ui/primitives/avatar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { SegmentedControl, SegmentedControlItem } from '@open-mercato/ui/primitives/segmented-control'\nimport { SwitchField } from '@open-mercato/ui/primitives/switch-field'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\n\nexport type ProjectBudgetKind = 'none' | 'hours' | 'amount'\n\nexport const DEFAULT_BUDGET_WARN_AT_PERCENT = 80\n\nexport type BudgetSectionProps = {\n enabled: boolean\n kind: ProjectBudgetKind\n value: string\n warnAtPercent: string\n currencyCode: string | null\n disabled?: boolean\n onChange: (patch: {\n enabled?: boolean\n kind?: ProjectBudgetKind\n value?: string\n warnAtPercent?: string\n }) => void\n}\n\n/**\n * Screen 4, \"Bud\u017Cet\" \u2014 first-class scope per the spec (\u00A79 promoted out of\n * \"proposal\"). The switch maps to `budgetKind: 'none'`; turning it on restores\n * the last picked kind so a toggle never silently discards the limit.\n */\nexport function ProjectBudgetSection({\n enabled,\n kind,\n value,\n warnAtPercent,\n currencyCode,\n disabled,\n onChange,\n}: BudgetSectionProps) {\n const t = useT()\n const unitLabel = kind === 'amount'\n ? (currencyCode ?? t('staff.time_tracking.projects.budget.amountUnit', 'amount'))\n : t('staff.time_tracking.projects.budget.hoursUnit', 'h')\n\n return (\n <div className=\"flex flex-col gap-4\">\n <SwitchField\n checked={enabled}\n disabled={disabled}\n onCheckedChange={(next) => onChange({ enabled: next === true, kind: next === true && kind === 'none' ? 'hours' : kind })}\n label={t('staff.time_tracking.projects.budget.enable', 'Track the limit')}\n description={t(\n 'staff.time_tracking.projects.budget.enableHint',\n 'Warns at {{percent}}% and 100% of the limit.',\n { percent: warnAtPercent || String(DEFAULT_BUDGET_WARN_AT_PERCENT) },\n )}\n />\n\n {enabled ? (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div className=\"flex flex-col gap-1.5\">\n <Label>{t('staff.time_tracking.projects.budget.kind', 'Limit type')}</Label>\n <SegmentedControl\n value={kind === 'none' ? 'hours' : kind}\n disabled={disabled}\n onValueChange={(next) => onChange({ kind: next as ProjectBudgetKind })}\n aria-label={t('staff.time_tracking.projects.budget.kind', 'Limit type')}\n >\n <SegmentedControlItem value=\"hours\">\n {t('staff.time_tracking.projects.budget.kindHours', 'Hours')}\n </SegmentedControlItem>\n <SegmentedControlItem value=\"amount\">\n {t('staff.time_tracking.projects.budget.kindAmount', 'Amount')}\n </SegmentedControlItem>\n </SegmentedControl>\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"tt-budget-value\">\n {t('staff.time_tracking.projects.budget.value', 'Limit')}\n </Label>\n <div className=\"flex items-center gap-2\">\n <Input\n id=\"tt-budget-value\"\n inputMode=\"decimal\"\n value={value}\n disabled={disabled}\n onChange={(event) => onChange({ value: event.target.value })}\n />\n <span className=\"shrink-0 text-sm text-muted-foreground\">{unitLabel}</span>\n </div>\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"tt-budget-warn\">\n {t('staff.time_tracking.projects.budget.warnAt', 'Warn at')}\n </Label>\n <div className=\"flex items-center gap-2\">\n <Input\n id=\"tt-budget-warn\"\n inputMode=\"numeric\"\n value={warnAtPercent}\n disabled={disabled}\n onChange={(event) => onChange({ warnAtPercent: event.target.value })}\n />\n <span className=\"shrink-0 text-sm text-muted-foreground\">%</span>\n </div>\n </div>\n </div>\n ) : null}\n </div>\n )\n}\n\ntype ProjectMemberRow = {\n id?: unknown\n staffMemberId?: unknown\n staff_member_id?: unknown\n status?: unknown\n}\n\ntype TeamMemberRow = {\n id?: unknown\n firstName?: unknown\n lastName?: unknown\n first_name?: unknown\n last_name?: unknown\n displayName?: unknown\n display_name?: unknown\n email?: unknown\n}\n\nfunction readString(value: unknown): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null\n}\n\n/**\n * `/api/staff/team-members` answers in snake_case, so `display_name` is the key\n * that actually arrives \u2014 reading only `displayName` fell through every branch\n * and rendered the raw staff-member UUID as the chip label.\n *\n * `StaffTeamMember` carries no first/last/email columns; those branches describe a\n * shape this endpoint never returns and are kept only for callers that pass a\n * different row in. An unnamed row yields an empty string rather than an id: the\n * caller filters empties out, so a missing name shows nothing instead of leaking\n * an internal identifier into the UI.\n */\nfunction memberLabel(row: TeamMemberRow): string {\n const display = readString(row.displayName) ?? readString(row.display_name)\n if (display) return display\n const first = readString(row.firstName) ?? readString(row.first_name)\n const last = readString(row.lastName) ?? readString(row.last_name)\n const joined = [first, last].filter(Boolean).join(' ')\n return joined || readString(row.email) || ''\n}\n\nexport type ProjectTeamSectionProps = {\n projectId?: string | null\n}\n\n/**\n * Screen 4's right column: the assigned team as chips plus the entry point into\n * the team drawer. The drawer itself is a separate task and lives behind\n * `?panel=team` on the project route.\n */\nexport function ProjectTeamSection({ projectId }: ProjectTeamSectionProps) {\n const t = useT()\n const [names, setNames] = React.useState<string[]>([])\n const [loading, setLoading] = React.useState(Boolean(projectId))\n\n React.useEffect(() => {\n if (!projectId) {\n setNames([])\n setLoading(false)\n return\n }\n let cancelled = false\n async function load() {\n setLoading(true)\n try {\n const membersRes = await apiCall<{ items?: ProjectMemberRow[] }>(\n `/api/staff/timesheets/time-projects/${projectId}/employees?pageSize=100&status=active`,\n )\n const memberIds = (Array.isArray(membersRes.result?.items) ? membersRes.result.items : [])\n .map((row) => readString(row.staffMemberId) ?? readString(row.staff_member_id))\n .filter((id): id is string => id !== null)\n if (memberIds.length === 0) {\n if (!cancelled) setNames([])\n return\n }\n const staffRes = await apiCall<{ items?: TeamMemberRow[] }>(\n `/api/staff/team-members?ids=${memberIds.join(',')}&pageSize=100`,\n )\n const rows = Array.isArray(staffRes.result?.items) ? staffRes.result.items : []\n if (!cancelled) setNames(rows.map(memberLabel).filter((name) => name.length > 0))\n } catch {\n if (!cancelled) setNames([])\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n void load()\n return () => { cancelled = true }\n }, [projectId])\n\n return (\n <div className=\"flex flex-col gap-3\">\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'staff.time_tracking.projects.team.hint',\n 'Without an assignment only the Team Leader can see the project.',\n )}\n </p>\n {projectId && names.length > 0 ? (\n <div className=\"flex flex-wrap gap-2\">\n {names.map((name) => (\n <span\n key={name}\n className=\"inline-flex items-center gap-2 rounded-full border border-border bg-muted px-2 py-1 text-xs text-foreground\"\n >\n <Avatar label={name} size=\"sm\" />\n {name}\n </span>\n ))}\n </div>\n ) : null}\n {projectId && !loading && names.length === 0 ? (\n <p className=\"text-xs text-muted-foreground\">\n {t('staff.time_tracking.projects.team.empty', 'Nobody is assigned yet.')}\n </p>\n ) : null}\n {projectId ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" asChild>\n <Link href={`/backend/staff/time-tracking/projects/${projectId}?panel=team`}>\n <Users className=\"h-4 w-4\" aria-hidden />\n {t('staff.time_tracking.projects.team.manage', 'Manage team')}\n </Link>\n </Button>\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n {t('staff.time_tracking.projects.team.afterCreate', 'You can assign people once the project is saved.')}\n </p>\n )}\n </div>\n )\n}\n\ntype RoundingSettingsResponse = {\n rounding?: { unitMinutes?: unknown; direction?: unknown } | null\n}\n\nexport function describeRounding(\n unitMinutes: number,\n direction: string,\n t: TranslateFn,\n): string {\n if (!unitMinutes) return t('staff.time_tracking.projects.rounding.none', 'no rounding')\n return direction === 'nearest'\n ? t('staff.time_tracking.projects.rounding.nearest', 'to the nearest {{unit}} min', { unit: String(unitMinutes) })\n : t('staff.time_tracking.projects.rounding.up', 'up to {{unit}} min', { unit: String(unitMinutes) })\n}\n\n/**\n * Screen 4's info alert: rounding is global (\u00A710 fixes it there, with no\n * per-project override), so the form names the current tenant setting and points\n * at module settings rather than offering a field.\n */\nexport function ProjectRoundingNotice() {\n const t = useT()\n const [summary, setSummary] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n try {\n const res = await apiCall<RoundingSettingsResponse>('/api/staff/timesheets/settings')\n const rounding = res.result?.rounding\n const unitMinutes = typeof rounding?.unitMinutes === 'number' ? rounding.unitMinutes : 0\n const direction = typeof rounding?.direction === 'string' ? rounding.direction : 'up'\n if (!cancelled) setSummary(describeRounding(unitMinutes, direction, t))\n } catch {\n if (!cancelled) setSummary(null)\n }\n }\n void load()\n return () => { cancelled = true }\n }, [t])\n\n return (\n <Alert status=\"information\" style=\"lighter\" size=\"default\">\n <div className=\"flex flex-col\">\n <AlertTitle>\n {t('staff.time_tracking.projects.rounding.title', 'Rounding is global')}\n </AlertTitle>\n <AlertDescription>\n {summary\n ? t(\n 'staff.time_tracking.projects.rounding.body',\n 'Currently: {{summary}}. The change applies to every project \u2014 set it in the module settings.',\n { summary },\n )\n : t(\n 'staff.time_tracking.projects.rounding.bodyUnknown',\n 'The change applies to every project \u2014 set it in the module settings.',\n )}\n </AlertDescription>\n </div>\n </Alert>\n )\n}\n\n/** Exported for unit tests only. */\nexport const __testing = { memberLabel }\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Users } from 'lucide-react'\nimport { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'\nimport { Avatar } from '@open-mercato/ui/primitives/avatar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'\nimport { SegmentedControl, SegmentedControlItem } from '@open-mercato/ui/primitives/segmented-control'\nimport { SwitchField } from '@open-mercato/ui/primitives/switch-field'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { resolveGraceDays } from './projectTeamAssignment'\nimport {\n DEFAULT_ASSIGNEE_PAGE_SIZE,\n chunkIds,\n classifyDefaultAssigneeCandidates,\n collectProjectMemberships,\n readProjectMembershipPage,\n resolveSelectedDefaultAssignee,\n type ProjectMemberRow,\n} from './projectDefaultAssignee'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\n\nexport type ProjectBudgetKind = 'none' | 'hours' | 'amount'\n\nexport const DEFAULT_BUDGET_WARN_AT_PERCENT = 80\n\nexport type BudgetSectionProps = {\n enabled: boolean\n kind: ProjectBudgetKind\n value: string\n warnAtPercent: string\n currencyCode: string | null\n disabled?: boolean\n onChange: (patch: {\n enabled?: boolean\n kind?: ProjectBudgetKind\n value?: string\n warnAtPercent?: string\n }) => void\n}\n\n/**\n * Screen 4, \"Bud\u017Cet\" \u2014 first-class scope per the spec (\u00A79 promoted out of\n * \"proposal\"). The switch maps to `budgetKind: 'none'`; turning it on restores\n * the last picked kind so a toggle never silently discards the limit.\n */\nexport function ProjectBudgetSection({\n enabled,\n kind,\n value,\n warnAtPercent,\n currencyCode,\n disabled,\n onChange,\n}: BudgetSectionProps) {\n const t = useT()\n const unitLabel = kind === 'amount'\n ? (currencyCode ?? t('staff.time_tracking.projects.budget.amountUnit', 'amount'))\n : t('staff.time_tracking.projects.budget.hoursUnit', 'h')\n\n return (\n <div className=\"flex flex-col gap-4\">\n <SwitchField\n checked={enabled}\n disabled={disabled}\n onCheckedChange={(next) => onChange({ enabled: next === true, kind: next === true && kind === 'none' ? 'hours' : kind })}\n label={t('staff.time_tracking.projects.budget.enable', 'Track the limit')}\n description={t(\n 'staff.time_tracking.projects.budget.enableHint',\n 'Warns at {{percent}}% and 100% of the limit.',\n { percent: warnAtPercent || String(DEFAULT_BUDGET_WARN_AT_PERCENT) },\n )}\n />\n\n {enabled ? (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div className=\"flex flex-col gap-1.5\">\n <Label>{t('staff.time_tracking.projects.budget.kind', 'Limit type')}</Label>\n <SegmentedControl\n value={kind === 'none' ? 'hours' : kind}\n disabled={disabled}\n onValueChange={(next) => onChange({ kind: next as ProjectBudgetKind })}\n aria-label={t('staff.time_tracking.projects.budget.kind', 'Limit type')}\n >\n <SegmentedControlItem value=\"hours\">\n {t('staff.time_tracking.projects.budget.kindHours', 'Hours')}\n </SegmentedControlItem>\n <SegmentedControlItem value=\"amount\">\n {t('staff.time_tracking.projects.budget.kindAmount', 'Amount')}\n </SegmentedControlItem>\n </SegmentedControl>\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"tt-budget-value\">\n {t('staff.time_tracking.projects.budget.value', 'Limit')}\n </Label>\n <div className=\"flex items-center gap-2\">\n <Input\n id=\"tt-budget-value\"\n inputMode=\"decimal\"\n value={value}\n disabled={disabled}\n onChange={(event) => onChange({ value: event.target.value })}\n />\n <span className=\"shrink-0 text-sm text-muted-foreground\">{unitLabel}</span>\n </div>\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"tt-budget-warn\">\n {t('staff.time_tracking.projects.budget.warnAt', 'Warn at')}\n </Label>\n <div className=\"flex items-center gap-2\">\n <Input\n id=\"tt-budget-warn\"\n inputMode=\"numeric\"\n value={warnAtPercent}\n disabled={disabled}\n onChange={(event) => onChange({ warnAtPercent: event.target.value })}\n />\n <span className=\"shrink-0 text-sm text-muted-foreground\">%</span>\n </div>\n </div>\n </div>\n ) : null}\n </div>\n )\n}\n\nexport { isUsableDefaultAssigneeMembership } from './projectDefaultAssignee'\n\ntype TeamMemberRow = {\n id?: unknown\n firstName?: unknown\n lastName?: unknown\n first_name?: unknown\n last_name?: unknown\n displayName?: unknown\n display_name?: unknown\n email?: unknown\n}\n\nfunction readString(value: unknown): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null\n}\n\n/**\n * `/api/staff/team-members` answers in snake_case, so `display_name` is the key\n * that actually arrives \u2014 reading only `displayName` fell through every branch\n * and rendered the raw staff-member UUID as the chip label.\n *\n * `StaffTeamMember` carries no first/last/email columns; those branches describe a\n * shape this endpoint never returns and are kept only for callers that pass a\n * different row in. An unnamed row yields an empty string rather than an id: the\n * caller filters empties out, so a missing name shows nothing instead of leaking\n * an internal identifier into the UI.\n */\nfunction memberLabel(row: TeamMemberRow): string {\n const display = readString(row.displayName) ?? readString(row.display_name)\n if (display) return display\n const first = readString(row.firstName) ?? readString(row.first_name)\n const last = readString(row.lastName) ?? readString(row.last_name)\n const joined = [first, last].filter(Boolean).join(' ')\n return joined || readString(row.email) || ''\n}\n\nexport type ProjectTeamSectionProps = {\n projectId?: string | null\n}\n\n/**\n * Screen 4's right column: the assigned team as chips plus the entry point into\n * the team drawer. The drawer itself is a separate task and lives behind\n * `?panel=team` on the project route.\n */\nexport function ProjectTeamSection({ projectId }: ProjectTeamSectionProps) {\n const t = useT()\n const [names, setNames] = React.useState<string[]>([])\n const [loading, setLoading] = React.useState(Boolean(projectId))\n\n React.useEffect(() => {\n if (!projectId) {\n setNames([])\n setLoading(false)\n return\n }\n let cancelled = false\n async function load() {\n setLoading(true)\n try {\n const membersRes = await apiCall<{ items?: ProjectMemberRow[] }>(\n `/api/staff/timesheets/time-projects/${projectId}/employees?pageSize=100&status=active`,\n )\n const memberIds = (Array.isArray(membersRes.result?.items) ? membersRes.result.items : [])\n .map((row) => readString(row.staffMemberId) ?? readString(row.staff_member_id))\n .filter((id): id is string => id !== null)\n if (memberIds.length === 0) {\n if (!cancelled) setNames([])\n return\n }\n const staffRes = await apiCall<{ items?: TeamMemberRow[] }>(\n `/api/staff/team-members?ids=${memberIds.join(',')}&pageSize=100`,\n )\n const rows = Array.isArray(staffRes.result?.items) ? staffRes.result.items : []\n if (!cancelled) setNames(rows.map(memberLabel).filter((name) => name.length > 0))\n } catch {\n if (!cancelled) setNames([])\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n void load()\n return () => { cancelled = true }\n }, [projectId])\n\n return (\n <div className=\"flex flex-col gap-3\">\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'staff.time_tracking.projects.team.hint',\n 'Without an assignment only the Team Leader can see the project.',\n )}\n </p>\n {projectId && names.length > 0 ? (\n <div className=\"flex flex-wrap gap-2\">\n {names.map((name) => (\n <span\n key={name}\n className=\"inline-flex items-center gap-2 rounded-full border border-border bg-muted px-2 py-1 text-xs text-foreground\"\n >\n <Avatar label={name} size=\"sm\" />\n {name}\n </span>\n ))}\n </div>\n ) : null}\n {projectId && !loading && names.length === 0 ? (\n <p className=\"text-xs text-muted-foreground\">\n {t('staff.time_tracking.projects.team.empty', 'Nobody is assigned yet.')}\n </p>\n ) : null}\n {projectId ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" asChild>\n <Link href={`/backend/staff/time-tracking/projects/${projectId}?panel=team`}>\n <Users className=\"h-4 w-4\" aria-hidden />\n {t('staff.time_tracking.projects.team.manage', 'Manage team')}\n </Link>\n </Button>\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n {t('staff.time_tracking.projects.team.afterCreate', 'You can assign people once the project is saved.')}\n </p>\n )}\n </div>\n )\n}\n\ntype RoundingSettingsResponse = {\n rounding?: { unitMinutes?: unknown; direction?: unknown } | null\n}\n\nexport function describeRounding(\n unitMinutes: number,\n direction: string,\n t: TranslateFn,\n): string {\n if (!unitMinutes) return t('staff.time_tracking.projects.rounding.none', 'no rounding')\n return direction === 'nearest'\n ? t('staff.time_tracking.projects.rounding.nearest', 'to the nearest {{unit}} min', { unit: String(unitMinutes) })\n : t('staff.time_tracking.projects.rounding.up', 'up to {{unit}} min', { unit: String(unitMinutes) })\n}\n\n/**\n * Screen 4's info alert: rounding is global (\u00A710 fixes it there, with no\n * per-project override), so the form names the current tenant setting and points\n * at module settings rather than offering a field.\n */\nexport function ProjectRoundingNotice() {\n const t = useT()\n const [summary, setSummary] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n try {\n const res = await apiCall<RoundingSettingsResponse>('/api/staff/timesheets/settings')\n const rounding = res.result?.rounding\n const unitMinutes = typeof rounding?.unitMinutes === 'number' ? rounding.unitMinutes : 0\n const direction = typeof rounding?.direction === 'string' ? rounding.direction : 'up'\n if (!cancelled) setSummary(describeRounding(unitMinutes, direction, t))\n } catch {\n if (!cancelled) setSummary(null)\n }\n }\n void load()\n return () => { cancelled = true }\n }, [t])\n\n return (\n <Alert status=\"information\" style=\"lighter\" size=\"default\">\n <div className=\"flex flex-col\">\n <AlertTitle>\n {t('staff.time_tracking.projects.rounding.title', 'Rounding is global')}\n </AlertTitle>\n <AlertDescription>\n {summary\n ? t(\n 'staff.time_tracking.projects.rounding.body',\n 'Currently: {{summary}}. The change applies to every project \u2014 set it in the module settings.',\n { summary },\n )\n : t(\n 'staff.time_tracking.projects.rounding.bodyUnknown',\n 'The change applies to every project \u2014 set it in the module settings.',\n )}\n </AlertDescription>\n </div>\n </Alert>\n )\n}\n\n/** Exported for unit tests only. */\nexport const __testing = { memberLabel }\n\nconst NO_DEFAULT_ASSIGNEE = '__none__'\n\nexport type ProjectDefaultAssigneeFieldProps = {\n projectId: string | null\n value: string | null\n onChange: (value: string | null) => void\n disabled?: boolean\n}\n\ntype DefaultAssigneeOption = { id: string; label: string; active: boolean }\n\n/**\n * The project's default assignee for customer portal tickets. Only active project\n * members are offered; a stored default whose membership has since gone inactive or\n * been removed stays visible with a warning, because new tickets then fall to the\n * project's assigners instead of failing.\n */\nexport function ProjectDefaultAssigneeField({ projectId, value, onChange, disabled }: ProjectDefaultAssigneeFieldProps) {\n const t = useT()\n const [options, setOptions] = React.useState<DefaultAssigneeOption[]>([])\n const [loadState, setLoadState] = React.useState<'loading' | 'loaded' | 'failed'>('loading')\n const [reloadToken, setReloadToken] = React.useState(0)\n const [truncated, setTruncated] = React.useState(false)\n const [extraLabels, setExtraLabels] = React.useState<Map<string, string>>(new Map())\n const valueRef = React.useRef(value)\n valueRef.current = value\n\n React.useEffect(() => {\n if (!projectId) {\n setOptions([])\n setTruncated(false)\n setExtraLabels(new Map())\n setLoadState('loaded')\n return\n }\n let cancelled = false\n setLoadState('loading')\n async function load() {\n try {\n const [collection, settingsRes] = await Promise.all([\n collectProjectMemberships(async (page) => {\n const membersRes = await apiCall<{ items?: unknown; totalPages?: unknown }>(\n `/api/staff/timesheets/time-projects/${projectId}/employees?page=${page}&pageSize=${DEFAULT_ASSIGNEE_PAGE_SIZE}`,\n )\n return readProjectMembershipPage(membersRes)\n }),\n apiCall<{ access?: { assignmentGraceDays?: unknown } | null }>('/api/staff/timesheets/settings'),\n ])\n const graceDays = resolveGraceDays(settingsRes.ok ? settingsRes.result?.access?.assignmentGraceDays : null)\n const candidates = classifyDefaultAssigneeCandidates(collection.rows, graceDays)\n if (!cancelled) setTruncated(collection.truncated)\n const selectedId = valueRef.current\n const labelIds = Array.from(new Set([\n ...candidates.map((candidate) => candidate.staffMemberId),\n ...(selectedId ? [selectedId] : []),\n ]))\n if (labelIds.length === 0) {\n if (!cancelled) {\n setOptions([])\n setExtraLabels(new Map())\n setLoadState('loaded')\n }\n return\n }\n const staffResults = await Promise.all(\n chunkIds(labelIds).map((ids) =>\n apiCall<{ items?: TeamMemberRow[] }>(\n `/api/staff/team-members?ids=${ids.join(',')}&pageSize=${DEFAULT_ASSIGNEE_PAGE_SIZE}`,\n ),\n ),\n )\n const staffRows = staffResults.flatMap((staffRes) => (Array.isArray(staffRes.result?.items) ? staffRes.result.items : []))\n const labels = new Map(\n staffRows\n .map((row) => [readString(row.id), memberLabel(row)] as const)\n .filter((entry): entry is readonly [string, string] => entry[0] !== null),\n )\n if (!cancelled) {\n setOptions(candidates.map((candidate) => ({\n id: candidate.staffMemberId,\n label: labels.get(candidate.staffMemberId) || candidate.staffMemberId,\n active: candidate.active,\n })))\n setExtraLabels(labels)\n setLoadState('loaded')\n }\n } catch {\n if (!cancelled) setLoadState('failed')\n }\n }\n void load()\n return () => { cancelled = true }\n }, [projectId, reloadToken])\n\n const retryLoad = React.useCallback(() => setReloadToken((token) => token + 1), [])\n\n const selection = resolveSelectedDefaultAssignee(\n options.map((option) => ({ staffMemberId: option.id, active: option.active })),\n value,\n truncated,\n )\n const selected = value ? options.find((option) => option.id === value) ?? null : null\n const selectedLabel = selected?.label ?? (value ? extraLabels.get(value) || value : null)\n const loadFailed = loadState === 'failed'\n const activeOptions = loadFailed ? [] : options.filter((option) => option.active)\n const showWarning = loadState === 'loaded' && selection.unusable\n\n return (\n <div className=\"flex flex-col gap-2\" data-testid=\"project-default-assignee\">\n <Label htmlFor=\"project-default-assignee-select\">\n {t('staff.time_tracking.assigners.defaultAssignee', 'Default assignee for portal tickets')}\n </Label>\n <Select\n value={value ?? NO_DEFAULT_ASSIGNEE}\n disabled={disabled || !projectId}\n onValueChange={(next) => onChange(next === NO_DEFAULT_ASSIGNEE ? null : next)}\n >\n <SelectTrigger id=\"project-default-assignee-select\" data-testid=\"project-default-assignee-select\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={NO_DEFAULT_ASSIGNEE}>\n {t('staff.time_tracking.assigners.defaultAssigneeNone', 'No default assignee')}\n </SelectItem>\n {activeOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n {value && (loadFailed || !selected || !selected.active) ? (\n <SelectItem value={value}>{selectedLabel ?? value}</SelectItem>\n ) : null}\n </SelectContent>\n </Select>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'staff.time_tracking.assigners.defaultAssigneeHelp',\n 'New customer portal tickets routed to this project are assigned to this member. Tasks created on the board stay assigned to their creator.',\n )}\n </p>\n {loadFailed ? (\n <ErrorMessage\n label={t(\n 'staff.time_tracking.assigners.defaultAssigneeLoadFailed',\n 'Could not load the project members, so the default assignee cannot be checked right now.',\n )}\n action={(\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={retryLoad} data-testid=\"project-default-assignee-retry\">\n {t('staff.time_tracking.assigners.defaultAssigneeRetry', 'Retry')}\n </Button>\n )}\n />\n ) : null}\n {showWarning ? (\n <Alert status=\"warning\">\n <AlertDescription>\n {t(\n 'staff.time_tracking.assigners.defaultAssigneeInactive',\n 'This member is no longer an active project member. New tickets will be left unassigned and the assigners notified.',\n )}\n </AlertDescription>\n </Alert>\n ) : null}\n {!projectId ? (\n <p className=\"text-xs text-muted-foreground\">\n {t('staff.time_tracking.assigners.defaultAssigneeAfterCreate', 'Save the project and add members to choose a default assignee.')}\n </p>\n ) : null}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { Download, Paperclip } from "lucide-react";
|
|
6
|
+
import { Badge } from "@open-mercato/ui/primitives/badge";
|
|
7
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
8
|
+
import { Checkbox } from "@open-mercato/ui/primitives/checkbox";
|
|
9
|
+
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
10
|
+
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
11
|
+
import { ErrorMessage, LoadingMessage } from "@open-mercato/ui/backend/detail";
|
|
12
|
+
import { useBackendChrome } from "@open-mercato/ui/backend/BackendChromeProvider";
|
|
13
|
+
import { hasFeature } from "@open-mercato/shared/security/features";
|
|
14
|
+
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
15
|
+
import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
|
|
16
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
17
|
+
import { useOrganizationScopeVersion } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
|
|
18
|
+
import { TASK_DRAWER_QUERY_ROOT } from "./taskDrawerData.js";
|
|
19
|
+
const ENTITY_ID = "staff:staff_time_task";
|
|
20
|
+
const CUSTOMER_VISIBLE_TAG = "customer-visible";
|
|
21
|
+
const CUSTOMER_UPLOAD_PREFIX = "customer-upload:";
|
|
22
|
+
const MUTATION_CONTEXT_ID = "staff.task_drawer.support_attachments";
|
|
23
|
+
const ATTACHMENTS_MANAGE_FEATURE = "attachments.manage";
|
|
24
|
+
function formatSize(bytes) {
|
|
25
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
|
|
26
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
27
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
28
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
29
|
+
}
|
|
30
|
+
function SupportAttachmentsSection({ taskId }) {
|
|
31
|
+
const t = useT();
|
|
32
|
+
const { runMutation, retryLastMutation } = useGuardedMutation({
|
|
33
|
+
contextId: MUTATION_CONTEXT_ID,
|
|
34
|
+
blockedMessage: t("staff.support.attachments.uploadFailed", "Could not upload the file.")
|
|
35
|
+
});
|
|
36
|
+
const { payload } = useBackendChrome();
|
|
37
|
+
const canUpload = hasFeature(payload?.grantedFeatures, ATTACHMENTS_MANAGE_FEATURE);
|
|
38
|
+
const queryClient = useQueryClient();
|
|
39
|
+
const scopeVersion = useOrganizationScopeVersion();
|
|
40
|
+
const [uploading, setUploading] = React.useState(false);
|
|
41
|
+
const [share, setShare] = React.useState(false);
|
|
42
|
+
const inputRef = React.useRef(null);
|
|
43
|
+
const attachmentsQueryKey = React.useMemo(
|
|
44
|
+
() => [...TASK_DRAWER_QUERY_ROOT, "attachments", `scope:${scopeVersion}`, taskId],
|
|
45
|
+
[scopeVersion, taskId]
|
|
46
|
+
);
|
|
47
|
+
const attachmentsQuery = useQuery({
|
|
48
|
+
queryKey: attachmentsQueryKey,
|
|
49
|
+
enabled: taskId.length > 0,
|
|
50
|
+
queryFn: async () => {
|
|
51
|
+
const response = await apiCall(
|
|
52
|
+
`/api/attachments?entityId=${encodeURIComponent(ENTITY_ID)}&recordId=${encodeURIComponent(taskId)}`
|
|
53
|
+
);
|
|
54
|
+
if (!response.ok) throw new Error("[internal] attachments request failed");
|
|
55
|
+
return response.result?.items ?? [];
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
const items = attachmentsQuery.data ?? [];
|
|
59
|
+
const refreshAttachments = React.useCallback(async () => {
|
|
60
|
+
await queryClient.invalidateQueries({ queryKey: attachmentsQueryKey });
|
|
61
|
+
}, [attachmentsQueryKey, queryClient]);
|
|
62
|
+
const upload = React.useCallback(async (list) => {
|
|
63
|
+
if (!list || !list.length || !canUpload) return;
|
|
64
|
+
setUploading(true);
|
|
65
|
+
try {
|
|
66
|
+
for (const file of Array.from(list)) {
|
|
67
|
+
const form = new FormData();
|
|
68
|
+
form.set("entityId", ENTITY_ID);
|
|
69
|
+
form.set("recordId", taskId);
|
|
70
|
+
form.set("file", file);
|
|
71
|
+
if (share) form.set("tags", JSON.stringify([CUSTOMER_VISIBLE_TAG]));
|
|
72
|
+
const result = await runMutation({
|
|
73
|
+
operation: () => apiCall("/api/attachments", { method: "POST", body: form }),
|
|
74
|
+
// Like the rest of the task drawer, an upload carries no record-lock
|
|
75
|
+
// resource: it adds a row rather than editing one.
|
|
76
|
+
context: {
|
|
77
|
+
formId: `${MUTATION_CONTEXT_ID}:${taskId}`,
|
|
78
|
+
retryLastMutation
|
|
79
|
+
},
|
|
80
|
+
mutationPayload: { taskId, fileName: file.name, shared: share }
|
|
81
|
+
});
|
|
82
|
+
if (!result.ok) flash(t("staff.support.attachments.uploadFailed", "Could not upload the file."), "error");
|
|
83
|
+
}
|
|
84
|
+
await refreshAttachments();
|
|
85
|
+
} catch {
|
|
86
|
+
flash(t("staff.support.attachments.uploadFailed", "Could not upload the file."), "error");
|
|
87
|
+
} finally {
|
|
88
|
+
setUploading(false);
|
|
89
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
90
|
+
}
|
|
91
|
+
}, [canUpload, refreshAttachments, retryLastMutation, runMutation, share, t, taskId]);
|
|
92
|
+
return /* @__PURE__ */ jsxs("section", { className: "space-y-2", "data-testid": "support-attachments-section", children: [
|
|
93
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
94
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-foreground", children: t("staff.support.attachments.title", "Ticket files") }),
|
|
95
|
+
canUpload ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
96
|
+
/* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
|
|
97
|
+
/* @__PURE__ */ jsx(
|
|
98
|
+
Checkbox,
|
|
99
|
+
{
|
|
100
|
+
checked: share,
|
|
101
|
+
onCheckedChange: (value) => setShare(value === true),
|
|
102
|
+
"aria-label": t("staff.support.attachments.shareWithCustomer", "Share with customer")
|
|
103
|
+
}
|
|
104
|
+
),
|
|
105
|
+
t("staff.support.attachments.shareWithCustomer", "Share with customer")
|
|
106
|
+
] }),
|
|
107
|
+
/* @__PURE__ */ jsx(
|
|
108
|
+
"input",
|
|
109
|
+
{
|
|
110
|
+
ref: inputRef,
|
|
111
|
+
type: "file",
|
|
112
|
+
multiple: true,
|
|
113
|
+
className: "sr-only",
|
|
114
|
+
"data-testid": "support-attachments-input",
|
|
115
|
+
onChange: (event) => void upload(event.target.files)
|
|
116
|
+
}
|
|
117
|
+
),
|
|
118
|
+
/* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: uploading, onClick: () => inputRef.current?.click(), children: [
|
|
119
|
+
uploading ? /* @__PURE__ */ jsx(Spinner, { className: "mr-2 h-4 w-4" }) : /* @__PURE__ */ jsx(Paperclip, { className: "mr-2 h-4 w-4" }),
|
|
120
|
+
t("staff.support.attachments.upload", "Upload")
|
|
121
|
+
] })
|
|
122
|
+
] }) : null
|
|
123
|
+
] }),
|
|
124
|
+
attachmentsQuery.isLoading ? /* @__PURE__ */ jsx(LoadingMessage, { label: t("staff.support.attachments.loading", "Loading files\u2026") }) : attachmentsQuery.isError ? /* @__PURE__ */ jsx(
|
|
125
|
+
ErrorMessage,
|
|
126
|
+
{
|
|
127
|
+
label: t("staff.support.attachments.loadFailed", "Could not load the ticket files."),
|
|
128
|
+
action: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => {
|
|
129
|
+
void attachmentsQuery.refetch();
|
|
130
|
+
}, children: t("staff.support.attachments.retry", "Try again") })
|
|
131
|
+
}
|
|
132
|
+
) : items.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("staff.support.attachments.empty", "No files on this ticket.") }) : /* @__PURE__ */ jsx("ul", { className: "space-y-1", children: items.map((item) => {
|
|
133
|
+
const tags = item.tags ?? [];
|
|
134
|
+
const visible = tags.includes(CUSTOMER_VISIBLE_TAG);
|
|
135
|
+
const fromCustomer = tags.some((tag) => tag.startsWith(CUSTOMER_UPLOAD_PREFIX));
|
|
136
|
+
return /* @__PURE__ */ jsxs("li", { className: "flex items-center justify-between gap-2 text-sm", children: [
|
|
137
|
+
/* @__PURE__ */ jsxs("a", { className: "flex min-w-0 items-center gap-2 text-foreground hover:underline", href: item.url, children: [
|
|
138
|
+
/* @__PURE__ */ jsx(Download, { className: "h-4 w-4 shrink-0" }),
|
|
139
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: item.fileName }),
|
|
140
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground", children: formatSize(item.fileSize) })
|
|
141
|
+
] }),
|
|
142
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 gap-1", children: [
|
|
143
|
+
fromCustomer ? /* @__PURE__ */ jsx(Badge, { variant: "outline", children: t("staff.support.attachments.fromCustomer", "From customer") }) : null,
|
|
144
|
+
/* @__PURE__ */ jsx(Badge, { variant: visible ? "secondary" : "outline", children: visible ? t("staff.support.attachments.visible", "Visible to customer") : t("staff.support.attachments.internal", "Internal") })
|
|
145
|
+
] })
|
|
146
|
+
] }, item.id);
|
|
147
|
+
}) })
|
|
148
|
+
] });
|
|
149
|
+
}
|
|
150
|
+
export {
|
|
151
|
+
SupportAttachmentsSection
|
|
152
|
+
};
|
|
153
|
+
//# sourceMappingURL=SupportAttachmentsSection.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { Download, Paperclip } from 'lucide-react'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { TASK_DRAWER_QUERY_ROOT } from './taskDrawerData'\n\nconst ENTITY_ID = 'staff:staff_time_task'\nconst CUSTOMER_VISIBLE_TAG = 'customer-visible'\nconst CUSTOMER_UPLOAD_PREFIX = 'customer-upload:'\nconst MUTATION_CONTEXT_ID = 'staff.task_drawer.support_attachments'\nconst ATTACHMENTS_MANAGE_FEATURE = 'attachments.manage'\n\ntype StaffAttachment = {\n id: string\n url: string\n fileName: string\n fileSize: number\n tags?: string[]\n}\n\ntype SupportAttachmentsSectionProps = {\n taskId: string\n}\n\nfunction formatSize(bytes: number): string {\n if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'\n if (bytes < 1024) return `${bytes} B`\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n}\n\n/**\n * Files on a portal ticket. Customer uploads and files staff chose to share carry\n * the `customer-visible` tag and are the only ones the portal lists; everything\n * else stays internal. \"Share with customer\" is unchecked by default.\n */\nexport function SupportAttachmentsSection({ taskId }: SupportAttachmentsSectionProps) {\n const t = useT()\n const { runMutation, retryLastMutation } = useGuardedMutation<Record<string, unknown>>({\n contextId: MUTATION_CONTEXT_ID,\n blockedMessage: t('staff.support.attachments.uploadFailed', 'Could not upload the file.'),\n })\n const { payload } = useBackendChrome()\n const canUpload = hasFeature(payload?.grantedFeatures, ATTACHMENTS_MANAGE_FEATURE)\n const queryClient = useQueryClient()\n const scopeVersion = useOrganizationScopeVersion()\n const [uploading, setUploading] = React.useState(false)\n const [share, setShare] = React.useState(false)\n const inputRef = React.useRef<HTMLInputElement | null>(null)\n\n const attachmentsQueryKey = React.useMemo(\n () => [...TASK_DRAWER_QUERY_ROOT, 'attachments', `scope:${scopeVersion}`, taskId],\n [scopeVersion, taskId],\n )\n const attachmentsQuery = useQuery<StaffAttachment[]>({\n queryKey: attachmentsQueryKey,\n enabled: taskId.length > 0,\n queryFn: async () => {\n const response = await apiCall<{ items?: StaffAttachment[] }>(\n `/api/attachments?entityId=${encodeURIComponent(ENTITY_ID)}&recordId=${encodeURIComponent(taskId)}`,\n )\n if (!response.ok) throw new Error('[internal] attachments request failed')\n return response.result?.items ?? []\n },\n })\n const items = attachmentsQuery.data ?? []\n\n const refreshAttachments = React.useCallback(async () => {\n await queryClient.invalidateQueries({ queryKey: attachmentsQueryKey })\n }, [attachmentsQueryKey, queryClient])\n\n const upload = React.useCallback(async (list: FileList | null) => {\n if (!list || !list.length || !canUpload) return\n setUploading(true)\n try {\n for (const file of Array.from(list)) {\n const form = new FormData()\n form.set('entityId', ENTITY_ID)\n form.set('recordId', taskId)\n form.set('file', file)\n if (share) form.set('tags', JSON.stringify([CUSTOMER_VISIBLE_TAG]))\n const result = await runMutation({\n operation: () => apiCall('/api/attachments', { method: 'POST', body: form }),\n // Like the rest of the task drawer, an upload carries no record-lock\n // resource: it adds a row rather than editing one.\n context: {\n formId: `${MUTATION_CONTEXT_ID}:${taskId}`,\n retryLastMutation,\n },\n mutationPayload: { taskId, fileName: file.name, shared: share },\n })\n if (!result.ok) flash(t('staff.support.attachments.uploadFailed', 'Could not upload the file.'), 'error')\n }\n await refreshAttachments()\n } catch {\n flash(t('staff.support.attachments.uploadFailed', 'Could not upload the file.'), 'error')\n } finally {\n setUploading(false)\n if (inputRef.current) inputRef.current.value = ''\n }\n }, [canUpload, refreshAttachments, retryLastMutation, runMutation, share, t, taskId])\n\n return (\n <section className=\"space-y-2\" data-testid=\"support-attachments-section\">\n <div className=\"flex items-center justify-between gap-2\">\n <h3 className=\"text-sm font-semibold text-foreground\">{t('staff.support.attachments.title', 'Ticket files')}</h3>\n {canUpload ? (\n <div className=\"flex items-center gap-3\">\n <label className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Checkbox\n checked={share}\n onCheckedChange={(value) => setShare(value === true)}\n aria-label={t('staff.support.attachments.shareWithCustomer', 'Share with customer')}\n />\n {t('staff.support.attachments.shareWithCustomer', 'Share with customer')}\n </label>\n <input\n ref={inputRef}\n type=\"file\"\n multiple\n className=\"sr-only\"\n data-testid=\"support-attachments-input\"\n onChange={(event) => void upload(event.target.files)}\n />\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={uploading} onClick={() => inputRef.current?.click()}>\n {uploading ? <Spinner className=\"mr-2 h-4 w-4\" /> : <Paperclip className=\"mr-2 h-4 w-4\" />}\n {t('staff.support.attachments.upload', 'Upload')}\n </Button>\n </div>\n ) : null}\n </div>\n {attachmentsQuery.isLoading ? (\n <LoadingMessage label={t('staff.support.attachments.loading', 'Loading files\u2026')} />\n ) : attachmentsQuery.isError ? (\n <ErrorMessage\n label={t('staff.support.attachments.loadFailed', 'Could not load the ticket files.')}\n action={(\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => { void attachmentsQuery.refetch() }}>\n {t('staff.support.attachments.retry', 'Try again')}\n </Button>\n )}\n />\n ) : items.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">{t('staff.support.attachments.empty', 'No files on this ticket.')}</p>\n ) : (\n <ul className=\"space-y-1\">\n {items.map((item) => {\n const tags = item.tags ?? []\n const visible = tags.includes(CUSTOMER_VISIBLE_TAG)\n const fromCustomer = tags.some((tag) => tag.startsWith(CUSTOMER_UPLOAD_PREFIX))\n return (\n <li key={item.id} className=\"flex items-center justify-between gap-2 text-sm\">\n <a className=\"flex min-w-0 items-center gap-2 text-foreground hover:underline\" href={item.url}>\n <Download className=\"h-4 w-4 shrink-0\" />\n <span className=\"truncate\">{item.fileName}</span>\n <span className=\"shrink-0 text-muted-foreground\">{formatSize(item.fileSize)}</span>\n </a>\n <div className=\"flex shrink-0 gap-1\">\n {fromCustomer ? (\n <Badge variant=\"outline\">{t('staff.support.attachments.fromCustomer', 'From customer')}</Badge>\n ) : null}\n <Badge variant={visible ? 'secondary' : 'outline'}>\n {visible\n ? t('staff.support.attachments.visible', 'Visible to customer')\n : t('staff.support.attachments.internal', 'Internal')}\n </Badge>\n </div>\n </li>\n )\n })}\n </ul>\n )}\n </section>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAsHQ,cAGI,YAHJ;AApHR,YAAY,WAAW;AACvB,SAAS,UAAU,sBAAsB;AACzC,SAAS,UAAU,iBAAiB;AACpC,SAAS,aAAa;AACtB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,eAAe;AACxB,SAAS,aAAa;AACtB,SAAS,cAAc,sBAAsB;AAC7C,SAAS,wBAAwB;AACjC,SAAS,kBAAkB;AAC3B,SAAS,eAAe;AACxB,SAAS,0BAA0B;AACnC,SAAS,YAAY;AACrB,SAAS,mCAAmC;AAC5C,SAAS,8BAA8B;AAEvC,MAAM,YAAY;AAClB,MAAM,uBAAuB;AAC7B,MAAM,yBAAyB;AAC/B,MAAM,sBAAsB;AAC5B,MAAM,6BAA6B;AAcnC,SAAS,WAAW,OAAuB;AACzC,MAAI,CAAC,OAAO,SAAS,KAAK,KAAK,SAAS,EAAG,QAAO;AAClD,MAAI,QAAQ,KAAM,QAAO,GAAG,KAAK;AACjC,MAAI,QAAQ,OAAO,KAAM,QAAO,IAAI,QAAQ,MAAM,QAAQ,CAAC,CAAC;AAC5D,SAAO,IAAI,SAAS,OAAO,OAAO,QAAQ,CAAC,CAAC;AAC9C;AAOO,SAAS,0BAA0B,EAAE,OAAO,GAAmC;AACpF,QAAM,IAAI,KAAK;AACf,QAAM,EAAE,aAAa,kBAAkB,IAAI,mBAA4C;AAAA,IACrF,WAAW;AAAA,IACX,gBAAgB,EAAE,0CAA0C,4BAA4B;AAAA,EAC1F,CAAC;AACD,QAAM,EAAE,QAAQ,IAAI,iBAAiB;AACrC,QAAM,YAAY,WAAW,SAAS,iBAAiB,0BAA0B;AACjF,QAAM,cAAc,eAAe;AACnC,QAAM,eAAe,4BAA4B;AACjD,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,KAAK;AACtD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,KAAK;AAC9C,QAAM,WAAW,MAAM,OAAgC,IAAI;AAE3D,QAAM,sBAAsB,MAAM;AAAA,IAChC,MAAM,CAAC,GAAG,wBAAwB,eAAe,SAAS,YAAY,IAAI,MAAM;AAAA,IAChF,CAAC,cAAc,MAAM;AAAA,EACvB;AACA,QAAM,mBAAmB,SAA4B;AAAA,IACnD,UAAU;AAAA,IACV,SAAS,OAAO,SAAS;AAAA,IACzB,SAAS,YAAY;AACnB,YAAM,WAAW,MAAM;AAAA,QACrB,6BAA6B,mBAAmB,SAAS,CAAC,aAAa,mBAAmB,MAAM,CAAC;AAAA,MACnG;AACA,UAAI,CAAC,SAAS,GAAI,OAAM,IAAI,MAAM,uCAAuC;AACzE,aAAO,SAAS,QAAQ,SAAS,CAAC;AAAA,IACpC;AAAA,EACF,CAAC;AACD,QAAM,QAAQ,iBAAiB,QAAQ,CAAC;AAExC,QAAM,qBAAqB,MAAM,YAAY,YAAY;AACvD,UAAM,YAAY,kBAAkB,EAAE,UAAU,oBAAoB,CAAC;AAAA,EACvE,GAAG,CAAC,qBAAqB,WAAW,CAAC;AAErC,QAAM,SAAS,MAAM,YAAY,OAAO,SAA0B;AAChE,QAAI,CAAC,QAAQ,CAAC,KAAK,UAAU,CAAC,UAAW;AACzC,iBAAa,IAAI;AACjB,QAAI;AACF,iBAAW,QAAQ,MAAM,KAAK,IAAI,GAAG;AACnC,cAAM,OAAO,IAAI,SAAS;AAC1B,aAAK,IAAI,YAAY,SAAS;AAC9B,aAAK,IAAI,YAAY,MAAM;AAC3B,aAAK,IAAI,QAAQ,IAAI;AACrB,YAAI,MAAO,MAAK,IAAI,QAAQ,KAAK,UAAU,CAAC,oBAAoB,CAAC,CAAC;AAClE,cAAM,SAAS,MAAM,YAAY;AAAA,UAC/B,WAAW,MAAM,QAAQ,oBAAoB,EAAE,QAAQ,QAAQ,MAAM,KAAK,CAAC;AAAA;AAAA;AAAA,UAG3E,SAAS;AAAA,YACP,QAAQ,GAAG,mBAAmB,IAAI,MAAM;AAAA,YACxC;AAAA,UACF;AAAA,UACA,iBAAiB,EAAE,QAAQ,UAAU,KAAK,MAAM,QAAQ,MAAM;AAAA,QAChE,CAAC;AACD,YAAI,CAAC,OAAO,GAAI,OAAM,EAAE,0CAA0C,4BAA4B,GAAG,OAAO;AAAA,MAC1G;AACA,YAAM,mBAAmB;AAAA,IAC3B,QAAQ;AACN,YAAM,EAAE,0CAA0C,4BAA4B,GAAG,OAAO;AAAA,IAC1F,UAAE;AACA,mBAAa,KAAK;AAClB,UAAI,SAAS,QAAS,UAAS,QAAQ,QAAQ;AAAA,IACjD;AAAA,EACF,GAAG,CAAC,WAAW,oBAAoB,mBAAmB,aAAa,OAAO,GAAG,MAAM,CAAC;AAEpF,SACE,qBAAC,aAAQ,WAAU,aAAY,eAAY,+BACzC;AAAA,yBAAC,SAAI,WAAU,2CACb;AAAA,0BAAC,QAAG,WAAU,yCAAyC,YAAE,mCAAmC,cAAc,GAAE;AAAA,MAC3G,YACC,qBAAC,SAAI,WAAU,2BACb;AAAA,6BAAC,WAAM,WAAU,yDACf;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,SAAS;AAAA,cACT,iBAAiB,CAAC,UAAU,SAAS,UAAU,IAAI;AAAA,cACnD,cAAY,EAAE,+CAA+C,qBAAqB;AAAA;AAAA,UACpF;AAAA,UACC,EAAE,+CAA+C,qBAAqB;AAAA,WACzE;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,KAAK;AAAA,YACL,MAAK;AAAA,YACL,UAAQ;AAAA,YACR,WAAU;AAAA,YACV,eAAY;AAAA,YACZ,UAAU,CAAC,UAAU,KAAK,OAAO,MAAM,OAAO,KAAK;AAAA;AAAA,QACrD;AAAA,QACA,qBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,UAAU,WAAW,SAAS,MAAM,SAAS,SAAS,MAAM,GAC3G;AAAA,sBAAY,oBAAC,WAAQ,WAAU,gBAAe,IAAK,oBAAC,aAAU,WAAU,gBAAe;AAAA,UACvF,EAAE,oCAAoC,QAAQ;AAAA,WACjD;AAAA,SACF,IACE;AAAA,OACN;AAAA,IACC,iBAAiB,YAChB,oBAAC,kBAAe,OAAO,EAAE,qCAAqC,qBAAgB,GAAG,IAC/E,iBAAiB,UACnB;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,EAAE,wCAAwC,kCAAkC;AAAA,QACnF,QACE,oBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,SAAS,MAAM;AAAE,eAAK,iBAAiB,QAAQ;AAAA,QAAE,GAChG,YAAE,mCAAmC,WAAW,GACnD;AAAA;AAAA,IAEJ,IACE,MAAM,WAAW,IACnB,oBAAC,OAAE,WAAU,iCAAiC,YAAE,mCAAmC,0BAA0B,GAAE,IAE/G,oBAAC,QAAG,WAAU,aACX,gBAAM,IAAI,CAAC,SAAS;AACnB,YAAM,OAAO,KAAK,QAAQ,CAAC;AAC3B,YAAM,UAAU,KAAK,SAAS,oBAAoB;AAClD,YAAM,eAAe,KAAK,KAAK,CAAC,QAAQ,IAAI,WAAW,sBAAsB,CAAC;AAC9E,aACE,qBAAC,QAAiB,WAAU,mDAC1B;AAAA,6BAAC,OAAE,WAAU,mEAAkE,MAAM,KAAK,KACxF;AAAA,8BAAC,YAAS,WAAU,oBAAmB;AAAA,UACvC,oBAAC,UAAK,WAAU,YAAY,eAAK,UAAS;AAAA,UAC1C,oBAAC,UAAK,WAAU,kCAAkC,qBAAW,KAAK,QAAQ,GAAE;AAAA,WAC9E;AAAA,QACA,qBAAC,SAAI,WAAU,uBACZ;AAAA,yBACC,oBAAC,SAAM,SAAQ,WAAW,YAAE,0CAA0C,eAAe,GAAE,IACrF;AAAA,UACJ,oBAAC,SAAM,SAAS,UAAU,cAAc,WACrC,oBACG,EAAE,qCAAqC,qBAAqB,IAC5D,EAAE,sCAAsC,UAAU,GACxD;AAAA,WACF;AAAA,WAfO,KAAK,EAgBd;AAAA,IAEJ,CAAC,GACH;AAAA,KAEJ;AAEJ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Badge } from "@open-mercato/ui/primitives/badge";
|
|
4
|
+
import { StatusBadge } from "@open-mercato/ui/primitives/status-badge";
|
|
5
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
6
|
+
import { SUPPORT_PORTAL_STATUS_VARIANT } from "./supportTicketData.js";
|
|
7
|
+
function supportPortalStatusLabel(t, status) {
|
|
8
|
+
if (status === "resolved") return t("staff.support.tickets.status.resolved", "Resolved");
|
|
9
|
+
if (status === "in_progress") return t("staff.support.tickets.status.inProgress", "In progress");
|
|
10
|
+
return t("staff.support.tickets.status.received", "Received");
|
|
11
|
+
}
|
|
12
|
+
function SupportRequestPanel({ ticket }) {
|
|
13
|
+
const t = useT();
|
|
14
|
+
const rows = [
|
|
15
|
+
{ label: t("staff.time_tracking.taskDrawer.support.reference", "Ticket"), value: ticket.supportReference },
|
|
16
|
+
{
|
|
17
|
+
label: t("staff.time_tracking.taskDrawer.support.customer", "Customer"),
|
|
18
|
+
value: ticket.customerName ?? t("staff.time_tracking.taskDrawer.support.unknown", "Unknown")
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
label: t("staff.time_tracking.taskDrawer.support.reporter", "Reported by"),
|
|
22
|
+
value: ticket.reporterName ?? t("staff.time_tracking.taskDrawer.support.unknown", "Unknown")
|
|
23
|
+
},
|
|
24
|
+
{ label: t("staff.time_tracking.taskDrawer.support.category", "Category"), value: ticket.categoryLabel },
|
|
25
|
+
{
|
|
26
|
+
label: t("staff.time_tracking.taskDrawer.support.routedVia", "Routed by"),
|
|
27
|
+
value: ticket.routedVia === "category" ? t("staff.time_tracking.taskDrawer.support.routedViaCategory", "Category rule") : t("staff.time_tracking.taskDrawer.support.routedViaFallback", "Fallback project")
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
label: t("staff.time_tracking.taskDrawer.support.portalStatus", "Customer sees"),
|
|
31
|
+
value: /* @__PURE__ */ jsx(StatusBadge, { variant: SUPPORT_PORTAL_STATUS_VARIANT[ticket.portalStatus], dot: true, children: supportPortalStatusLabel(t, ticket.portalStatus) })
|
|
32
|
+
}
|
|
33
|
+
];
|
|
34
|
+
return /* @__PURE__ */ jsxs(
|
|
35
|
+
"section",
|
|
36
|
+
{
|
|
37
|
+
className: "flex flex-col gap-3 rounded-lg border border-border bg-muted px-4 py-3",
|
|
38
|
+
"data-testid": "task-drawer-support-panel",
|
|
39
|
+
children: [
|
|
40
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
41
|
+
/* @__PURE__ */ jsx("h3", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: t("staff.time_tracking.taskDrawer.support.title", "Customer request") }),
|
|
42
|
+
ticket.unnotifiedAt ? /* @__PURE__ */ jsx(Badge, { variant: "warning", size: "sm", children: t("staff.support.tickets.nobodyNotified", "Nobody notified") }) : null
|
|
43
|
+
] }),
|
|
44
|
+
/* @__PURE__ */ jsx("dl", { className: "grid grid-cols-1 gap-x-4 gap-y-2 text-sm sm:grid-cols-2", children: rows.map((row) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
|
|
45
|
+
/* @__PURE__ */ jsx("dt", { className: "text-xs text-muted-foreground", children: row.label }),
|
|
46
|
+
/* @__PURE__ */ jsx("dd", { className: "text-foreground", children: row.value })
|
|
47
|
+
] }, row.label)) }),
|
|
48
|
+
ticket.requestSubject || ticket.requestBody ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1 border-t border-border pt-3", children: [
|
|
49
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("staff.time_tracking.taskDrawer.support.originalRequest", "Original request") }),
|
|
50
|
+
ticket.requestSubject ? /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground", children: ticket.requestSubject }) : null,
|
|
51
|
+
ticket.requestBody ? /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap break-words text-sm text-foreground", children: ticket.requestBody }) : null
|
|
52
|
+
] }) : null,
|
|
53
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", "data-testid": "task-drawer-support-hint", children: t(
|
|
54
|
+
"staff.time_tracking.taskDrawer.support.titleHint",
|
|
55
|
+
"The customer sees their original request, not the task title and description."
|
|
56
|
+
) })
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
var SupportRequestPanel_default = SupportRequestPanel;
|
|
62
|
+
export {
|
|
63
|
+
SupportRequestPanel,
|
|
64
|
+
SupportRequestPanel_default as default,
|
|
65
|
+
supportPortalStatusLabel
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=SupportRequestPanel.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/staff/lib/time-tracking-ui/SupportRequestPanel.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { SUPPORT_PORTAL_STATUS_VARIANT, type SupportTicketView } from './supportTicketData'\n\nexport function supportPortalStatusLabel(\n t: ReturnType<typeof useT>,\n status: SupportTicketView['portalStatus'],\n): string {\n if (status === 'resolved') return t('staff.support.tickets.status.resolved', 'Resolved')\n if (status === 'in_progress') return t('staff.support.tickets.status.inProgress', 'In progress')\n return t('staff.support.tickets.status.received', 'Received')\n}\n\ntype SupportRequestPanelProps = {\n ticket: SupportTicketView\n}\n\n/**\n * The customer request behind a portal ticket, as the customer wrote it. The task\n * title and description above are staff-editable working copies; the portal only\n * ever shows this original.\n */\nexport function SupportRequestPanel({ ticket }: SupportRequestPanelProps) {\n const t = useT()\n const rows: Array<{ label: string; value: React.ReactNode }> = [\n { label: t('staff.time_tracking.taskDrawer.support.reference', 'Ticket'), value: ticket.supportReference },\n {\n label: t('staff.time_tracking.taskDrawer.support.customer', 'Customer'),\n value: ticket.customerName ?? t('staff.time_tracking.taskDrawer.support.unknown', 'Unknown'),\n },\n {\n label: t('staff.time_tracking.taskDrawer.support.reporter', 'Reported by'),\n value: ticket.reporterName ?? t('staff.time_tracking.taskDrawer.support.unknown', 'Unknown'),\n },\n { label: t('staff.time_tracking.taskDrawer.support.category', 'Category'), value: ticket.categoryLabel },\n {\n label: t('staff.time_tracking.taskDrawer.support.routedVia', 'Routed by'),\n value:\n ticket.routedVia === 'category'\n ? t('staff.time_tracking.taskDrawer.support.routedViaCategory', 'Category rule')\n : t('staff.time_tracking.taskDrawer.support.routedViaFallback', 'Fallback project'),\n },\n {\n label: t('staff.time_tracking.taskDrawer.support.portalStatus', 'Customer sees'),\n value: (\n <StatusBadge variant={SUPPORT_PORTAL_STATUS_VARIANT[ticket.portalStatus]} dot>\n {supportPortalStatusLabel(t, ticket.portalStatus)}\n </StatusBadge>\n ),\n },\n ]\n return (\n <section\n className=\"flex flex-col gap-3 rounded-lg border border-border bg-muted px-4 py-3\"\n data-testid=\"task-drawer-support-panel\"\n >\n <div className=\"flex flex-wrap items-center gap-2\">\n <h3 className=\"text-xs font-semibold uppercase tracking-wide text-muted-foreground\">\n {t('staff.time_tracking.taskDrawer.support.title', 'Customer request')}\n </h3>\n {ticket.unnotifiedAt ? (\n <Badge variant=\"warning\" size=\"sm\">\n {t('staff.support.tickets.nobodyNotified', 'Nobody notified')}\n </Badge>\n ) : null}\n </div>\n <dl className=\"grid grid-cols-1 gap-x-4 gap-y-2 text-sm sm:grid-cols-2\">\n {rows.map((row) => (\n <div key={row.label} className=\"flex flex-col gap-0.5\">\n <dt className=\"text-xs text-muted-foreground\">{row.label}</dt>\n <dd className=\"text-foreground\">{row.value}</dd>\n </div>\n ))}\n </dl>\n {ticket.requestSubject || ticket.requestBody ? (\n <div className=\"flex flex-col gap-1 border-t border-border pt-3\">\n <span className=\"text-xs text-muted-foreground\">\n {t('staff.time_tracking.taskDrawer.support.originalRequest', 'Original request')}\n </span>\n {ticket.requestSubject ? (\n <p className=\"text-sm font-medium text-foreground\">{ticket.requestSubject}</p>\n ) : null}\n {ticket.requestBody ? (\n <p className=\"whitespace-pre-wrap break-words text-sm text-foreground\">{ticket.requestBody}</p>\n ) : null}\n </div>\n ) : null}\n <p className=\"text-xs text-muted-foreground\" data-testid=\"task-drawer-support-hint\">\n {t(\n 'staff.time_tracking.taskDrawer.support.titleHint',\n 'The customer sees their original request, not the task title and description.',\n )}\n </p>\n </section>\n )\n}\n\nexport default SupportRequestPanel\n"],
|
|
5
|
+
"mappings": ";AAiDQ,cAWF,YAXE;AA9CR,SAAS,aAAa;AACtB,SAAS,mBAAmB;AAC5B,SAAS,YAAY;AACrB,SAAS,qCAA6D;AAE/D,SAAS,yBACd,GACA,QACQ;AACR,MAAI,WAAW,WAAY,QAAO,EAAE,yCAAyC,UAAU;AACvF,MAAI,WAAW,cAAe,QAAO,EAAE,2CAA2C,aAAa;AAC/F,SAAO,EAAE,yCAAyC,UAAU;AAC9D;AAWO,SAAS,oBAAoB,EAAE,OAAO,GAA6B;AACxE,QAAM,IAAI,KAAK;AACf,QAAM,OAAyD;AAAA,IAC7D,EAAE,OAAO,EAAE,oDAAoD,QAAQ,GAAG,OAAO,OAAO,iBAAiB;AAAA,IACzG;AAAA,MACE,OAAO,EAAE,mDAAmD,UAAU;AAAA,MACtE,OAAO,OAAO,gBAAgB,EAAE,kDAAkD,SAAS;AAAA,IAC7F;AAAA,IACA;AAAA,MACE,OAAO,EAAE,mDAAmD,aAAa;AAAA,MACzE,OAAO,OAAO,gBAAgB,EAAE,kDAAkD,SAAS;AAAA,IAC7F;AAAA,IACA,EAAE,OAAO,EAAE,mDAAmD,UAAU,GAAG,OAAO,OAAO,cAAc;AAAA,IACvG;AAAA,MACE,OAAO,EAAE,oDAAoD,WAAW;AAAA,MACxE,OACE,OAAO,cAAc,aACjB,EAAE,4DAA4D,eAAe,IAC7E,EAAE,4DAA4D,kBAAkB;AAAA,IACxF;AAAA,IACA;AAAA,MACE,OAAO,EAAE,uDAAuD,eAAe;AAAA,MAC/E,OACE,oBAAC,eAAY,SAAS,8BAA8B,OAAO,YAAY,GAAG,KAAG,MAC1E,mCAAyB,GAAG,OAAO,YAAY,GAClD;AAAA,IAEJ;AAAA,EACF;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,eAAY;AAAA,MAEZ;AAAA,6BAAC,SAAI,WAAU,qCACb;AAAA,8BAAC,QAAG,WAAU,uEACX,YAAE,gDAAgD,kBAAkB,GACvE;AAAA,UACC,OAAO,eACN,oBAAC,SAAM,SAAQ,WAAU,MAAK,MAC3B,YAAE,wCAAwC,iBAAiB,GAC9D,IACE;AAAA,WACN;AAAA,QACA,oBAAC,QAAG,WAAU,2DACX,eAAK,IAAI,CAAC,QACT,qBAAC,SAAoB,WAAU,yBAC7B;AAAA,8BAAC,QAAG,WAAU,iCAAiC,cAAI,OAAM;AAAA,UACzD,oBAAC,QAAG,WAAU,mBAAmB,cAAI,OAAM;AAAA,aAFnC,IAAI,KAGd,CACD,GACH;AAAA,QACC,OAAO,kBAAkB,OAAO,cAC/B,qBAAC,SAAI,WAAU,mDACb;AAAA,8BAAC,UAAK,WAAU,iCACb,YAAE,0DAA0D,kBAAkB,GACjF;AAAA,UACC,OAAO,iBACN,oBAAC,OAAE,WAAU,uCAAuC,iBAAO,gBAAe,IACxE;AAAA,UACH,OAAO,cACN,oBAAC,OAAE,WAAU,2DAA2D,iBAAO,aAAY,IACzF;AAAA,WACN,IACE;AAAA,QACJ,oBAAC,OAAE,WAAU,iCAAgC,eAAY,4BACtD;AAAA,UACC;AAAA,UACA;AAAA,QACF,GACF;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,8BAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -134,6 +134,7 @@ function TaskBoardScreen({
|
|
|
134
134
|
onChange: setFilters,
|
|
135
135
|
self: selfQuery.data ?? null,
|
|
136
136
|
statuses,
|
|
137
|
+
timeProjectId,
|
|
137
138
|
testId: "board-filters-button",
|
|
138
139
|
trigger: /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", size: "sm", "data-testid": "board-filters-button", children: [
|
|
139
140
|
/* @__PURE__ */ jsx(SlidersHorizontal, { className: "size-3.5", "aria-hidden": "true" }),
|
|
@@ -165,7 +166,8 @@ function TaskBoardScreen({
|
|
|
165
166
|
filters,
|
|
166
167
|
onChange: setFilters,
|
|
167
168
|
self: selfQuery.data ?? null,
|
|
168
|
-
statuses
|
|
169
|
+
statuses,
|
|
170
|
+
timeProjectId
|
|
169
171
|
}
|
|
170
172
|
),
|
|
171
173
|
view === "board" ? /* @__PURE__ */ jsx(
|