@open-mercato/core 0.10.1-develop.7716.1.cefe7975df → 0.10.1-develop.7721.1.b5c4a00a68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/staff_support_routing_rule/index.js +21 -0
- package/dist/generated/entities/staff_support_routing_rule/index.js.map +7 -0
- package/dist/generated/entities/staff_support_ticket/index.js +37 -0
- package/dist/generated/entities/staff_support_ticket/index.js.map +7 -0
- package/dist/generated/entities/staff_time_project/index.js +2 -0
- package/dist/generated/entities/staff_time_project/index.js.map +2 -2
- package/dist/generated/entities/staff_time_project_member/index.js +2 -0
- package/dist/generated/entities/staff_time_project_member/index.js.map +2 -2
- package/dist/generated/entities/staff_time_task/index.js +4 -0
- package/dist/generated/entities/staff_time_task/index.js.map +2 -2
- package/dist/generated/entities/staff_time_task_comment/index.js +9 -1
- package/dist/generated/entities/staff_time_task_comment/index.js.map +2 -2
- package/dist/generated/entities.ids.generated.js +2 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +38 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/auth.js +1 -0
- package/dist/helpers/integration/auth.js.map +2 -2
- package/dist/helpers/integration/communicationChannelsFixtures.js +20 -0
- package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
- package/dist/modules/catalog/api/bulk-delete/route.js +12 -2
- package/dist/modules/catalog/api/bulk-delete/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/test-seed/route.js +17 -3
- package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
- package/dist/modules/communication_channels/lib/test-seed.js +38 -5
- package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
- package/dist/modules/messages/commands/messages.js +2 -3
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
- package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js +299 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js +91 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/route.js +146 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/options/route.js +60 -0
- package/dist/modules/staff/api/portal/support-tickets/options/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/route.js +220 -0
- package/dist/modules/staff/api/portal/support-tickets/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/settings/route.js +7 -1
- package/dist/modules/staff/api/timesheets/settings/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js +122 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js +180 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/route.js +147 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/support-tickets/options/route.js +133 -0
- package/dist/modules/staff/api/timesheets/support-tickets/options/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js +33 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js +25 -7
- package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js +32 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/route.js +94 -47
- package/dist/modules/staff/api/timesheets/tasks/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +3 -0
- package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/route.js +3 -0
- package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +1 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +54 -4
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js +21 -5
- package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/settings/page.js +319 -254
- package/dist/modules/staff/backend/staff/time-tracking/settings/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/support/page.js +363 -0
- package/dist/modules/staff/backend/staff/time-tracking/support/page.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js +18 -0
- package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js.map +7 -0
- package/dist/modules/staff/cli.js +27 -0
- package/dist/modules/staff/cli.js.map +2 -2
- package/dist/modules/staff/commands/index.js +3 -0
- package/dist/modules/staff/commands/index.js.map +2 -2
- package/dist/modules/staff/commands/shared.js +10 -1
- package/dist/modules/staff/commands/shared.js.map +2 -2
- package/dist/modules/staff/commands/support-routing-rules.js +505 -0
- package/dist/modules/staff/commands/support-routing-rules.js.map +7 -0
- package/dist/modules/staff/commands/support-tickets.js +513 -0
- package/dist/modules/staff/commands/support-tickets.js.map +7 -0
- package/dist/modules/staff/commands/timesheets-projects.js +72 -14
- package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-task-assignment.js +270 -0
- package/dist/modules/staff/commands/timesheets-task-assignment.js.map +7 -0
- package/dist/modules/staff/commands/timesheets-task-comments.js +58 -12
- package/dist/modules/staff/commands/timesheets-task-comments.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-tasks.js +91 -9
- package/dist/modules/staff/commands/timesheets-tasks.js.map +3 -3
- package/dist/modules/staff/data/enrichers.js +141 -0
- package/dist/modules/staff/data/enrichers.js.map +2 -2
- package/dist/modules/staff/data/entities.js +143 -2
- package/dist/modules/staff/data/entities.js.map +2 -2
- package/dist/modules/staff/data/extensions.js +8 -0
- package/dist/modules/staff/data/extensions.js.map +2 -2
- package/dist/modules/staff/data/validators.js +34 -5
- package/dist/modules/staff/data/validators.js.map +2 -2
- package/dist/modules/staff/encryption.js +6 -0
- package/dist/modules/staff/encryption.js.map +2 -2
- package/dist/modules/staff/events.js +25 -0
- package/dist/modules/staff/events.js.map +2 -2
- package/dist/modules/staff/events.payloads.js +54 -0
- package/dist/modules/staff/events.payloads.js.map +2 -2
- package/dist/modules/staff/extension-points.js +1 -0
- package/dist/modules/staff/extension-points.js.map +2 -2
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js +468 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js +12 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js +316 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js +12 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js +190 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js +19 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js.map +7 -0
- package/dist/modules/staff/index.js +1 -1
- package/dist/modules/staff/index.js.map +2 -2
- package/dist/modules/staff/lib/crud.js +2 -0
- package/dist/modules/staff/lib/crud.js.map +2 -2
- package/dist/modules/staff/lib/support/attachments.js +36 -0
- package/dist/modules/staff/lib/support/attachments.js.map +7 -0
- package/dist/modules/staff/lib/support/dictionaries.js +87 -0
- package/dist/modules/staff/lib/support/dictionaries.js.map +7 -0
- package/dist/modules/staff/lib/support/notifications.js +111 -0
- package/dist/modules/staff/lib/support/notifications.js.map +7 -0
- package/dist/modules/staff/lib/support/portalAttachments.js +100 -0
- package/dist/modules/staff/lib/support/portalAttachments.js.map +7 -0
- package/dist/modules/staff/lib/support/portalBroadcast.js +117 -0
- package/dist/modules/staff/lib/support/portalBroadcast.js.map +7 -0
- package/dist/modules/staff/lib/support/portalRoute.js +99 -0
- package/dist/modules/staff/lib/support/portalRoute.js.map +7 -0
- package/dist/modules/staff/lib/support/portalStatus.js +12 -0
- package/dist/modules/staff/lib/support/portalStatus.js.map +7 -0
- package/dist/modules/staff/lib/support/portalTickets.js +148 -0
- package/dist/modules/staff/lib/support/portalTickets.js.map +7 -0
- package/dist/modules/staff/lib/support/routing.js +81 -0
- package/dist/modules/staff/lib/support/routing.js.map +7 -0
- package/dist/modules/staff/lib/support/routingRuleDraft.js +25 -0
- package/dist/modules/staff/lib/support/routingRuleDraft.js.map +7 -0
- package/dist/modules/staff/lib/support/seedDefaults.js +114 -0
- package/dist/modules/staff/lib/support/seedDefaults.js.map +7 -0
- package/dist/modules/staff/lib/support/supportActivity.js +26 -0
- package/dist/modules/staff/lib/support/supportActivity.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js +23 -0
- package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js +112 -0
- package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/portalPendingUploads.js +45 -0
- package/dist/modules/staff/lib/support-ui/portalPendingUploads.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/portalSupport.js +51 -0
- package/dist/modules/staff/lib/support-ui/portalSupport.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/access.js +3 -39
- package/dist/modules/staff/lib/time-tracking/access.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/listProjectAccess.js +54 -0
- package/dist/modules/staff/lib/time-tracking/listProjectAccess.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/portalRecipients.js +92 -2
- package/dist/modules/staff/lib/time-tracking/portalRecipients.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/projectAssignment.js +112 -0
- package/dist/modules/staff/lib/time-tracking/projectAssignment.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/settingKeys.js +14 -0
- package/dist/modules/staff/lib/time-tracking/settingKeys.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/settings.js +28 -1
- package/dist/modules/staff/lib/time-tracking/settings.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js +143 -0
- package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/workRecipients.js +92 -0
- package/dist/modules/staff/lib/time-tracking/workRecipients.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js +32 -3
- package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js +29 -0
- package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js +4 -0
- package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js +141 -1
- package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js +153 -0
- package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js +67 -0
- package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js +3 -1
- package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js +80 -17
- package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js +230 -41
- package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js +21 -1
- package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js +16 -4
- package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js +3 -1
- package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js +68 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js +50 -0
- package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js +13 -1
- package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js +36 -2
- package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js.map +2 -2
- package/dist/modules/staff/migrations/Migration20261009180543_staff.js +42 -0
- package/dist/modules/staff/migrations/Migration20261009180543_staff.js.map +7 -0
- package/dist/modules/staff/notifications.js +92 -0
- package/dist/modules/staff/notifications.js.map +2 -2
- package/dist/modules/staff/setup.js +6 -2
- package/dist/modules/staff/setup.js.map +2 -2
- package/dist/modules/staff/subscribers/support-portal-comment-created.js +26 -0
- package/dist/modules/staff/subscribers/support-portal-comment-created.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-comment-deleted.js +26 -0
- package/dist/modules/staff/subscribers/support-portal-comment-deleted.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-comment-updated.js +26 -0
- package/dist/modules/staff/subscribers/support-portal-comment-updated.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-status-column-updated.js +24 -0
- package/dist/modules/staff/subscribers/support-portal-status-column-updated.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-task-status-changed.js +25 -0
- package/dist/modules/staff/subscribers/support-portal-task-status-changed.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-task-updated.js +25 -0
- package/dist/modules/staff/subscribers/support-portal-task-updated.js.map +7 -0
- package/dist/modules/staff/subscribers/support-ticket-created-notification.js +25 -0
- package/dist/modules/staff/subscribers/support-ticket-created-notification.js.map +7 -0
- package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js +25 -0
- package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js.map +7 -0
- package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js +26 -0
- package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js.map +7 -0
- package/dist/modules/staff/subscribers/time-task-assignee-notification.js +83 -0
- package/dist/modules/staff/subscribers/time-task-assignee-notification.js.map +7 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js +112 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js.map +7 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js +17 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js.map +7 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js +870 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js.map +7 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js +17 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js.map +7 -0
- package/dist/modules/staff/widgets/injection-table.js +16 -1
- package/dist/modules/staff/widgets/injection-table.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +4 -18
- package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLotDetailPage.js +4 -18
- package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +4 -18
- package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +2 -2
- package/dist/modules/wms/lib/inventoryCsvExport.js +19 -0
- package/dist/modules/wms/lib/inventoryCsvExport.js.map +7 -0
- package/generated/entities/staff_support_routing_rule/index.ts +9 -0
- package/generated/entities/staff_support_ticket/index.ts +17 -0
- package/generated/entities/staff_time_project/index.ts +1 -0
- package/generated/entities/staff_time_project_member/index.ts +1 -0
- package/generated/entities/staff_time_task/index.ts +2 -0
- package/generated/entities/staff_time_task_comment/index.ts +4 -0
- package/generated/entities.ids.generated.ts +2 -0
- package/generated/entity-fields-registry.ts +38 -0
- package/package.json +8 -8
- package/src/helpers/integration/auth.ts +1 -1
- package/src/helpers/integration/communicationChannelsFixtures.ts +60 -2
- package/src/modules/catalog/api/bulk-delete/route.ts +12 -2
- package/src/modules/communication_channels/api/post/test-seed/route.ts +29 -12
- package/src/modules/communication_channels/lib/test-seed.ts +49 -10
- package/src/modules/messages/commands/messages.ts +8 -10
- package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +2 -2
- package/src/modules/staff/AGENTS.md +22 -6
- package/src/modules/staff/api/portal/support-tickets/[id]/attachments/route.ts +327 -0
- package/src/modules/staff/api/portal/support-tickets/[id]/messages/route.ts +94 -0
- package/src/modules/staff/api/portal/support-tickets/[id]/route.ts +160 -0
- package/src/modules/staff/api/portal/support-tickets/options/route.ts +60 -0
- package/src/modules/staff/api/portal/support-tickets/route.ts +253 -0
- package/src/modules/staff/api/timesheets/settings/route.ts +7 -1
- package/src/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.ts +131 -0
- package/src/modules/staff/api/timesheets/support-routing-rules/preview/route.ts +248 -0
- package/src/modules/staff/api/timesheets/support-routing-rules/route.ts +150 -0
- package/src/modules/staff/api/timesheets/support-tickets/options/route.ts +181 -0
- package/src/modules/staff/api/timesheets/tasks/[id]/claim/route.ts +31 -0
- package/src/modules/staff/api/timesheets/tasks/[id]/comments/route.ts +25 -7
- package/src/modules/staff/api/timesheets/tasks/[id]/release/route.ts +30 -0
- package/src/modules/staff/api/timesheets/tasks/route.ts +119 -67
- package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +3 -0
- package/src/modules/staff/api/timesheets/time-projects/route.ts +3 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +1 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +67 -2
- package/src/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.ts +22 -3
- package/src/modules/staff/backend/staff/time-tracking/settings/page.tsx +405 -308
- package/src/modules/staff/backend/staff/time-tracking/support/page.meta.ts +14 -0
- package/src/modules/staff/backend/staff/time-tracking/support/page.tsx +428 -0
- package/src/modules/staff/cli.ts +28 -0
- package/src/modules/staff/commands/index.ts +3 -0
- package/src/modules/staff/commands/shared.ts +22 -1
- package/src/modules/staff/commands/support-routing-rules.ts +595 -0
- package/src/modules/staff/commands/support-tickets.ts +580 -0
- package/src/modules/staff/commands/timesheets-projects.ts +84 -13
- package/src/modules/staff/commands/timesheets-task-assignment.ts +319 -0
- package/src/modules/staff/commands/timesheets-task-comments.ts +70 -15
- package/src/modules/staff/commands/timesheets-tasks.ts +106 -15
- package/src/modules/staff/data/enrichers.ts +203 -0
- package/src/modules/staff/data/entities.ts +133 -2
- package/src/modules/staff/data/extensions.ts +9 -1
- package/src/modules/staff/data/validators.ts +36 -0
- package/src/modules/staff/encryption.ts +6 -0
- package/src/modules/staff/events.payloads.ts +87 -1
- package/src/modules/staff/events.ts +26 -0
- package/src/modules/staff/extension-points.ts +1 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.ts +10 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.tsx +553 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.ts +10 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.tsx +346 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/page.meta.ts +17 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/page.tsx +253 -0
- package/src/modules/staff/i18n/de.json +291 -0
- package/src/modules/staff/i18n/en.json +291 -0
- package/src/modules/staff/i18n/es.json +291 -0
- package/src/modules/staff/i18n/ko.json +291 -0
- package/src/modules/staff/i18n/pl.json +291 -0
- package/src/modules/staff/index.ts +1 -1
- package/src/modules/staff/lib/crud.ts +2 -1
- package/src/modules/staff/lib/support/attachments.ts +43 -0
- package/src/modules/staff/lib/support/dictionaries.ts +120 -0
- package/src/modules/staff/lib/support/notifications.ts +138 -0
- package/src/modules/staff/lib/support/portalAttachments.ts +140 -0
- package/src/modules/staff/lib/support/portalBroadcast.ts +161 -0
- package/src/modules/staff/lib/support/portalRoute.ts +108 -0
- package/src/modules/staff/lib/support/portalStatus.ts +15 -0
- package/src/modules/staff/lib/support/portalTickets.ts +233 -0
- package/src/modules/staff/lib/support/routing.ts +117 -0
- package/src/modules/staff/lib/support/routingRuleDraft.ts +45 -0
- package/src/modules/staff/lib/support/seedDefaults.ts +151 -0
- package/src/modules/staff/lib/support/supportActivity.ts +39 -0
- package/src/modules/staff/lib/support-ui/PortalSupportNotLinked.tsx +20 -0
- package/src/modules/staff/lib/support-ui/portalAttachmentValidation.ts +125 -0
- package/src/modules/staff/lib/support-ui/portalPendingUploads.ts +49 -0
- package/src/modules/staff/lib/support-ui/portalSupport.ts +116 -0
- package/src/modules/staff/lib/time-tracking/access.ts +2 -62
- package/src/modules/staff/lib/time-tracking/listProjectAccess.ts +78 -0
- package/src/modules/staff/lib/time-tracking/portalRecipients.ts +173 -5
- package/src/modules/staff/lib/time-tracking/projectAssignment.ts +172 -0
- package/src/modules/staff/lib/time-tracking/settingKeys.ts +11 -0
- package/src/modules/staff/lib/time-tracking/settings.ts +46 -1
- package/src/modules/staff/lib/time-tracking/taskAssignmentRoute.ts +165 -0
- package/src/modules/staff/lib/time-tracking/workRecipients.ts +183 -0
- package/src/modules/staff/lib/time-tracking-ui/BoardFilterChips.tsx +36 -1
- package/src/modules/staff/lib/time-tracking-ui/KanbanBoard.tsx +30 -0
- package/src/modules/staff/lib/time-tracking-ui/KanbanCard.tsx +9 -0
- package/src/modules/staff/lib/time-tracking-ui/ProjectFormSections.tsx +185 -6
- package/src/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.tsx +188 -0
- package/src/modules/staff/lib/time-tracking-ui/SupportRequestPanel.tsx +102 -0
- package/src/modules/staff/lib/time-tracking-ui/TaskBoardScreen.tsx +2 -0
- package/src/modules/staff/lib/time-tracking-ui/TaskCommentThread.tsx +105 -22
- package/src/modules/staff/lib/time-tracking-ui/TaskDrawer.tsx +278 -43
- package/src/modules/staff/lib/time-tracking-ui/boardDirectory.ts +21 -0
- package/src/modules/staff/lib/time-tracking-ui/boardFilters.ts +16 -2
- package/src/modules/staff/lib/time-tracking-ui/kanbanBoardData.ts +4 -0
- package/src/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.ts +117 -0
- package/src/modules/staff/lib/time-tracking-ui/settingsTabs.ts +17 -0
- package/src/modules/staff/lib/time-tracking-ui/supportTicketData.ts +71 -0
- package/src/modules/staff/lib/time-tracking-ui/taskDrawerData.ts +15 -0
- package/src/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.ts +48 -1
- package/src/modules/staff/migrations/.snapshot-open-mercato.json +714 -0
- package/src/modules/staff/migrations/Migration20261009180543_staff.ts +50 -0
- package/src/modules/staff/notifications.ts +92 -0
- package/src/modules/staff/setup.ts +6 -2
- package/src/modules/staff/subscribers/support-portal-comment-created.ts +35 -0
- package/src/modules/staff/subscribers/support-portal-comment-deleted.ts +35 -0
- package/src/modules/staff/subscribers/support-portal-comment-updated.ts +35 -0
- package/src/modules/staff/subscribers/support-portal-status-column-updated.ts +36 -0
- package/src/modules/staff/subscribers/support-portal-task-status-changed.ts +34 -0
- package/src/modules/staff/subscribers/support-portal-task-updated.ts +34 -0
- package/src/modules/staff/subscribers/support-ticket-created-notification.ts +27 -0
- package/src/modules/staff/subscribers/support-ticket-customer-replied-notification.ts +27 -0
- package/src/modules/staff/subscribers/support-ticket-unnotified-clear.ts +29 -0
- package/src/modules/staff/subscribers/time-task-assignee-notification.ts +102 -0
- package/src/modules/staff/widgets/injection/portal-support-tickets/widget.client.tsx +151 -0
- package/src/modules/staff/widgets/injection/portal-support-tickets/widget.ts +16 -0
- package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.tsx +1058 -0
- package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.ts +17 -0
- package/src/modules/staff/widgets/injection-table.ts +15 -0
- package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +4 -20
- package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +4 -20
- package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +4 -19
- package/src/modules/wms/lib/inventoryCsvExport.ts +18 -0
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
sortByPosition,
|
|
61
61
|
toBoardTask
|
|
62
62
|
} from "./kanbanBoardData.js";
|
|
63
|
+
import { SUPPORT_ACTIVITY_EVENT } from "./supportTicketData.js";
|
|
63
64
|
const logger = createLogger("staff").child({ component: "KanbanBoard" });
|
|
64
65
|
const BOARD_MAX_CHILD_PAGES = 5;
|
|
65
66
|
const MOVE_MUTATION_CONTEXT_ID = "staff.time_tracking.board.move";
|
|
@@ -67,6 +68,7 @@ const CREATE_MUTATION_CONTEXT_ID = "staff.time_tracking.board.create";
|
|
|
67
68
|
const MANAGE_COLUMNS_FEATURE = "staff.timesheets.projects.manage";
|
|
68
69
|
const MANAGE_TASKS_FEATURE = "staff.timesheets.tasks.manage";
|
|
69
70
|
const TASK_STATUS_CHANGED_EVENT = "staff.timesheets.time_task.status_changed";
|
|
71
|
+
const TASK_ASSIGNEE_CHANGED_EVENT = "staff.timesheets.time_task.assignee_changed";
|
|
70
72
|
const OWN_MOVE_ECHO_TTL_MS = 15e3;
|
|
71
73
|
function statusIdFromColumnKey(key) {
|
|
72
74
|
const part = key.find((entry) => typeof entry === "string" && entry.startsWith("status:"));
|
|
@@ -242,6 +244,10 @@ function KanbanBoard({
|
|
|
242
244
|
queryClient.invalidateQueries({ queryKey: [...BOARD_QUERY_ROOT, "children"] }).catch(() => {
|
|
243
245
|
});
|
|
244
246
|
}, [queryClient]);
|
|
247
|
+
const invalidateUnassignedCount = React.useCallback(() => {
|
|
248
|
+
queryClient.invalidateQueries({ queryKey: [...BOARD_QUERY_ROOT, "unassigned-count"] }).catch(() => {
|
|
249
|
+
});
|
|
250
|
+
}, [queryClient]);
|
|
245
251
|
const invalidateStatuses = React.useCallback(() => {
|
|
246
252
|
queryClient.invalidateQueries({ queryKey: [...BOARD_QUERY_ROOT, "statuses"] }).catch(() => {
|
|
247
253
|
});
|
|
@@ -274,6 +280,28 @@ function KanbanBoard({
|
|
|
274
280
|
},
|
|
275
281
|
[consumeOwnMove, invalidateBoard, timeProjectId]
|
|
276
282
|
);
|
|
283
|
+
useAppEvent(
|
|
284
|
+
TASK_ASSIGNEE_CHANGED_EVENT,
|
|
285
|
+
(event) => {
|
|
286
|
+
const eventPayload = event.payload;
|
|
287
|
+
const projectId = typeof eventPayload?.timeProjectId === "string" ? eventPayload.timeProjectId : null;
|
|
288
|
+
if (projectId && projectId !== timeProjectId) return;
|
|
289
|
+
invalidateBoard();
|
|
290
|
+
invalidateUnassignedCount();
|
|
291
|
+
},
|
|
292
|
+
[invalidateBoard, invalidateUnassignedCount, timeProjectId]
|
|
293
|
+
);
|
|
294
|
+
useAppEvent(
|
|
295
|
+
SUPPORT_ACTIVITY_EVENT,
|
|
296
|
+
(event) => {
|
|
297
|
+
const eventPayload = event.payload;
|
|
298
|
+
const projectId = typeof eventPayload?.projectId === "string" ? eventPayload.projectId : null;
|
|
299
|
+
if (projectId && projectId !== timeProjectId) return;
|
|
300
|
+
invalidateBoard();
|
|
301
|
+
invalidateUnassignedCount();
|
|
302
|
+
},
|
|
303
|
+
[invalidateBoard, invalidateUnassignedCount, timeProjectId]
|
|
304
|
+
);
|
|
277
305
|
const setTaskPending = React.useCallback((taskId, pending) => {
|
|
278
306
|
setPendingTaskIds((previous) => {
|
|
279
307
|
const next = new Set(previous);
|
|
@@ -849,6 +877,7 @@ export {
|
|
|
849
877
|
MANAGE_COLUMNS_FEATURE,
|
|
850
878
|
MANAGE_TASKS_FEATURE,
|
|
851
879
|
MOVE_MUTATION_CONTEXT_ID,
|
|
880
|
+
TASK_ASSIGNEE_CHANGED_EVENT,
|
|
852
881
|
TASK_STATUS_CHANGED_EVENT,
|
|
853
882
|
KanbanBoard_default as default
|
|
854
883
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/staff/lib/time-tracking-ui/KanbanBoard.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { useQueries, useQuery, useQueryClient } from '@tanstack/react-query'\nimport {\n DndContext,\n DragOverlay,\n MeasuringStrategy,\n PointerSensor,\n pointerWithin,\n useSensor,\n useSensors,\n type Announcements,\n type CollisionDetection,\n type DragEndEvent,\n type DragStartEvent,\n type ScreenReaderInstructions,\n type UniqueIdentifier,\n} from '@dnd-kit/core'\nimport { Plus, RotateCcw } from 'lucide-react'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { apiCall, apiCallOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { sumTaskLoggedMinutes } from '../timesheets-tasks/taskHoursTotals'\nimport { KanbanColumn, KANBAN_COLUMN_DROPPABLE_PREFIX } from './KanbanColumn'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'\nimport { TaskDrawer } from './TaskDrawer'\nimport type { KanbanMoveTarget, KanbanTagOption } from './KanbanCard'\nimport {\n BOARD_DIRECTORY_PAGE_SIZE,\n BOARD_PAGE_SIZE,\n BOARD_QUERY_ROOT,\n boardColumnQueryKey,\n fetchTaskPages,\n useAssigneeNames,\n useBoardSelf,\n useBoardStatuses,\n useTagLabels,\n} from './boardDirectory'\nimport {\n EMPTY_BOARD_FILTERS,\n boardServerFilterKey,\n boardServerFilterParams,\n visibleBoardStatusIds,\n type BoardFilters,\n} from './boardFilters'\nimport type { BoardSummary } from './boardSummary'\nimport {\n computeSubtaskProgress,\n formatBoardMinutes,\n nextPositionAtColumnTop,\n readItems,\n readString,\n sortByPosition,\n toBoardTask,\n type BoardApiRow,\n type BoardTask,\n} from './kanbanBoardData'\n\nconst logger = createLogger('staff').child({ component: 'KanbanBoard' })\n\nexport { BOARD_PAGE_SIZE, BOARD_DIRECTORY_PAGE_SIZE }\nexport const BOARD_MAX_CHILD_PAGES = 5\nexport const MOVE_MUTATION_CONTEXT_ID = 'staff.time_tracking.board.move'\nexport const CREATE_MUTATION_CONTEXT_ID = 'staff.time_tracking.board.create'\nexport const MANAGE_COLUMNS_FEATURE = 'staff.timesheets.projects.manage'\nexport const MANAGE_TASKS_FEATURE = 'staff.timesheets.tasks.manage'\nexport const TASK_STATUS_CHANGED_EVENT = 'staff.timesheets.time_task.status_changed'\n\n/** Window a locally-originated move stays \"mine\" for, so a lost SSE cannot wedge the filter. */\nconst OWN_MOVE_ECHO_TTL_MS = 15_000\n\ntype ColumnCache = { items: BoardApiRow[]; total: number }\n\ntype PendingMove = {\n taskId: string\n targetStatusId: string\n}\n\nexport type KanbanBoardProps = {\n timeProjectId: string\n /** Used by the drawer's header; the page owns the breadcrumb and the rest of the chrome. */\n projectName?: string | null\n /** Board filters (T3.8). Assignee narrows the request, status picks columns, tag filters rows. */\n filters?: BoardFilters\n /** Reports what is on screen so the page head can describe it without a second fetch. */\n onSummaryChange?: (summary: BoardSummary) => void\n}\n\nfunction statusIdFromColumnKey(key: readonly unknown[]): string | null {\n const part = key.find((entry) => typeof entry === 'string' && entry.startsWith('status:'))\n return typeof part === 'string' ? part.slice('status:'.length) : null\n}\n\n/**\n * `true` when `candidate` names a strictly earlier version than `reference`.\n * Unparseable input answers `false`, so an unexpected format leaves whatever the\n * list handed us in charge rather than pinning the board to a remembered token.\n */\nfunction isOlderVersion(candidate: string, reference: string): boolean {\n const candidateAt = Date.parse(candidate)\n const referenceAt = Date.parse(reference)\n if (!Number.isFinite(candidateAt) || !Number.isFinite(referenceAt)) return false\n return candidateAt < referenceAt\n}\n\n/**\n * The Kanban board of screen 6 (T3.6, US-C1/US-C2).\n *\n * Four things carry the design intent and are worth reading before changing anything:\n *\n * 1. **The move is optimistic and the rollback is exact.** Before the PATCH the board\n * snapshots every column cache; the card is moved between them immediately. On any\n * failure the whole snapshot is written back, which returns the card to the column\n * it came from even when the drag crossed columns whose pages had shifted.\n * 2. **A 409 is not a retryable failure.** The PATCH carries the card's `updatedAt` in\n * the optimistic-lock header, so a 409 means somebody else moved this card while the\n * drag was in flight. `surfaceRecordConflict` raises the shared conflict bar, the\n * board refetches, and no Retry is offered \u2014 retrying would overwrite the other\n * move. Every other failure gets the retry affordance of note 2.\n * 3. **Echoes of your own move are ignored.** `status_changed` is broadcast to every\n * connected board, including the one that caused it. Each locally-issued move\n * registers a `taskId|statusId` key that the event handler consumes instead of\n * refetching, so a colleague's drag moves the card here and yours does not fight the\n * optimistic update that already landed.\n * 4. **The next move's version comes out of the last move's response.** A move bumps\n * the row's `updated_at`, so the version the board is holding for that card is spent\n * the moment the PATCH commits \u2014 and the optimistic write of note 1 carries the\n * pre-move version forward until a refetch replaces it. Reading the version back off\n * the response (as `TaskDrawer` already does) is what lets the same card be moved\n * twice in a row; waiting for the refetch made every second move answer 409 \"changed\n * by someone else\" with nobody else editing.\n */\nexport function KanbanBoard({\n timeProjectId,\n projectName,\n filters = EMPTY_BOARD_FILTERS,\n onSummaryChange,\n}: KanbanBoardProps): React.ReactElement {\n const t = useT()\n const router = useRouter()\n const searchParams = useSearchParams()\n const queryClient = useQueryClient()\n const scopeVersion = useOrganizationScopeVersion()\n const { payload } = useBackendChrome()\n const canManageColumns = hasFeature(payload?.grantedFeatures, MANAGE_COLUMNS_FEATURE)\n const canManageTasks = hasFeature(payload?.grantedFeatures, MANAGE_TASKS_FEATURE)\n const serverFilterKey = boardServerFilterKey(filters)\n const serverFilterParams = boardServerFilterParams(filters)\n\n const [pagesByStatus, setPagesByStatus] = React.useState<Record<string, number>>({})\n const [activeDragTaskId, setActiveDragTaskId] = React.useState<string | null>(null)\n const [pendingTaskIds, setPendingTaskIds] = React.useState<ReadonlySet<string>>(new Set())\n const [failedMove, setFailedMove] = React.useState<PendingMove | null>(null)\n const [quickAddPendingStatusId, setQuickAddPendingStatusId] = React.useState<string | null>(null)\n const [composingStatus, setComposingStatus] = React.useState(false)\n const [statusDraft, setStatusDraft] = React.useState('')\n\n const ownMoveKeysRef = React.useRef(new Map<string, number>())\n\n // The version the server confirmed for a card in its own move response, kept until\n // a fetched row catches up with it (note 4). \"Newest wins\" never suppresses a real\n // conflict: somebody else's move produces a version NEWER than the one confirmed\n // here, and the row that carries it takes over \u2014 this only stops the board from\n // replaying a version its own last move already spent.\n const confirmedVersionsRef = React.useRef(new Map<string, string>())\n\n const resolveTaskVersion = React.useCallback(\n (taskId: string, rowVersion: string | null): string | null => {\n const confirmed = confirmedVersionsRef.current.get(taskId)\n if (!confirmed) return rowVersion\n if (rowVersion && !isOlderVersion(rowVersion, confirmed)) {\n confirmedVersionsRef.current.delete(taskId)\n return rowVersion\n }\n return confirmed\n },\n [],\n )\n\n const { runMutation: runMoveMutation, retryLastMutation: retryMoveMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: MOVE_MUTATION_CONTEXT_ID,\n blockedMessage: t('staff.time_tracking.board.move.error', 'Could not move the task.'),\n })\n\n const { runMutation: runCreateMutation, retryLastMutation: retryCreateMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: CREATE_MUTATION_CONTEXT_ID,\n blockedMessage: t('staff.time_tracking.board.quickAdd.error', 'Could not create the task.'),\n })\n\n const statusesQuery = useBoardStatuses(timeProjectId)\n\n const statuses = React.useMemo(() => statusesQuery.data ?? [], [statusesQuery.data])\n\n // A status filter is not a request parameter here \u2014 it selects which columns exist.\n const visibleStatuses = React.useMemo(\n () => visibleBoardStatusIds(statuses, filters),\n [filters, statuses],\n )\n\n const columnQueries = useQueries({\n queries: visibleStatuses.map((status) => {\n const pages = pagesByStatus[status.id] ?? 1\n return {\n queryKey: boardColumnQueryKey(scopeVersion, timeProjectId, status.id, pages, serverFilterKey),\n staleTime: 15_000,\n queryFn: async (): Promise<ColumnCache> =>\n fetchTaskPages(\n (page) =>\n `timeProjectId=${encodeURIComponent(timeProjectId)}&taskStatusId=${encodeURIComponent(status.id)}&topLevelOnly=true&page=${page}&pageSize=${BOARD_PAGE_SIZE}&sortField=position&sortDir=asc${serverFilterParams}`,\n pages,\n ),\n }\n }),\n })\n\n const tasksByStatusId = React.useMemo(() => {\n const map = new Map<string, { tasks: BoardTask[]; loaded: number; total: number }>()\n visibleStatuses.forEach((status, index) => {\n const data = columnQueries[index]?.data\n const loaded = (data?.items ?? [])\n .map(toBoardTask)\n .filter((task): task is BoardTask => task !== null)\n map.set(status.id, {\n tasks: sortByPosition(loaded),\n loaded: loaded.length,\n total: data?.total ?? loaded.length,\n })\n })\n return map\n }, [columnQueries, visibleStatuses])\n\n const allTasks = React.useMemo(() => {\n const rows: BoardTask[] = []\n for (const entry of tasksByStatusId.values()) rows.push(...entry.tasks)\n return rows\n }, [tasksByStatusId])\n\n const hasChildren = React.useMemo(() => allTasks.some((task) => task.childCount > 0), [allTasks])\n\n const childrenQuery = useQuery<BoardTask[]>({\n queryKey: [...BOARD_QUERY_ROOT, 'children', `scope:${scopeVersion}`, `project:${timeProjectId}`],\n enabled: hasChildren,\n staleTime: 30_000,\n queryFn: async () => {\n // The list route filters children only by a single `parentTaskId`, so a board-wide\n // sweep is the one request that answers \"how many of each card's subtasks are done\"\n // without one call per card. Bounded, because the count is decoration, not data.\n const { items } = await fetchTaskPages(\n (page) =>\n `timeProjectId=${encodeURIComponent(timeProjectId)}&page=${page}&pageSize=${BOARD_PAGE_SIZE}&sortField=position&sortDir=asc`,\n BOARD_MAX_CHILD_PAGES,\n )\n return items\n .map(toBoardTask)\n .filter((task): task is BoardTask => task !== null && !!task.parentTaskId)\n },\n })\n\n const doneStatusIds = React.useMemo(\n () => new Set(statuses.filter((status) => status.isDone).map((status) => status.id)),\n [statuses],\n )\n\n const subtasksByTaskId = React.useMemo(\n () => computeSubtaskProgress(childrenQuery.data ?? [], doneStatusIds),\n [childrenQuery.data, doneStatusIds],\n )\n\n const assigneeIds = React.useMemo(() => {\n const ids = new Set<string>()\n for (const task of allTasks) {\n if (task.assigneeStaffMemberId) ids.add(task.assigneeStaffMemberId)\n }\n return Array.from(ids).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))\n }, [allTasks])\n\n const assigneeQuery = useAssigneeNames(assigneeIds)\n\n const tagIds = React.useMemo(() => {\n const ids = new Set<string>()\n for (const task of allTasks) {\n for (const id of task.tagIds) ids.add(id)\n }\n return Array.from(ids).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))\n }, [allTasks])\n\n const tagQuery = useTagLabels(tagIds)\n\n // A chip carries a tag's name or it is not drawn at all. The label lookup is a\n // separate request from the card's own row, so falling back to the id painted a\n // raw uuid on the card for as long as that request was in flight.\n const tagsByTaskId = React.useMemo(() => {\n const labels = tagQuery.data ?? new Map<string, string>()\n const map = new Map<string, KanbanTagOption[]>()\n for (const task of allTasks) {\n if (task.tagIds.length === 0) continue\n const named: KanbanTagOption[] = []\n for (const id of task.tagIds) {\n const label = labels.get(id)\n if (label) named.push({ id, label })\n }\n if (named.length === 0) continue\n map.set(task.id, named)\n }\n return map\n }, [allTasks, tagQuery.data])\n\n const selfQuery = useBoardSelf()\n\n const selfStaffMemberId = selfQuery.data?.id ?? null\n\n const runningTimerQuery = useQuery<{ entryId: string | null; taskId: string | null }>({\n queryKey: [...BOARD_QUERY_ROOT, 'running-timer', `scope:${scopeVersion}`, selfStaffMemberId ?? 'none'],\n enabled: !!selfStaffMemberId,\n staleTime: 15_000,\n refetchInterval: 30_000,\n queryFn: async () => {\n if (!selfStaffMemberId) return { entryId: null, taskId: null }\n const call = await apiCall<Record<string, unknown>>(\n `/api/staff/timesheets/time-entries?staffMemberId=${encodeURIComponent(selfStaffMemberId)}&running=true&pageSize=${BOARD_PAGE_SIZE}`,\n )\n if (!call.ok) return { entryId: null, taskId: null }\n for (const row of readItems(call.result)) {\n const endedAt = readString(row, 'ended_at', 'endedAt')\n const startedAt = readString(row, 'started_at', 'startedAt')\n if (!startedAt || endedAt) continue\n return { entryId: readString(row, 'id'), taskId: readString(row, 'task_id', 'taskId') }\n }\n return { entryId: null, taskId: null }\n },\n })\n\n const runningTimerTaskId = runningTimerQuery.data?.taskId ?? null\n const runningTimerEntryId = runningTimerQuery.data?.entryId ?? null\n\n const invalidateBoard = React.useCallback(() => {\n queryClient.invalidateQueries({ queryKey: [...BOARD_QUERY_ROOT, 'column'] }).catch(() => {})\n queryClient.invalidateQueries({ queryKey: [...BOARD_QUERY_ROOT, 'children'] }).catch(() => {})\n }, [queryClient])\n\n const invalidateStatuses = React.useCallback(() => {\n queryClient.invalidateQueries({ queryKey: [...BOARD_QUERY_ROOT, 'statuses'] }).catch(() => {})\n }, [queryClient])\n\n const markOwnMove = React.useCallback((taskId: string, statusId: string) => {\n ownMoveKeysRef.current.set(`${taskId}|${statusId}`, Date.now())\n }, [])\n\n const consumeOwnMove = React.useCallback((taskId: string, statusId: string): boolean => {\n const keys = ownMoveKeysRef.current\n const key = `${taskId}|${statusId}`\n const stamped = keys.get(key)\n // Drop stale entries so a broadcast that never arrived cannot suppress a later,\n // genuinely remote move of the same card into the same column.\n for (const [entry, at] of keys) {\n if (Date.now() - at > OWN_MOVE_ECHO_TTL_MS) keys.delete(entry)\n }\n if (stamped === undefined) return false\n keys.delete(key)\n return true\n }, [])\n\n useAppEvent(\n TASK_STATUS_CHANGED_EVENT,\n (event) => {\n const eventPayload = event.payload as Record<string, unknown> | null | undefined\n if (!eventPayload) return\n const projectId = typeof eventPayload.timeProjectId === 'string' ? eventPayload.timeProjectId : null\n if (projectId && projectId !== timeProjectId) return\n const taskId =\n typeof eventPayload.taskId === 'string'\n ? eventPayload.taskId\n : typeof eventPayload.id === 'string'\n ? eventPayload.id\n : null\n const statusId = typeof eventPayload.taskStatusId === 'string' ? eventPayload.taskStatusId : null\n if (taskId && statusId && consumeOwnMove(taskId, statusId)) return\n invalidateBoard()\n },\n [consumeOwnMove, invalidateBoard, timeProjectId],\n )\n\n const setTaskPending = React.useCallback((taskId: string, pending: boolean) => {\n setPendingTaskIds((previous) => {\n const next = new Set(previous)\n if (pending) next.add(taskId)\n else next.delete(taskId)\n return next\n })\n }, [])\n\n const moveTask = React.useCallback(\n (taskId: string, targetStatusId: string) => {\n const columnCaches = queryClient.getQueriesData<ColumnCache>({\n queryKey: [...BOARD_QUERY_ROOT, 'column'],\n })\n // The full pre-move picture. Restoring it wholesale is what puts the card back in\n // its origin column on failure, regardless of how many columns the drag touched.\n const snapshot = columnCaches.map(([key, data]) => [key, data] as const)\n\n let sourceStatusId: string | null = null\n let movingRow: BoardApiRow | null = null\n for (const [key, data] of columnCaches) {\n if (!data) continue\n const statusId = statusIdFromColumnKey(key as readonly unknown[])\n if (!statusId) continue\n const index = data.items.findIndex((item) => readString(item, 'id') === taskId)\n if (index < 0) continue\n sourceStatusId = statusId\n movingRow = { ...data.items[index], task_status_id: targetStatusId, taskStatusId: targetStatusId }\n queryClient.setQueryData(key, {\n items: data.items.filter((item) => readString(item, 'id') !== taskId),\n total: Math.max(0, data.total - 1),\n })\n break\n }\n\n if (!movingRow || !sourceStatusId || sourceStatusId === targetStatusId) {\n for (const [key, data] of snapshot) queryClient.setQueryData(key, data)\n return\n }\n\n const targetTasks = tasksByStatusId.get(targetStatusId)?.tasks ?? []\n const position = nextPositionAtColumnTop(targetTasks)\n const landed: BoardApiRow = { ...movingRow, position }\n\n for (const [key, data] of columnCaches) {\n if (!data) continue\n if (statusIdFromColumnKey(key as readonly unknown[]) !== targetStatusId) continue\n queryClient.setQueryData(key, {\n items: [landed, ...data.items],\n total: data.total + 1,\n })\n }\n\n const taskVersion = resolveTaskVersion(taskId, readString(movingRow, 'updated_at', 'updatedAt'))\n setTaskPending(taskId, true)\n setFailedMove(null)\n markOwnMove(taskId, targetStatusId)\n\n runMoveMutation({\n operation: async () => {\n const call = await withScopedApiRequestHeaders(buildOptimisticLockHeader(taskVersion), () =>\n apiCallOrThrow<Record<string, unknown>>(\n `/api/staff/timesheets/tasks/${encodeURIComponent(taskId)}/status`,\n {\n method: 'PATCH',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ taskStatusId: targetStatusId, position }),\n },\n { errorMessage: t('staff.time_tracking.board.move.error', 'Could not move the task.') },\n ),\n )\n return readString(call.result ?? {}, 'updatedAt', 'updated_at')\n },\n context: {\n formId: MOVE_MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.task',\n resourceId: taskId,\n retryLastMutation: retryMoveMutation,\n },\n })\n .then((confirmedVersion) => {\n // Note 4: the card the optimistic write left in the target column still\n // carries the version this move just spent, so the response's version has\n // to take over until a fetched row carries a newer one.\n if (confirmedVersion) confirmedVersionsRef.current.set(taskId, confirmedVersion)\n invalidateBoard()\n })\n .catch((error: unknown) => {\n // Card first: it goes back where it came from before anything else is decided.\n for (const [key, data] of snapshot) queryClient.setQueryData(key, data)\n consumeOwnMove(taskId, targetStatusId)\n if (surfaceRecordConflict(error, t)) {\n // Somebody else moved this card. Refetch and let the conflict bar speak \u2014\n // retrying here would silently overwrite their move. The version this board\n // remembered is behind the server's, so the refetched row takes over.\n confirmedVersionsRef.current.delete(taskId)\n invalidateBoard()\n return\n }\n setFailedMove({ taskId, targetStatusId })\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('staff.time_tracking.board.move.error', 'Could not move the task.')\n flash(message, 'error')\n })\n .finally(() => {\n setTaskPending(taskId, false)\n })\n },\n [\n consumeOwnMove,\n invalidateBoard,\n markOwnMove,\n queryClient,\n resolveTaskVersion,\n retryMoveMutation,\n runMoveMutation,\n setTaskPending,\n t,\n tasksByStatusId,\n ],\n )\n\n // Pointer only. dnd-kit's keyboard sensor would have to own Enter/Space on the card \u2014\n // the key that opens the drawer \u2014 and it has no sortable context to traverse anyway;\n // keyboard moves go through the card's explicit \"Move\" menu, which calls `moveTask`\n // directly (U3).\n const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 4 } }))\n\n const collisionDetection = React.useCallback<CollisionDetection>((args) => pointerWithin(args), [])\n\n const measuringConfig = React.useMemo(\n () => ({ droppable: { strategy: MeasuringStrategy.BeforeDragging } }),\n [],\n )\n\n const statusIdByTaskId = React.useMemo(() => {\n const map = new Map<string, string>()\n for (const [statusId, entry] of tasksByStatusId) {\n for (const task of entry.tasks) map.set(task.id, statusId)\n }\n return map\n }, [tasksByStatusId])\n\n const activeDragTask = React.useMemo(\n () => (activeDragTaskId ? allTasks.find((task) => task.id === activeDragTaskId) ?? null : null),\n [activeDragTaskId, allTasks],\n )\n\n const handleDragStart = React.useCallback((event: DragStartEvent) => {\n setActiveDragTaskId(typeof event.active.id === 'string' ? event.active.id : null)\n }, [])\n\n const handleDragCancel = React.useCallback(() => setActiveDragTaskId(null), [])\n\n const targetStatusIdFor = React.useCallback(\n (overId: UniqueIdentifier | null | undefined): string | null => {\n if (typeof overId !== 'string') return null\n return overId.startsWith(KANBAN_COLUMN_DROPPABLE_PREFIX)\n ? overId.slice(KANBAN_COLUMN_DROPPABLE_PREFIX.length)\n : statusIdByTaskId.get(overId) ?? null\n },\n [statusIdByTaskId],\n )\n\n const handleDragEnd = React.useCallback(\n (event: DragEndEvent) => {\n setActiveDragTaskId(null)\n const taskId = typeof event.active.id === 'string' ? event.active.id : null\n if (!taskId) return\n const targetStatusId = targetStatusIdFor(event.over?.id)\n if (!targetStatusId) return\n if (statusIdByTaskId.get(taskId) === targetStatusId) return\n moveTask(taskId, targetStatusId)\n },\n [moveTask, statusIdByTaskId, targetStatusIdFor],\n )\n\n const moveTargets = React.useMemo<KanbanMoveTarget[]>(\n () => visibleStatuses.map((status) => ({ id: status.id, name: status.name })),\n [visibleStatuses],\n )\n\n const describeTask = React.useCallback(\n (taskId: UniqueIdentifier | null | undefined): string => {\n if (typeof taskId !== 'string') return ''\n return allTasks.find((task) => task.id === taskId)?.title ?? taskId\n },\n [allTasks],\n )\n\n const describeColumn = React.useCallback(\n (overId: UniqueIdentifier | null | undefined): string | null => {\n const statusId = targetStatusIdFor(overId)\n if (!statusId) return null\n return statuses.find((status) => status.id === statusId)?.name ?? statusId\n },\n [statuses, targetStatusIdFor],\n )\n\n // dnd-kit narrates every drag through a live region. Left unset it narrates in English\n // regardless of locale (U9), so the board owns the strings.\n const announcements = React.useMemo<Announcements>(\n () => ({\n onDragStart: ({ active }) =>\n t('staff.time_tracking.board.dnd.dragStart', 'Picked up the task {task}.', {\n task: describeTask(active.id),\n }),\n onDragOver: ({ active, over }) => {\n const column = describeColumn(over?.id)\n return column\n ? t('staff.time_tracking.board.dnd.dragOver', 'The task {task} is over the column {column}.', {\n task: describeTask(active.id),\n column,\n })\n : t('staff.time_tracking.board.dnd.dragOverNothing', 'The task {task} is not over a column.', {\n task: describeTask(active.id),\n })\n },\n onDragEnd: ({ active, over }) => {\n const column = describeColumn(over?.id)\n return column\n ? t('staff.time_tracking.board.dnd.dragEnd', 'The task {task} was dropped into the column {column}.', {\n task: describeTask(active.id),\n column,\n })\n : t('staff.time_tracking.board.dnd.dragEndOutside', 'The task {task} was dropped outside the columns.', {\n task: describeTask(active.id),\n })\n },\n onDragCancel: ({ active }) =>\n t('staff.time_tracking.board.dnd.dragCancel', 'Moving the task {task} was cancelled.', {\n task: describeTask(active.id),\n }),\n }),\n [describeColumn, describeTask, t],\n )\n\n const screenReaderInstructions = React.useMemo<ScreenReaderInstructions>(\n () => ({\n draggable: t(\n 'staff.time_tracking.board.dnd.instructions',\n 'Drag a card with the pointer to move it between columns. With the keyboard, press Enter on a card to open it, or use the Move button in the card actions to choose a target column.',\n ),\n }),\n [t],\n )\n\n const accessibility = React.useMemo(\n () => ({ announcements, screenReaderInstructions }),\n [announcements, screenReaderInstructions],\n )\n\n const handleQuickAdd = React.useCallback(\n async (statusId: string, title: string) => {\n setQuickAddPendingStatusId(statusId)\n try {\n await runCreateMutation({\n operation: () =>\n apiCallOrThrow(\n '/api/staff/timesheets/tasks',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n timeProjectId,\n taskStatusId: statusId,\n title,\n // US-C1: the creator owns what they just typed until somebody reassigns it.\n assigneeStaffMemberId: selfStaffMemberId,\n }),\n },\n { errorMessage: t('staff.time_tracking.board.quickAdd.error', 'Could not create the task.') },\n ),\n context: {\n formId: CREATE_MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.task',\n resourceId: statusId,\n retryLastMutation: retryCreateMutation,\n },\n })\n invalidateBoard()\n } catch (error) {\n if (surfaceRecordConflict(error, t)) return\n logger.error('staff.time_tracking board quick add failed', { err: error })\n flash(\n error instanceof Error && error.message\n ? error.message\n : t('staff.time_tracking.board.quickAdd.error', 'Could not create the task.'),\n 'error',\n )\n } finally {\n setQuickAddPendingStatusId(null)\n }\n },\n [invalidateBoard, retryCreateMutation, runCreateMutation, selfStaffMemberId, t, timeProjectId],\n )\n\n const handleCreateStatus = React.useCallback(async () => {\n const name = statusDraft.trim()\n if (!name) {\n setComposingStatus(false)\n setStatusDraft('')\n return\n }\n try {\n await runCreateMutation({\n operation: () =>\n apiCallOrThrow(\n '/api/staff/timesheets/task-statuses',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ timeProjectId, name }),\n },\n { errorMessage: t('staff.time_tracking.board.addStatus.error', 'Could not add the column.') },\n ),\n context: {\n formId: CREATE_MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.task_status',\n resourceId: timeProjectId,\n retryLastMutation: retryCreateMutation,\n },\n })\n setStatusDraft('')\n setComposingStatus(false)\n invalidateStatuses()\n } catch (error) {\n if (surfaceRecordConflict(error, t)) return\n flash(\n error instanceof Error && error.message\n ? error.message\n : t('staff.time_tracking.board.addStatus.error', 'Could not add the column.'),\n 'error',\n )\n }\n }, [invalidateStatuses, retryCreateMutation, runCreateMutation, statusDraft, t, timeProjectId])\n\n const handleLoadMore = React.useCallback((statusId: string) => {\n setPagesByStatus((previous) => ({ ...previous, [statusId]: (previous[statusId] ?? 1) + 1 }))\n }, [])\n\n const openTask = React.useCallback(\n (taskId: string) => {\n // Seam for the task drawer (T3.7): the drawer reads `?task=` and the board keeps\n // rendering underneath it.\n const params = new URLSearchParams(searchParams?.toString() ?? '')\n params.set('task', taskId)\n router.push(`?${params.toString()}`, { scroll: false })\n },\n [router, searchParams],\n )\n\n const handleAddTime = React.useCallback(\n (taskId: string) => {\n const params = new URLSearchParams(searchParams?.toString() ?? '')\n params.set('task', taskId)\n params.set('panel', 'time')\n router.push(`?${params.toString()}`, { scroll: false })\n },\n [router, searchParams],\n )\n\n const drawerTaskId = searchParams?.get('task') ?? null\n const drawerPanel = searchParams?.get('panel') ?? null\n\n const closeTaskDrawer = React.useCallback(() => {\n const params = new URLSearchParams(searchParams?.toString() ?? '')\n params.delete('task')\n params.delete('panel')\n const query = params.toString()\n router.push(query.length > 0 ? `?${query}` : '?', { scroll: false })\n }, [router, searchParams])\n\n const handleStartTimer = React.useCallback(\n async (taskId: string) => {\n if (!selfStaffMemberId) return\n const task = allTasks.find((entry) => entry.id === taskId) ?? null\n try {\n const started = await runCreateMutation({\n operation: () =>\n apiCallOrThrow<Record<string, unknown>>(\n '/api/staff/timesheets/time-entries/start-timer',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n staffMemberId: selfStaffMemberId,\n timeProjectId,\n date: new Date().toISOString().slice(0, 10),\n notes: task?.title ?? null,\n }),\n },\n { errorMessage: t('staff.time_tracking.board.timer.startError', 'Could not start the timer.') },\n ),\n context: {\n formId: CREATE_MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: taskId,\n retryLastMutation: retryCreateMutation,\n },\n })\n const entryId = typeof started?.result?.id === 'string' ? started.result.id : null\n if (entryId) {\n // `start-timer` creates the entry atomically but takes no task, so the link is a\n // second write. A failure here leaves a running, project-level timer rather than\n // no timer at all \u2014 the lesser of the two wrong states.\n const linked = await apiCall(`/api/staff/timesheets/time-entries`, {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: entryId, taskId }),\n })\n if (!linked.ok) {\n flash(\n t(\n 'staff.time_tracking.board.timer.linkWarning',\n 'The timer is running but is not linked to this task yet.',\n ),\n 'warning',\n )\n }\n }\n await runningTimerQuery.refetch()\n } catch (error) {\n if (surfaceRecordConflict(error, t)) return\n flash(\n error instanceof Error && error.message\n ? error.message\n : t('staff.time_tracking.board.timer.startError', 'Could not start the timer.'),\n 'error',\n )\n }\n },\n [allTasks, retryCreateMutation, runCreateMutation, runningTimerQuery, selfStaffMemberId, t, timeProjectId],\n )\n\n const handleStopTimer = React.useCallback(async () => {\n if (!runningTimerEntryId) return\n try {\n await runCreateMutation({\n operation: () =>\n apiCallOrThrow(\n `/api/staff/timesheets/time-entries/${encodeURIComponent(runningTimerEntryId)}/timer-stop`,\n { method: 'POST' },\n { errorMessage: t('staff.time_tracking.board.timer.stopError', 'Could not stop the timer.') },\n ),\n context: {\n formId: CREATE_MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: runningTimerEntryId,\n retryLastMutation: retryCreateMutation,\n },\n })\n await runningTimerQuery.refetch()\n invalidateBoard()\n } catch (error) {\n if (surfaceRecordConflict(error, t)) return\n flash(\n error instanceof Error && error.message\n ? error.message\n : t('staff.time_tracking.board.timer.stopError', 'Could not stop the timer.'),\n 'error',\n )\n }\n }, [invalidateBoard, retryCreateMutation, runCreateMutation, runningTimerEntryId, runningTimerQuery, t])\n\n // Never fold `loggedMinutes` inline: a parent's figure already contains its children's,\n // so the shared helper \u2014 which drops any row whose parent is in the same set \u2014 is the\n // only thing standing between this number and risk R10.\n const summary = React.useMemo<BoardSummary>(\n () => ({ taskCount: allTasks.length, loggedMinutes: sumTaskLoggedMinutes(allTasks) }),\n [allTasks],\n )\n\n React.useEffect(() => {\n onSummaryChange?.(summary)\n }, [onSummaryChange, summary])\n\n if (statusesQuery.isLoading) {\n return <LoadingMessage label={t('staff.time_tracking.board.loading', 'Loading the board\u2026')} />\n }\n\n if (statusesQuery.isError) {\n return (\n <ErrorMessage\n label={t('staff.time_tracking.board.loadError', 'Could not load the board.')}\n action={\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void statusesQuery.refetch()}>\n {t('staff.time_tracking.board.reload', 'Try again')}\n </Button>\n }\n />\n )\n }\n\n if (statuses.length === 0) {\n return (\n <EmptyState\n title={t('staff.time_tracking.board.empty.title', 'This project has no columns yet')}\n description={t(\n 'staff.time_tracking.board.empty.description',\n 'A Team Leader configures the board columns for each project.',\n )}\n />\n )\n }\n\n return (\n <div className=\"flex flex-col gap-4\">\n <InjectionSpot\n spotId={extensionPoints.hosts.taskBoardToolbar.spotId}\n context={{ timeProjectId, projectName: projectName ?? null, statusIds: visibleStatuses.map((status) => status.id) }}\n data={summary}\n />\n\n {failedMove ? (\n <Alert status=\"error\" data-testid=\"kanban-move-retry\">\n <AlertDescription className=\"flex flex-wrap items-center gap-3\">\n <span>\n {t('staff.time_tracking.board.move.rolledBack', 'The task was moved back \u2014 the change was not saved.')}\n </span>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n const retry = failedMove\n setFailedMove(null)\n moveTask(retry.taskId, retry.targetStatusId)\n }}\n >\n <RotateCcw className=\"size-3.5\" aria-hidden=\"true\" />\n {t('staff.time_tracking.board.move.retry', 'Retry')}\n </Button>\n </AlertDescription>\n </Alert>\n ) : null}\n\n <DndContext\n sensors={sensors}\n accessibility={accessibility}\n collisionDetection={collisionDetection}\n measuring={measuringConfig}\n onDragStart={handleDragStart}\n onDragEnd={handleDragEnd}\n onDragCancel={handleDragCancel}\n >\n <div\n className={`flex min-w-0 gap-3.5 overflow-x-auto pb-6 ${\n activeDragTaskId ? 'cursor-grabbing select-none' : ''\n }`}\n >\n {visibleStatuses.map((status, index) => {\n const entry = tasksByStatusId.get(status.id)\n const serverTotal = entry?.total ?? 0\n const loaded = entry?.loaded ?? 0\n return (\n <KanbanColumn\n key={status.id}\n status={status}\n tasks={entry?.tasks ?? []}\n total={serverTotal}\n remaining={Math.max(0, serverTotal - loaded)}\n canQuickAdd={canManageTasks}\n loadingMore={columnQueries[index]?.isFetching ?? false}\n activeDragTaskId={activeDragTaskId}\n pendingTaskIds={pendingTaskIds}\n runningTimerTaskId={runningTimerTaskId}\n assigneeNames={assigneeQuery.data ?? new Map<string, string>()}\n tagsByTaskId={tagsByTaskId}\n subtasksByTaskId={subtasksByTaskId}\n quickAddPending={quickAddPendingStatusId === status.id}\n moveTargets={moveTargets}\n onQuickAdd={handleQuickAdd}\n onLoadMore={handleLoadMore}\n onOpenTask={openTask}\n onStartTimer={handleStartTimer}\n onStopTimer={handleStopTimer}\n onAddTime={handleAddTime}\n onMoveTask={moveTask}\n />\n )\n })}\n\n {canManageColumns ? (\n <div className=\"flex w-72 flex-none flex-col gap-3\">\n {composingStatus ? (\n <Input\n autoFocus\n value={statusDraft}\n onChange={(event) => setStatusDraft(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n void handleCreateStatus()\n }\n if (event.key === 'Escape') {\n event.preventDefault()\n setComposingStatus(false)\n setStatusDraft('')\n }\n }}\n placeholder={t('staff.time_tracking.board.addStatus.placeholder', 'Column name')}\n aria-label={t('staff.time_tracking.board.addStatus.cta', 'Add status')}\n data-testid=\"kanban-add-status-input\"\n />\n ) : (\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={() => setComposingStatus(true)}\n data-testid=\"kanban-add-status\"\n className=\"flex h-24 flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-muted-foreground/60 bg-muted/40 text-sm font-semibold leading-normal text-muted-foreground hover:text-foreground\"\n >\n <Plus className=\"size-5\" aria-hidden=\"true\" />\n {t('staff.time_tracking.board.addStatus.cta', 'Add status')}\n </Button>\n )}\n </div>\n ) : null}\n </div>\n\n <DragOverlay dropAnimation={null}>\n {activeDragTask ? (\n <div\n data-testid=\"kanban-drag-overlay\"\n className=\"pointer-events-none w-72 rotate-2 cursor-grabbing select-none rounded-lg border border-border bg-card px-4 py-3 opacity-80 shadow-xl\"\n >\n <div className=\"text-sm font-semibold leading-normal text-foreground\">\n {activeDragTask.title}\n </div>\n <div className=\"mt-2 text-xs font-semibold tabular-nums leading-normal text-muted-foreground\">\n {formatBoardMinutes(activeDragTask.loggedMinutes)}\n </div>\n </div>\n ) : null}\n </DragOverlay>\n </DndContext>\n\n {drawerTaskId ? (\n <TaskDrawer\n taskId={drawerTaskId}\n open\n onOpenChange={(next) => {\n if (!next) closeTaskDrawer()\n }}\n projectName={projectName ?? null}\n focusPanel={drawerPanel}\n onChanged={invalidateBoard}\n />\n ) : null}\n </div>\n )\n}\n\nexport default KanbanBoard\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { useQueries, useQuery, useQueryClient } from '@tanstack/react-query'\nimport {\n DndContext,\n DragOverlay,\n MeasuringStrategy,\n PointerSensor,\n pointerWithin,\n useSensor,\n useSensors,\n type Announcements,\n type CollisionDetection,\n type DragEndEvent,\n type DragStartEvent,\n type ScreenReaderInstructions,\n type UniqueIdentifier,\n} from '@dnd-kit/core'\nimport { Plus, RotateCcw } from 'lucide-react'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { apiCall, apiCallOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { sumTaskLoggedMinutes } from '../timesheets-tasks/taskHoursTotals'\nimport { KanbanColumn, KANBAN_COLUMN_DROPPABLE_PREFIX } from './KanbanColumn'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'\nimport { TaskDrawer } from './TaskDrawer'\nimport type { KanbanMoveTarget, KanbanTagOption } from './KanbanCard'\nimport {\n BOARD_DIRECTORY_PAGE_SIZE,\n BOARD_PAGE_SIZE,\n BOARD_QUERY_ROOT,\n boardColumnQueryKey,\n fetchTaskPages,\n useAssigneeNames,\n useBoardSelf,\n useBoardStatuses,\n useTagLabels,\n} from './boardDirectory'\nimport {\n EMPTY_BOARD_FILTERS,\n boardServerFilterKey,\n boardServerFilterParams,\n visibleBoardStatusIds,\n type BoardFilters,\n} from './boardFilters'\nimport type { BoardSummary } from './boardSummary'\nimport {\n computeSubtaskProgress,\n formatBoardMinutes,\n nextPositionAtColumnTop,\n readItems,\n readString,\n sortByPosition,\n toBoardTask,\n type BoardApiRow,\n type BoardTask,\n} from './kanbanBoardData'\nimport { SUPPORT_ACTIVITY_EVENT } from './supportTicketData'\n\nconst logger = createLogger('staff').child({ component: 'KanbanBoard' })\n\nexport { BOARD_PAGE_SIZE, BOARD_DIRECTORY_PAGE_SIZE }\nexport const BOARD_MAX_CHILD_PAGES = 5\nexport const MOVE_MUTATION_CONTEXT_ID = 'staff.time_tracking.board.move'\nexport const CREATE_MUTATION_CONTEXT_ID = 'staff.time_tracking.board.create'\nexport const MANAGE_COLUMNS_FEATURE = 'staff.timesheets.projects.manage'\nexport const MANAGE_TASKS_FEATURE = 'staff.timesheets.tasks.manage'\nexport const TASK_STATUS_CHANGED_EVENT = 'staff.timesheets.time_task.status_changed'\nexport const TASK_ASSIGNEE_CHANGED_EVENT = 'staff.timesheets.time_task.assignee_changed'\n\n/** Window a locally-originated move stays \"mine\" for, so a lost SSE cannot wedge the filter. */\nconst OWN_MOVE_ECHO_TTL_MS = 15_000\n\ntype ColumnCache = { items: BoardApiRow[]; total: number }\n\ntype PendingMove = {\n taskId: string\n targetStatusId: string\n}\n\nexport type KanbanBoardProps = {\n timeProjectId: string\n /** Used by the drawer's header; the page owns the breadcrumb and the rest of the chrome. */\n projectName?: string | null\n /** Board filters (T3.8). Assignee narrows the request, status picks columns, tag filters rows. */\n filters?: BoardFilters\n /** Reports what is on screen so the page head can describe it without a second fetch. */\n onSummaryChange?: (summary: BoardSummary) => void\n}\n\nfunction statusIdFromColumnKey(key: readonly unknown[]): string | null {\n const part = key.find((entry) => typeof entry === 'string' && entry.startsWith('status:'))\n return typeof part === 'string' ? part.slice('status:'.length) : null\n}\n\n/**\n * `true` when `candidate` names a strictly earlier version than `reference`.\n * Unparseable input answers `false`, so an unexpected format leaves whatever the\n * list handed us in charge rather than pinning the board to a remembered token.\n */\nfunction isOlderVersion(candidate: string, reference: string): boolean {\n const candidateAt = Date.parse(candidate)\n const referenceAt = Date.parse(reference)\n if (!Number.isFinite(candidateAt) || !Number.isFinite(referenceAt)) return false\n return candidateAt < referenceAt\n}\n\n/**\n * The Kanban board of screen 6 (T3.6, US-C1/US-C2).\n *\n * Four things carry the design intent and are worth reading before changing anything:\n *\n * 1. **The move is optimistic and the rollback is exact.** Before the PATCH the board\n * snapshots every column cache; the card is moved between them immediately. On any\n * failure the whole snapshot is written back, which returns the card to the column\n * it came from even when the drag crossed columns whose pages had shifted.\n * 2. **A 409 is not a retryable failure.** The PATCH carries the card's `updatedAt` in\n * the optimistic-lock header, so a 409 means somebody else moved this card while the\n * drag was in flight. `surfaceRecordConflict` raises the shared conflict bar, the\n * board refetches, and no Retry is offered \u2014 retrying would overwrite the other\n * move. Every other failure gets the retry affordance of note 2.\n * 3. **Echoes of your own move are ignored.** `status_changed` is broadcast to every\n * connected board, including the one that caused it. Each locally-issued move\n * registers a `taskId|statusId` key that the event handler consumes instead of\n * refetching, so a colleague's drag moves the card here and yours does not fight the\n * optimistic update that already landed.\n * 4. **The next move's version comes out of the last move's response.** A move bumps\n * the row's `updated_at`, so the version the board is holding for that card is spent\n * the moment the PATCH commits \u2014 and the optimistic write of note 1 carries the\n * pre-move version forward until a refetch replaces it. Reading the version back off\n * the response (as `TaskDrawer` already does) is what lets the same card be moved\n * twice in a row; waiting for the refetch made every second move answer 409 \"changed\n * by someone else\" with nobody else editing.\n */\nexport function KanbanBoard({\n timeProjectId,\n projectName,\n filters = EMPTY_BOARD_FILTERS,\n onSummaryChange,\n}: KanbanBoardProps): React.ReactElement {\n const t = useT()\n const router = useRouter()\n const searchParams = useSearchParams()\n const queryClient = useQueryClient()\n const scopeVersion = useOrganizationScopeVersion()\n const { payload } = useBackendChrome()\n const canManageColumns = hasFeature(payload?.grantedFeatures, MANAGE_COLUMNS_FEATURE)\n const canManageTasks = hasFeature(payload?.grantedFeatures, MANAGE_TASKS_FEATURE)\n const serverFilterKey = boardServerFilterKey(filters)\n const serverFilterParams = boardServerFilterParams(filters)\n\n const [pagesByStatus, setPagesByStatus] = React.useState<Record<string, number>>({})\n const [activeDragTaskId, setActiveDragTaskId] = React.useState<string | null>(null)\n const [pendingTaskIds, setPendingTaskIds] = React.useState<ReadonlySet<string>>(new Set())\n const [failedMove, setFailedMove] = React.useState<PendingMove | null>(null)\n const [quickAddPendingStatusId, setQuickAddPendingStatusId] = React.useState<string | null>(null)\n const [composingStatus, setComposingStatus] = React.useState(false)\n const [statusDraft, setStatusDraft] = React.useState('')\n\n const ownMoveKeysRef = React.useRef(new Map<string, number>())\n\n // The version the server confirmed for a card in its own move response, kept until\n // a fetched row catches up with it (note 4). \"Newest wins\" never suppresses a real\n // conflict: somebody else's move produces a version NEWER than the one confirmed\n // here, and the row that carries it takes over \u2014 this only stops the board from\n // replaying a version its own last move already spent.\n const confirmedVersionsRef = React.useRef(new Map<string, string>())\n\n const resolveTaskVersion = React.useCallback(\n (taskId: string, rowVersion: string | null): string | null => {\n const confirmed = confirmedVersionsRef.current.get(taskId)\n if (!confirmed) return rowVersion\n if (rowVersion && !isOlderVersion(rowVersion, confirmed)) {\n confirmedVersionsRef.current.delete(taskId)\n return rowVersion\n }\n return confirmed\n },\n [],\n )\n\n const { runMutation: runMoveMutation, retryLastMutation: retryMoveMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: MOVE_MUTATION_CONTEXT_ID,\n blockedMessage: t('staff.time_tracking.board.move.error', 'Could not move the task.'),\n })\n\n const { runMutation: runCreateMutation, retryLastMutation: retryCreateMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: CREATE_MUTATION_CONTEXT_ID,\n blockedMessage: t('staff.time_tracking.board.quickAdd.error', 'Could not create the task.'),\n })\n\n const statusesQuery = useBoardStatuses(timeProjectId)\n\n const statuses = React.useMemo(() => statusesQuery.data ?? [], [statusesQuery.data])\n\n // A status filter is not a request parameter here \u2014 it selects which columns exist.\n const visibleStatuses = React.useMemo(\n () => visibleBoardStatusIds(statuses, filters),\n [filters, statuses],\n )\n\n const columnQueries = useQueries({\n queries: visibleStatuses.map((status) => {\n const pages = pagesByStatus[status.id] ?? 1\n return {\n queryKey: boardColumnQueryKey(scopeVersion, timeProjectId, status.id, pages, serverFilterKey),\n staleTime: 15_000,\n queryFn: async (): Promise<ColumnCache> =>\n fetchTaskPages(\n (page) =>\n `timeProjectId=${encodeURIComponent(timeProjectId)}&taskStatusId=${encodeURIComponent(status.id)}&topLevelOnly=true&page=${page}&pageSize=${BOARD_PAGE_SIZE}&sortField=position&sortDir=asc${serverFilterParams}`,\n pages,\n ),\n }\n }),\n })\n\n const tasksByStatusId = React.useMemo(() => {\n const map = new Map<string, { tasks: BoardTask[]; loaded: number; total: number }>()\n visibleStatuses.forEach((status, index) => {\n const data = columnQueries[index]?.data\n const loaded = (data?.items ?? [])\n .map(toBoardTask)\n .filter((task): task is BoardTask => task !== null)\n map.set(status.id, {\n tasks: sortByPosition(loaded),\n loaded: loaded.length,\n total: data?.total ?? loaded.length,\n })\n })\n return map\n }, [columnQueries, visibleStatuses])\n\n const allTasks = React.useMemo(() => {\n const rows: BoardTask[] = []\n for (const entry of tasksByStatusId.values()) rows.push(...entry.tasks)\n return rows\n }, [tasksByStatusId])\n\n const hasChildren = React.useMemo(() => allTasks.some((task) => task.childCount > 0), [allTasks])\n\n const childrenQuery = useQuery<BoardTask[]>({\n queryKey: [...BOARD_QUERY_ROOT, 'children', `scope:${scopeVersion}`, `project:${timeProjectId}`],\n enabled: hasChildren,\n staleTime: 30_000,\n queryFn: async () => {\n // The list route filters children only by a single `parentTaskId`, so a board-wide\n // sweep is the one request that answers \"how many of each card's subtasks are done\"\n // without one call per card. Bounded, because the count is decoration, not data.\n const { items } = await fetchTaskPages(\n (page) =>\n `timeProjectId=${encodeURIComponent(timeProjectId)}&page=${page}&pageSize=${BOARD_PAGE_SIZE}&sortField=position&sortDir=asc`,\n BOARD_MAX_CHILD_PAGES,\n )\n return items\n .map(toBoardTask)\n .filter((task): task is BoardTask => task !== null && !!task.parentTaskId)\n },\n })\n\n const doneStatusIds = React.useMemo(\n () => new Set(statuses.filter((status) => status.isDone).map((status) => status.id)),\n [statuses],\n )\n\n const subtasksByTaskId = React.useMemo(\n () => computeSubtaskProgress(childrenQuery.data ?? [], doneStatusIds),\n [childrenQuery.data, doneStatusIds],\n )\n\n const assigneeIds = React.useMemo(() => {\n const ids = new Set<string>()\n for (const task of allTasks) {\n if (task.assigneeStaffMemberId) ids.add(task.assigneeStaffMemberId)\n }\n return Array.from(ids).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))\n }, [allTasks])\n\n const assigneeQuery = useAssigneeNames(assigneeIds)\n\n const tagIds = React.useMemo(() => {\n const ids = new Set<string>()\n for (const task of allTasks) {\n for (const id of task.tagIds) ids.add(id)\n }\n return Array.from(ids).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))\n }, [allTasks])\n\n const tagQuery = useTagLabels(tagIds)\n\n // A chip carries a tag's name or it is not drawn at all. The label lookup is a\n // separate request from the card's own row, so falling back to the id painted a\n // raw uuid on the card for as long as that request was in flight.\n const tagsByTaskId = React.useMemo(() => {\n const labels = tagQuery.data ?? new Map<string, string>()\n const map = new Map<string, KanbanTagOption[]>()\n for (const task of allTasks) {\n if (task.tagIds.length === 0) continue\n const named: KanbanTagOption[] = []\n for (const id of task.tagIds) {\n const label = labels.get(id)\n if (label) named.push({ id, label })\n }\n if (named.length === 0) continue\n map.set(task.id, named)\n }\n return map\n }, [allTasks, tagQuery.data])\n\n const selfQuery = useBoardSelf()\n\n const selfStaffMemberId = selfQuery.data?.id ?? null\n\n const runningTimerQuery = useQuery<{ entryId: string | null; taskId: string | null }>({\n queryKey: [...BOARD_QUERY_ROOT, 'running-timer', `scope:${scopeVersion}`, selfStaffMemberId ?? 'none'],\n enabled: !!selfStaffMemberId,\n staleTime: 15_000,\n refetchInterval: 30_000,\n queryFn: async () => {\n if (!selfStaffMemberId) return { entryId: null, taskId: null }\n const call = await apiCall<Record<string, unknown>>(\n `/api/staff/timesheets/time-entries?staffMemberId=${encodeURIComponent(selfStaffMemberId)}&running=true&pageSize=${BOARD_PAGE_SIZE}`,\n )\n if (!call.ok) return { entryId: null, taskId: null }\n for (const row of readItems(call.result)) {\n const endedAt = readString(row, 'ended_at', 'endedAt')\n const startedAt = readString(row, 'started_at', 'startedAt')\n if (!startedAt || endedAt) continue\n return { entryId: readString(row, 'id'), taskId: readString(row, 'task_id', 'taskId') }\n }\n return { entryId: null, taskId: null }\n },\n })\n\n const runningTimerTaskId = runningTimerQuery.data?.taskId ?? null\n const runningTimerEntryId = runningTimerQuery.data?.entryId ?? null\n\n const invalidateBoard = React.useCallback(() => {\n queryClient.invalidateQueries({ queryKey: [...BOARD_QUERY_ROOT, 'column'] }).catch(() => {})\n queryClient.invalidateQueries({ queryKey: [...BOARD_QUERY_ROOT, 'children'] }).catch(() => {})\n }, [queryClient])\n\n const invalidateUnassignedCount = React.useCallback(() => {\n queryClient.invalidateQueries({ queryKey: [...BOARD_QUERY_ROOT, 'unassigned-count'] }).catch(() => {})\n }, [queryClient])\n\n const invalidateStatuses = React.useCallback(() => {\n queryClient.invalidateQueries({ queryKey: [...BOARD_QUERY_ROOT, 'statuses'] }).catch(() => {})\n }, [queryClient])\n\n const markOwnMove = React.useCallback((taskId: string, statusId: string) => {\n ownMoveKeysRef.current.set(`${taskId}|${statusId}`, Date.now())\n }, [])\n\n const consumeOwnMove = React.useCallback((taskId: string, statusId: string): boolean => {\n const keys = ownMoveKeysRef.current\n const key = `${taskId}|${statusId}`\n const stamped = keys.get(key)\n // Drop stale entries so a broadcast that never arrived cannot suppress a later,\n // genuinely remote move of the same card into the same column.\n for (const [entry, at] of keys) {\n if (Date.now() - at > OWN_MOVE_ECHO_TTL_MS) keys.delete(entry)\n }\n if (stamped === undefined) return false\n keys.delete(key)\n return true\n }, [])\n\n useAppEvent(\n TASK_STATUS_CHANGED_EVENT,\n (event) => {\n const eventPayload = event.payload as Record<string, unknown> | null | undefined\n if (!eventPayload) return\n const projectId = typeof eventPayload.timeProjectId === 'string' ? eventPayload.timeProjectId : null\n if (projectId && projectId !== timeProjectId) return\n const taskId =\n typeof eventPayload.taskId === 'string'\n ? eventPayload.taskId\n : typeof eventPayload.id === 'string'\n ? eventPayload.id\n : null\n const statusId = typeof eventPayload.taskStatusId === 'string' ? eventPayload.taskStatusId : null\n if (taskId && statusId && consumeOwnMove(taskId, statusId)) return\n invalidateBoard()\n },\n [consumeOwnMove, invalidateBoard, timeProjectId],\n )\n\n useAppEvent(\n TASK_ASSIGNEE_CHANGED_EVENT,\n (event) => {\n const eventPayload = event.payload as Record<string, unknown> | null | undefined\n const projectId = typeof eventPayload?.timeProjectId === 'string' ? eventPayload.timeProjectId : null\n if (projectId && projectId !== timeProjectId) return\n invalidateBoard()\n invalidateUnassignedCount()\n },\n [invalidateBoard, invalidateUnassignedCount, timeProjectId],\n )\n\n useAppEvent(\n SUPPORT_ACTIVITY_EVENT,\n (event) => {\n const eventPayload = event.payload as Record<string, unknown> | null | undefined\n const projectId = typeof eventPayload?.projectId === 'string' ? eventPayload.projectId : null\n if (projectId && projectId !== timeProjectId) return\n invalidateBoard()\n invalidateUnassignedCount()\n },\n [invalidateBoard, invalidateUnassignedCount, timeProjectId],\n )\n\n const setTaskPending = React.useCallback((taskId: string, pending: boolean) => {\n setPendingTaskIds((previous) => {\n const next = new Set(previous)\n if (pending) next.add(taskId)\n else next.delete(taskId)\n return next\n })\n }, [])\n\n const moveTask = React.useCallback(\n (taskId: string, targetStatusId: string) => {\n const columnCaches = queryClient.getQueriesData<ColumnCache>({\n queryKey: [...BOARD_QUERY_ROOT, 'column'],\n })\n // The full pre-move picture. Restoring it wholesale is what puts the card back in\n // its origin column on failure, regardless of how many columns the drag touched.\n const snapshot = columnCaches.map(([key, data]) => [key, data] as const)\n\n let sourceStatusId: string | null = null\n let movingRow: BoardApiRow | null = null\n for (const [key, data] of columnCaches) {\n if (!data) continue\n const statusId = statusIdFromColumnKey(key as readonly unknown[])\n if (!statusId) continue\n const index = data.items.findIndex((item) => readString(item, 'id') === taskId)\n if (index < 0) continue\n sourceStatusId = statusId\n movingRow = { ...data.items[index], task_status_id: targetStatusId, taskStatusId: targetStatusId }\n queryClient.setQueryData(key, {\n items: data.items.filter((item) => readString(item, 'id') !== taskId),\n total: Math.max(0, data.total - 1),\n })\n break\n }\n\n if (!movingRow || !sourceStatusId || sourceStatusId === targetStatusId) {\n for (const [key, data] of snapshot) queryClient.setQueryData(key, data)\n return\n }\n\n const targetTasks = tasksByStatusId.get(targetStatusId)?.tasks ?? []\n const position = nextPositionAtColumnTop(targetTasks)\n const landed: BoardApiRow = { ...movingRow, position }\n\n for (const [key, data] of columnCaches) {\n if (!data) continue\n if (statusIdFromColumnKey(key as readonly unknown[]) !== targetStatusId) continue\n queryClient.setQueryData(key, {\n items: [landed, ...data.items],\n total: data.total + 1,\n })\n }\n\n const taskVersion = resolveTaskVersion(taskId, readString(movingRow, 'updated_at', 'updatedAt'))\n setTaskPending(taskId, true)\n setFailedMove(null)\n markOwnMove(taskId, targetStatusId)\n\n runMoveMutation({\n operation: async () => {\n const call = await withScopedApiRequestHeaders(buildOptimisticLockHeader(taskVersion), () =>\n apiCallOrThrow<Record<string, unknown>>(\n `/api/staff/timesheets/tasks/${encodeURIComponent(taskId)}/status`,\n {\n method: 'PATCH',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ taskStatusId: targetStatusId, position }),\n },\n { errorMessage: t('staff.time_tracking.board.move.error', 'Could not move the task.') },\n ),\n )\n return readString(call.result ?? {}, 'updatedAt', 'updated_at')\n },\n context: {\n formId: MOVE_MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.task',\n resourceId: taskId,\n retryLastMutation: retryMoveMutation,\n },\n })\n .then((confirmedVersion) => {\n // Note 4: the card the optimistic write left in the target column still\n // carries the version this move just spent, so the response's version has\n // to take over until a fetched row carries a newer one.\n if (confirmedVersion) confirmedVersionsRef.current.set(taskId, confirmedVersion)\n invalidateBoard()\n })\n .catch((error: unknown) => {\n // Card first: it goes back where it came from before anything else is decided.\n for (const [key, data] of snapshot) queryClient.setQueryData(key, data)\n consumeOwnMove(taskId, targetStatusId)\n if (surfaceRecordConflict(error, t)) {\n // Somebody else moved this card. Refetch and let the conflict bar speak \u2014\n // retrying here would silently overwrite their move. The version this board\n // remembered is behind the server's, so the refetched row takes over.\n confirmedVersionsRef.current.delete(taskId)\n invalidateBoard()\n return\n }\n setFailedMove({ taskId, targetStatusId })\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('staff.time_tracking.board.move.error', 'Could not move the task.')\n flash(message, 'error')\n })\n .finally(() => {\n setTaskPending(taskId, false)\n })\n },\n [\n consumeOwnMove,\n invalidateBoard,\n markOwnMove,\n queryClient,\n resolveTaskVersion,\n retryMoveMutation,\n runMoveMutation,\n setTaskPending,\n t,\n tasksByStatusId,\n ],\n )\n\n // Pointer only. dnd-kit's keyboard sensor would have to own Enter/Space on the card \u2014\n // the key that opens the drawer \u2014 and it has no sortable context to traverse anyway;\n // keyboard moves go through the card's explicit \"Move\" menu, which calls `moveTask`\n // directly (U3).\n const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 4 } }))\n\n const collisionDetection = React.useCallback<CollisionDetection>((args) => pointerWithin(args), [])\n\n const measuringConfig = React.useMemo(\n () => ({ droppable: { strategy: MeasuringStrategy.BeforeDragging } }),\n [],\n )\n\n const statusIdByTaskId = React.useMemo(() => {\n const map = new Map<string, string>()\n for (const [statusId, entry] of tasksByStatusId) {\n for (const task of entry.tasks) map.set(task.id, statusId)\n }\n return map\n }, [tasksByStatusId])\n\n const activeDragTask = React.useMemo(\n () => (activeDragTaskId ? allTasks.find((task) => task.id === activeDragTaskId) ?? null : null),\n [activeDragTaskId, allTasks],\n )\n\n const handleDragStart = React.useCallback((event: DragStartEvent) => {\n setActiveDragTaskId(typeof event.active.id === 'string' ? event.active.id : null)\n }, [])\n\n const handleDragCancel = React.useCallback(() => setActiveDragTaskId(null), [])\n\n const targetStatusIdFor = React.useCallback(\n (overId: UniqueIdentifier | null | undefined): string | null => {\n if (typeof overId !== 'string') return null\n return overId.startsWith(KANBAN_COLUMN_DROPPABLE_PREFIX)\n ? overId.slice(KANBAN_COLUMN_DROPPABLE_PREFIX.length)\n : statusIdByTaskId.get(overId) ?? null\n },\n [statusIdByTaskId],\n )\n\n const handleDragEnd = React.useCallback(\n (event: DragEndEvent) => {\n setActiveDragTaskId(null)\n const taskId = typeof event.active.id === 'string' ? event.active.id : null\n if (!taskId) return\n const targetStatusId = targetStatusIdFor(event.over?.id)\n if (!targetStatusId) return\n if (statusIdByTaskId.get(taskId) === targetStatusId) return\n moveTask(taskId, targetStatusId)\n },\n [moveTask, statusIdByTaskId, targetStatusIdFor],\n )\n\n const moveTargets = React.useMemo<KanbanMoveTarget[]>(\n () => visibleStatuses.map((status) => ({ id: status.id, name: status.name })),\n [visibleStatuses],\n )\n\n const describeTask = React.useCallback(\n (taskId: UniqueIdentifier | null | undefined): string => {\n if (typeof taskId !== 'string') return ''\n return allTasks.find((task) => task.id === taskId)?.title ?? taskId\n },\n [allTasks],\n )\n\n const describeColumn = React.useCallback(\n (overId: UniqueIdentifier | null | undefined): string | null => {\n const statusId = targetStatusIdFor(overId)\n if (!statusId) return null\n return statuses.find((status) => status.id === statusId)?.name ?? statusId\n },\n [statuses, targetStatusIdFor],\n )\n\n // dnd-kit narrates every drag through a live region. Left unset it narrates in English\n // regardless of locale (U9), so the board owns the strings.\n const announcements = React.useMemo<Announcements>(\n () => ({\n onDragStart: ({ active }) =>\n t('staff.time_tracking.board.dnd.dragStart', 'Picked up the task {task}.', {\n task: describeTask(active.id),\n }),\n onDragOver: ({ active, over }) => {\n const column = describeColumn(over?.id)\n return column\n ? t('staff.time_tracking.board.dnd.dragOver', 'The task {task} is over the column {column}.', {\n task: describeTask(active.id),\n column,\n })\n : t('staff.time_tracking.board.dnd.dragOverNothing', 'The task {task} is not over a column.', {\n task: describeTask(active.id),\n })\n },\n onDragEnd: ({ active, over }) => {\n const column = describeColumn(over?.id)\n return column\n ? t('staff.time_tracking.board.dnd.dragEnd', 'The task {task} was dropped into the column {column}.', {\n task: describeTask(active.id),\n column,\n })\n : t('staff.time_tracking.board.dnd.dragEndOutside', 'The task {task} was dropped outside the columns.', {\n task: describeTask(active.id),\n })\n },\n onDragCancel: ({ active }) =>\n t('staff.time_tracking.board.dnd.dragCancel', 'Moving the task {task} was cancelled.', {\n task: describeTask(active.id),\n }),\n }),\n [describeColumn, describeTask, t],\n )\n\n const screenReaderInstructions = React.useMemo<ScreenReaderInstructions>(\n () => ({\n draggable: t(\n 'staff.time_tracking.board.dnd.instructions',\n 'Drag a card with the pointer to move it between columns. With the keyboard, press Enter on a card to open it, or use the Move button in the card actions to choose a target column.',\n ),\n }),\n [t],\n )\n\n const accessibility = React.useMemo(\n () => ({ announcements, screenReaderInstructions }),\n [announcements, screenReaderInstructions],\n )\n\n const handleQuickAdd = React.useCallback(\n async (statusId: string, title: string) => {\n setQuickAddPendingStatusId(statusId)\n try {\n await runCreateMutation({\n operation: () =>\n apiCallOrThrow(\n '/api/staff/timesheets/tasks',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n timeProjectId,\n taskStatusId: statusId,\n title,\n // US-C1: the creator owns what they just typed until somebody reassigns it.\n assigneeStaffMemberId: selfStaffMemberId,\n }),\n },\n { errorMessage: t('staff.time_tracking.board.quickAdd.error', 'Could not create the task.') },\n ),\n context: {\n formId: CREATE_MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.task',\n resourceId: statusId,\n retryLastMutation: retryCreateMutation,\n },\n })\n invalidateBoard()\n } catch (error) {\n if (surfaceRecordConflict(error, t)) return\n logger.error('staff.time_tracking board quick add failed', { err: error })\n flash(\n error instanceof Error && error.message\n ? error.message\n : t('staff.time_tracking.board.quickAdd.error', 'Could not create the task.'),\n 'error',\n )\n } finally {\n setQuickAddPendingStatusId(null)\n }\n },\n [invalidateBoard, retryCreateMutation, runCreateMutation, selfStaffMemberId, t, timeProjectId],\n )\n\n const handleCreateStatus = React.useCallback(async () => {\n const name = statusDraft.trim()\n if (!name) {\n setComposingStatus(false)\n setStatusDraft('')\n return\n }\n try {\n await runCreateMutation({\n operation: () =>\n apiCallOrThrow(\n '/api/staff/timesheets/task-statuses',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ timeProjectId, name }),\n },\n { errorMessage: t('staff.time_tracking.board.addStatus.error', 'Could not add the column.') },\n ),\n context: {\n formId: CREATE_MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.task_status',\n resourceId: timeProjectId,\n retryLastMutation: retryCreateMutation,\n },\n })\n setStatusDraft('')\n setComposingStatus(false)\n invalidateStatuses()\n } catch (error) {\n if (surfaceRecordConflict(error, t)) return\n flash(\n error instanceof Error && error.message\n ? error.message\n : t('staff.time_tracking.board.addStatus.error', 'Could not add the column.'),\n 'error',\n )\n }\n }, [invalidateStatuses, retryCreateMutation, runCreateMutation, statusDraft, t, timeProjectId])\n\n const handleLoadMore = React.useCallback((statusId: string) => {\n setPagesByStatus((previous) => ({ ...previous, [statusId]: (previous[statusId] ?? 1) + 1 }))\n }, [])\n\n const openTask = React.useCallback(\n (taskId: string) => {\n // Seam for the task drawer (T3.7): the drawer reads `?task=` and the board keeps\n // rendering underneath it.\n const params = new URLSearchParams(searchParams?.toString() ?? '')\n params.set('task', taskId)\n router.push(`?${params.toString()}`, { scroll: false })\n },\n [router, searchParams],\n )\n\n const handleAddTime = React.useCallback(\n (taskId: string) => {\n const params = new URLSearchParams(searchParams?.toString() ?? '')\n params.set('task', taskId)\n params.set('panel', 'time')\n router.push(`?${params.toString()}`, { scroll: false })\n },\n [router, searchParams],\n )\n\n const drawerTaskId = searchParams?.get('task') ?? null\n const drawerPanel = searchParams?.get('panel') ?? null\n\n const closeTaskDrawer = React.useCallback(() => {\n const params = new URLSearchParams(searchParams?.toString() ?? '')\n params.delete('task')\n params.delete('panel')\n const query = params.toString()\n router.push(query.length > 0 ? `?${query}` : '?', { scroll: false })\n }, [router, searchParams])\n\n const handleStartTimer = React.useCallback(\n async (taskId: string) => {\n if (!selfStaffMemberId) return\n const task = allTasks.find((entry) => entry.id === taskId) ?? null\n try {\n const started = await runCreateMutation({\n operation: () =>\n apiCallOrThrow<Record<string, unknown>>(\n '/api/staff/timesheets/time-entries/start-timer',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n staffMemberId: selfStaffMemberId,\n timeProjectId,\n date: new Date().toISOString().slice(0, 10),\n notes: task?.title ?? null,\n }),\n },\n { errorMessage: t('staff.time_tracking.board.timer.startError', 'Could not start the timer.') },\n ),\n context: {\n formId: CREATE_MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: taskId,\n retryLastMutation: retryCreateMutation,\n },\n })\n const entryId = typeof started?.result?.id === 'string' ? started.result.id : null\n if (entryId) {\n // `start-timer` creates the entry atomically but takes no task, so the link is a\n // second write. A failure here leaves a running, project-level timer rather than\n // no timer at all \u2014 the lesser of the two wrong states.\n const linked = await apiCall(`/api/staff/timesheets/time-entries`, {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: entryId, taskId }),\n })\n if (!linked.ok) {\n flash(\n t(\n 'staff.time_tracking.board.timer.linkWarning',\n 'The timer is running but is not linked to this task yet.',\n ),\n 'warning',\n )\n }\n }\n await runningTimerQuery.refetch()\n } catch (error) {\n if (surfaceRecordConflict(error, t)) return\n flash(\n error instanceof Error && error.message\n ? error.message\n : t('staff.time_tracking.board.timer.startError', 'Could not start the timer.'),\n 'error',\n )\n }\n },\n [allTasks, retryCreateMutation, runCreateMutation, runningTimerQuery, selfStaffMemberId, t, timeProjectId],\n )\n\n const handleStopTimer = React.useCallback(async () => {\n if (!runningTimerEntryId) return\n try {\n await runCreateMutation({\n operation: () =>\n apiCallOrThrow(\n `/api/staff/timesheets/time-entries/${encodeURIComponent(runningTimerEntryId)}/timer-stop`,\n { method: 'POST' },\n { errorMessage: t('staff.time_tracking.board.timer.stopError', 'Could not stop the timer.') },\n ),\n context: {\n formId: CREATE_MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: runningTimerEntryId,\n retryLastMutation: retryCreateMutation,\n },\n })\n await runningTimerQuery.refetch()\n invalidateBoard()\n } catch (error) {\n if (surfaceRecordConflict(error, t)) return\n flash(\n error instanceof Error && error.message\n ? error.message\n : t('staff.time_tracking.board.timer.stopError', 'Could not stop the timer.'),\n 'error',\n )\n }\n }, [invalidateBoard, retryCreateMutation, runCreateMutation, runningTimerEntryId, runningTimerQuery, t])\n\n // Never fold `loggedMinutes` inline: a parent's figure already contains its children's,\n // so the shared helper \u2014 which drops any row whose parent is in the same set \u2014 is the\n // only thing standing between this number and risk R10.\n const summary = React.useMemo<BoardSummary>(\n () => ({ taskCount: allTasks.length, loggedMinutes: sumTaskLoggedMinutes(allTasks) }),\n [allTasks],\n )\n\n React.useEffect(() => {\n onSummaryChange?.(summary)\n }, [onSummaryChange, summary])\n\n if (statusesQuery.isLoading) {\n return <LoadingMessage label={t('staff.time_tracking.board.loading', 'Loading the board\u2026')} />\n }\n\n if (statusesQuery.isError) {\n return (\n <ErrorMessage\n label={t('staff.time_tracking.board.loadError', 'Could not load the board.')}\n action={\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void statusesQuery.refetch()}>\n {t('staff.time_tracking.board.reload', 'Try again')}\n </Button>\n }\n />\n )\n }\n\n if (statuses.length === 0) {\n return (\n <EmptyState\n title={t('staff.time_tracking.board.empty.title', 'This project has no columns yet')}\n description={t(\n 'staff.time_tracking.board.empty.description',\n 'A Team Leader configures the board columns for each project.',\n )}\n />\n )\n }\n\n return (\n <div className=\"flex flex-col gap-4\">\n <InjectionSpot\n spotId={extensionPoints.hosts.taskBoardToolbar.spotId}\n context={{ timeProjectId, projectName: projectName ?? null, statusIds: visibleStatuses.map((status) => status.id) }}\n data={summary}\n />\n\n {failedMove ? (\n <Alert status=\"error\" data-testid=\"kanban-move-retry\">\n <AlertDescription className=\"flex flex-wrap items-center gap-3\">\n <span>\n {t('staff.time_tracking.board.move.rolledBack', 'The task was moved back \u2014 the change was not saved.')}\n </span>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n const retry = failedMove\n setFailedMove(null)\n moveTask(retry.taskId, retry.targetStatusId)\n }}\n >\n <RotateCcw className=\"size-3.5\" aria-hidden=\"true\" />\n {t('staff.time_tracking.board.move.retry', 'Retry')}\n </Button>\n </AlertDescription>\n </Alert>\n ) : null}\n\n <DndContext\n sensors={sensors}\n accessibility={accessibility}\n collisionDetection={collisionDetection}\n measuring={measuringConfig}\n onDragStart={handleDragStart}\n onDragEnd={handleDragEnd}\n onDragCancel={handleDragCancel}\n >\n <div\n className={`flex min-w-0 gap-3.5 overflow-x-auto pb-6 ${\n activeDragTaskId ? 'cursor-grabbing select-none' : ''\n }`}\n >\n {visibleStatuses.map((status, index) => {\n const entry = tasksByStatusId.get(status.id)\n const serverTotal = entry?.total ?? 0\n const loaded = entry?.loaded ?? 0\n return (\n <KanbanColumn\n key={status.id}\n status={status}\n tasks={entry?.tasks ?? []}\n total={serverTotal}\n remaining={Math.max(0, serverTotal - loaded)}\n canQuickAdd={canManageTasks}\n loadingMore={columnQueries[index]?.isFetching ?? false}\n activeDragTaskId={activeDragTaskId}\n pendingTaskIds={pendingTaskIds}\n runningTimerTaskId={runningTimerTaskId}\n assigneeNames={assigneeQuery.data ?? new Map<string, string>()}\n tagsByTaskId={tagsByTaskId}\n subtasksByTaskId={subtasksByTaskId}\n quickAddPending={quickAddPendingStatusId === status.id}\n moveTargets={moveTargets}\n onQuickAdd={handleQuickAdd}\n onLoadMore={handleLoadMore}\n onOpenTask={openTask}\n onStartTimer={handleStartTimer}\n onStopTimer={handleStopTimer}\n onAddTime={handleAddTime}\n onMoveTask={moveTask}\n />\n )\n })}\n\n {canManageColumns ? (\n <div className=\"flex w-72 flex-none flex-col gap-3\">\n {composingStatus ? (\n <Input\n autoFocus\n value={statusDraft}\n onChange={(event) => setStatusDraft(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n void handleCreateStatus()\n }\n if (event.key === 'Escape') {\n event.preventDefault()\n setComposingStatus(false)\n setStatusDraft('')\n }\n }}\n placeholder={t('staff.time_tracking.board.addStatus.placeholder', 'Column name')}\n aria-label={t('staff.time_tracking.board.addStatus.cta', 'Add status')}\n data-testid=\"kanban-add-status-input\"\n />\n ) : (\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={() => setComposingStatus(true)}\n data-testid=\"kanban-add-status\"\n className=\"flex h-24 flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-muted-foreground/60 bg-muted/40 text-sm font-semibold leading-normal text-muted-foreground hover:text-foreground\"\n >\n <Plus className=\"size-5\" aria-hidden=\"true\" />\n {t('staff.time_tracking.board.addStatus.cta', 'Add status')}\n </Button>\n )}\n </div>\n ) : null}\n </div>\n\n <DragOverlay dropAnimation={null}>\n {activeDragTask ? (\n <div\n data-testid=\"kanban-drag-overlay\"\n className=\"pointer-events-none w-72 rotate-2 cursor-grabbing select-none rounded-lg border border-border bg-card px-4 py-3 opacity-80 shadow-xl\"\n >\n <div className=\"text-sm font-semibold leading-normal text-foreground\">\n {activeDragTask.title}\n </div>\n <div className=\"mt-2 text-xs font-semibold tabular-nums leading-normal text-muted-foreground\">\n {formatBoardMinutes(activeDragTask.loggedMinutes)}\n </div>\n </div>\n ) : null}\n </DragOverlay>\n </DndContext>\n\n {drawerTaskId ? (\n <TaskDrawer\n taskId={drawerTaskId}\n open\n onOpenChange={(next) => {\n if (!next) closeTaskDrawer()\n }}\n projectName={projectName ?? null}\n focusPanel={drawerPanel}\n onChanged={invalidateBoard}\n />\n ) : null}\n </div>\n )\n}\n\nexport default KanbanBoard\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -156,6 +156,10 @@ function KanbanCardImpl({
|
|
|
156
156
|
className: `group relative flex w-full flex-col gap-2 rounded-lg border border-border bg-card px-4 py-3 shadow-xs transition-shadow ${dimmed ? "cursor-grabbing opacity-30" : "cursor-grab hover:shadow-sm active:cursor-grabbing"} ${pending ? "opacity-70" : ""}`,
|
|
157
157
|
children: [
|
|
158
158
|
/* @__PURE__ */ jsx("div", { className: "text-sm font-semibold leading-normal text-foreground", children: task.title }),
|
|
159
|
+
task.supportReference ? /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1", "data-testid": "task-card-support", children: [
|
|
160
|
+
/* @__PURE__ */ jsx(Badge, { variant: "brand", size: "sm", children: t("staff.time_tracking.board.card.customerRequest", "Customer request") }),
|
|
161
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: task.supportReference })
|
|
162
|
+
] }) : null,
|
|
159
163
|
tags.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: tags.map((tag) => /* @__PURE__ */ jsx(Badge, { variant: "info", size: "sm", children: tag.label }, tag.id)) }) : null,
|
|
160
164
|
/* @__PURE__ */ jsx(
|
|
161
165
|
InjectionSpot,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/staff/lib/time-tracking-ui/KanbanCard.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { z } from 'zod'\nimport { useDraggable } from '@dnd-kit/core'\nimport { ArrowRightLeft, Check, Play, Plus, Square } from 'lucide-react'\nimport { Avatar } from '@open-mercato/ui/primitives/avatar'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Progress } from '@open-mercato/ui/primitives/progress'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'\nimport { registerComponent } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'\nimport { callbackProp, opaqueProp, optionalCallbackProp } from '../time-tracking/componentContracts'\nimport {\n formatBoardMinutes,\n initialsFromName,\n type BoardTask,\n type SubtaskProgress,\n} from './kanbanBoardData'\n\n/**\n * Quick actions are hover- and focus-revealed on a pointer device (mockup note 3),\n * which leaves them unreachable on a touch screen where neither state exists. The\n * media query is read at runtime rather than expressed as a Tailwind variant so the\n * card can hand the same `visible` decision to both the class list and the tests.\n */\nexport function useCoarsePointer(): boolean {\n const [coarse, setCoarse] = React.useState(false)\n React.useEffect(() => {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return\n const query = window.matchMedia('(hover: none)')\n setCoarse(query.matches)\n const listener = (event: MediaQueryListEvent) => setCoarse(event.matches)\n if (typeof query.addEventListener === 'function') {\n query.addEventListener('change', listener)\n return () => query.removeEventListener('change', listener)\n }\n return undefined\n }, [])\n return coarse\n}\n\nconst TASK_ENTITY_ID = 'staff:staff_time_task'\n\nexport type KanbanTagOption = {\n id: string\n label: string\n}\n\n/** A column this card can be moved into \u2014 every board column except its own. */\nexport type KanbanMoveTarget = {\n id: string\n name: string\n}\n\nexport type KanbanCardProps = {\n task: BoardTask\n assigneeName: string | null\n tags: KanbanTagOption[]\n subtasks: SubtaskProgress | null\n /** A running timer marks the card permanently \u2014 never hover-dependent (note 3). */\n timerRunning: boolean\n pending: boolean\n isActiveDrag: boolean\n /**\n * Columns offered by the keyboard move menu. Empty (the default) hides the\n * control, which is what a single-column board should do.\n */\n moveTargets?: readonly KanbanMoveTarget[]\n onOpen: (taskId: string) => void\n onStartTimer: (taskId: string) => void\n onStopTimer: (taskId: string) => void\n onAddTime: (taskId: string) => void\n /** Same move the drag performs \u2014 optimistic update, lock header, rollback and all. */\n onMoveToColumn?: (taskId: string, targetStatusId: string) => void\n}\n\nconst EMPTY_MOVE_TARGETS: readonly KanbanMoveTarget[] = []\n\nfunction KanbanCardImpl({\n task,\n assigneeName,\n tags,\n subtasks,\n timerRunning,\n pending,\n isActiveDrag,\n moveTargets = EMPTY_MOVE_TARGETS,\n onOpen,\n onStartTimer,\n onStopTimer,\n onAddTime,\n onMoveToColumn,\n}: KanbanCardProps): React.ReactElement {\n const t = useT()\n const coarsePointer = useCoarsePointer()\n const { attributes, listeners, setNodeRef, isDragging } = useDraggable({\n id: task.id,\n data: { type: 'task', taskStatusId: task.taskStatusId },\n })\n const dimmed = isActiveDrag || isDragging\n\n // dnd-kit's draggable listeners take pointer capture on the card, which rewrites the\n // subsequent click's target to the card itself and swallows the inner buttons. Cutting\n // the pointerdown at the action row keeps Start / Stop / Add time clickable.\n const stopPointerDown = React.useCallback((event: React.PointerEvent) => {\n event.stopPropagation()\n }, [])\n\n const handleCardClick = React.useCallback(\n (event: React.MouseEvent<HTMLDivElement>) => {\n const target = event.target as HTMLElement\n if (target.closest('[data-card-action=\"true\"]')) return\n onOpen(task.id)\n },\n [onOpen, task.id],\n )\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key !== 'Enter') return\n const target = event.target as HTMLElement\n if (target.closest('[data-card-action=\"true\"]')) return\n event.preventDefault()\n onOpen(task.id)\n },\n [onOpen, task.id],\n )\n\n // Keyboard operability of the board does NOT go through dnd-kit: its keyboard sensor\n // would have to own Enter/Space on the card, and Enter is what opens the drawer. The\n // move is an explicit menu instead \u2014 same `onMoveToColumn` the drop calls, so a keyboard\n // move is the identical optimistic, version-locked request a drag issues.\n const [moveMenuOpen, setMoveMenuOpen] = React.useState(false)\n const moveTriggerRef = React.useRef<HTMLButtonElement | null>(null)\n const moveMenuRef = React.useRef<HTMLDivElement | null>(null)\n const moveItemRefs = React.useRef<(HTMLButtonElement | null)[]>([])\n const showMoveMenu = moveTargets.length > 0 && typeof onMoveToColumn === 'function'\n\n React.useEffect(() => {\n if (!moveMenuOpen) return\n moveItemRefs.current[0]?.focus()\n }, [moveMenuOpen])\n\n React.useEffect(() => {\n if (!moveMenuOpen) return\n const handleOutside = (event: MouseEvent) => {\n const target = event.target as Node\n if (moveMenuRef.current?.contains(target)) return\n if (moveTriggerRef.current?.contains(target)) return\n setMoveMenuOpen(false)\n }\n document.addEventListener('mousedown', handleOutside)\n return () => document.removeEventListener('mousedown', handleOutside)\n }, [moveMenuOpen])\n\n const closeMoveMenu = React.useCallback(() => {\n setMoveMenuOpen(false)\n moveTriggerRef.current?.focus()\n }, [])\n\n const handleMoveMenuKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'Escape') {\n event.preventDefault()\n closeMoveMenu()\n return\n }\n if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return\n event.preventDefault()\n const items = moveItemRefs.current.filter(\n (item): item is HTMLButtonElement => item !== null && item.isConnected,\n )\n if (items.length === 0) return\n const current = items.indexOf(document.activeElement as HTMLButtonElement)\n const step = event.key === 'ArrowDown' ? 1 : -1\n const next = current < 0 ? 0 : (current + step + items.length) % items.length\n items[next]?.focus()\n },\n [closeMoveMenu],\n )\n\n const handleMoveSelect = React.useCallback(\n (targetStatusId: string) => {\n setMoveMenuOpen(false)\n moveTriggerRef.current?.focus()\n onMoveToColumn?.(task.id, targetStatusId)\n },\n [onMoveToColumn, task.id],\n )\n\n const quickActionsVisible = coarsePointer\n ? 'opacity-100'\n : 'opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100'\n\n const cardInjectionContext = React.useMemo(\n () => ({\n entityId: TASK_ENTITY_ID,\n recordId: task.id,\n taskId: task.id,\n timeProjectId: task.timeProjectId ?? null,\n taskStatusId: task.taskStatusId ?? null,\n timerRunning,\n }),\n [task.id, task.taskStatusId, task.timeProjectId, timerRunning],\n )\n\n const subtaskPercent = subtasks && subtasks.total > 0\n ? Math.round((subtasks.done / subtasks.total) * 100)\n : 0\n\n return (\n <div\n ref={setNodeRef}\n {...attributes}\n {...listeners}\n data-task-card={task.id}\n aria-label={t('staff.time_tracking.board.card.aria', 'Task: {title}', { title: task.title })}\n onClick={handleCardClick}\n onKeyDown={handleKeyDown}\n className={`group relative flex w-full flex-col gap-2 rounded-lg border border-border bg-card px-4 py-3 shadow-xs transition-shadow ${\n dimmed ? 'cursor-grabbing opacity-30' : 'cursor-grab hover:shadow-sm active:cursor-grabbing'\n } ${pending ? 'opacity-70' : ''}`}\n >\n <div className=\"text-sm font-semibold leading-normal text-foreground\">{task.title}</div>\n\n {tags.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {tags.map((tag) => (\n <Badge key={tag.id} variant=\"info\" size=\"sm\">\n {tag.label}\n </Badge>\n ))}\n </div>\n ) : null}\n\n <InjectionSpot\n spotId={extensionPoints.hosts.taskBoardCardBadges.spotId}\n context={cardInjectionContext}\n data={task}\n />\n\n {subtasks && subtasks.total > 0 ? (\n <div className=\"flex flex-col gap-1\">\n <span className=\"flex items-center gap-1 text-xs font-medium leading-normal text-muted-foreground\">\n <Check className=\"size-3\" aria-hidden=\"true\" />\n {t('staff.time_tracking.board.card.subtasks', '{done}/{total} subtasks', {\n done: subtasks.done,\n total: subtasks.total,\n })}\n </span>\n <Progress value={subtaskPercent} size=\"sm\" tone=\"success\" />\n </div>\n ) : null}\n\n <div className=\"h-px w-full bg-border\" aria-hidden=\"true\" />\n\n <div className=\"flex items-center gap-2\">\n <Avatar\n size=\"xs\"\n label={initialsFromName(assigneeName)}\n ariaLabel={\n assigneeName\n ? t('staff.time_tracking.board.card.assignee', 'Assigned to {name}', { name: assigneeName })\n : t('staff.time_tracking.board.card.unassigned', 'Unassigned')\n }\n />\n {timerRunning ? (\n <Badge variant=\"success\" size=\"sm\" dot>\n {t('staff.time_tracking.board.card.timerRunning', 'timer')}\n </Badge>\n ) : null}\n <span className=\"grow\" />\n <span className=\"text-xs font-semibold tabular-nums leading-normal text-foreground\">\n {formatBoardMinutes(task.loggedMinutes)}\n </span>\n </div>\n\n <div\n data-card-action=\"true\"\n data-testid={`kanban-card-actions-${task.id}`}\n onPointerDown={stopPointerDown}\n className={`flex flex-wrap items-center gap-1.5 ${moveMenuOpen ? 'opacity-100' : quickActionsVisible}`}\n >\n {timerRunning ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"2xs\"\n aria-label={t('staff.time_tracking.board.card.stopTimerAria', 'Stop the timer for {title}', {\n title: task.title,\n })}\n onClick={() => onStopTimer(task.id)}\n >\n <Square className=\"size-3\" aria-hidden=\"true\" />\n {t('staff.time_tracking.board.card.stopTimer', 'Stop')}\n </Button>\n ) : (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"2xs\"\n aria-label={t('staff.time_tracking.board.card.startTimerAria', 'Start a timer for {title}', {\n title: task.title,\n })}\n onClick={() => onStartTimer(task.id)}\n >\n <Play className=\"size-3\" aria-hidden=\"true\" />\n {t('staff.time_tracking.board.card.startTimer', 'Start')}\n </Button>\n )}\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"2xs\"\n aria-label={t('staff.time_tracking.board.card.addTimeAria', 'Add time to {title}', {\n title: task.title,\n })}\n onClick={() => onAddTime(task.id)}\n >\n <Plus className=\"size-3\" aria-hidden=\"true\" />\n {t('staff.time_tracking.board.card.addTime', 'Add time')}\n </Button>\n {showMoveMenu ? (\n <div className=\"relative\">\n <Button\n ref={moveTriggerRef}\n type=\"button\"\n variant=\"outline\"\n size=\"2xs\"\n aria-haspopup=\"menu\"\n aria-expanded={moveMenuOpen}\n aria-label={t('staff.time_tracking.board.card.moveTo.aria', 'Move {title} to another column', {\n title: task.title,\n })}\n data-testid={`kanban-card-move-${task.id}`}\n onClick={() => setMoveMenuOpen((open) => !open)}\n onKeyDown={(event) => {\n if (event.key !== 'ArrowDown') return\n event.preventDefault()\n setMoveMenuOpen(true)\n }}\n >\n <ArrowRightLeft className=\"size-3\" aria-hidden=\"true\" />\n {t('staff.time_tracking.board.card.moveTo.cta', 'Move')}\n </Button>\n {moveMenuOpen ? (\n <div\n ref={moveMenuRef}\n role=\"menu\"\n aria-label={t('staff.time_tracking.board.card.moveTo.menu', 'Move {title} to', {\n title: task.title,\n })}\n data-testid={`kanban-card-move-menu-${task.id}`}\n onKeyDown={handleMoveMenuKeyDown}\n className=\"absolute bottom-full left-0 z-dropdown mb-1 flex min-w-40 flex-col gap-0.5 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md\"\n >\n {moveTargets.map((target, index) => (\n <Button\n key={target.id}\n ref={(node: HTMLButtonElement | null) => {\n moveItemRefs.current[index] = node\n }}\n type=\"button\"\n variant=\"ghost\"\n size=\"2xs\"\n role=\"menuitem\"\n className=\"justify-start\"\n data-testid={`kanban-card-move-option-${task.id}-${target.id}`}\n onClick={() => handleMoveSelect(target.id)}\n >\n {target.name}\n </Button>\n ))}\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n\n <InjectionSpot\n spotId={extensionPoints.hosts.taskBoardCardFooter.spotId}\n context={cardInjectionContext}\n data={task}\n />\n </div>\n )\n}\n\nconst DefaultKanbanCard = React.memo(KanbanCardImpl)\n\nconst kanbanTagOptionSchema = z.object({ id: z.string(), label: z.string() })\n\nconst kanbanMoveTargetSchema = z.object({ id: z.string(), name: z.string() })\n\nconst kanbanCardPropsSchema: z.ZodType<KanbanCardProps> = z.object({\n task: opaqueProp<BoardTask>(),\n assigneeName: z.string().nullable(),\n tags: z.array(kanbanTagOptionSchema),\n subtasks: opaqueProp<SubtaskProgress>().nullable(),\n timerRunning: z.boolean(),\n pending: z.boolean(),\n isActiveDrag: z.boolean(),\n moveTargets: z.array(kanbanMoveTargetSchema).optional(),\n onOpen: callbackProp<(taskId: string) => void>(),\n onStartTimer: callbackProp<(taskId: string) => void>(),\n onStopTimer: callbackProp<(taskId: string) => void>(),\n onAddTime: callbackProp<(taskId: string) => void>(),\n onMoveToColumn: optionalCallbackProp<(taskId: string, targetStatusId: string) => void>(),\n})\n\nregisterComponent<KanbanCardProps>({\n id: extensionPoints.hosts.kanbanCardComponent.componentId,\n component: DefaultKanbanCard,\n metadata: {\n module: 'staff',\n description: 'One task card on the time-tracking Kanban board.',\n propsSchema: kanbanCardPropsSchema,\n },\n})\n\nfunction KanbanCardHost(props: KanbanCardProps): React.ReactElement {\n const Resolved = useRegisteredComponent<KanbanCardProps>(\n extensionPoints.hosts.kanbanCardComponent.componentId,\n DefaultKanbanCard,\n )\n return <Resolved {...props} />\n}\n\nexport const KanbanCard = React.memo(KanbanCardHost)\n\nexport default KanbanCard\n"],
|
|
5
|
-
"mappings": ";AAmOM,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { z } from 'zod'\nimport { useDraggable } from '@dnd-kit/core'\nimport { ArrowRightLeft, Check, Play, Plus, Square } from 'lucide-react'\nimport { Avatar } from '@open-mercato/ui/primitives/avatar'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Progress } from '@open-mercato/ui/primitives/progress'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'\nimport { registerComponent } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'\nimport { callbackProp, opaqueProp, optionalCallbackProp } from '../time-tracking/componentContracts'\nimport {\n formatBoardMinutes,\n initialsFromName,\n type BoardTask,\n type SubtaskProgress,\n} from './kanbanBoardData'\n\n/**\n * Quick actions are hover- and focus-revealed on a pointer device (mockup note 3),\n * which leaves them unreachable on a touch screen where neither state exists. The\n * media query is read at runtime rather than expressed as a Tailwind variant so the\n * card can hand the same `visible` decision to both the class list and the tests.\n */\nexport function useCoarsePointer(): boolean {\n const [coarse, setCoarse] = React.useState(false)\n React.useEffect(() => {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return\n const query = window.matchMedia('(hover: none)')\n setCoarse(query.matches)\n const listener = (event: MediaQueryListEvent) => setCoarse(event.matches)\n if (typeof query.addEventListener === 'function') {\n query.addEventListener('change', listener)\n return () => query.removeEventListener('change', listener)\n }\n return undefined\n }, [])\n return coarse\n}\n\nconst TASK_ENTITY_ID = 'staff:staff_time_task'\n\nexport type KanbanTagOption = {\n id: string\n label: string\n}\n\n/** A column this card can be moved into \u2014 every board column except its own. */\nexport type KanbanMoveTarget = {\n id: string\n name: string\n}\n\nexport type KanbanCardProps = {\n task: BoardTask\n assigneeName: string | null\n tags: KanbanTagOption[]\n subtasks: SubtaskProgress | null\n /** A running timer marks the card permanently \u2014 never hover-dependent (note 3). */\n timerRunning: boolean\n pending: boolean\n isActiveDrag: boolean\n /**\n * Columns offered by the keyboard move menu. Empty (the default) hides the\n * control, which is what a single-column board should do.\n */\n moveTargets?: readonly KanbanMoveTarget[]\n onOpen: (taskId: string) => void\n onStartTimer: (taskId: string) => void\n onStopTimer: (taskId: string) => void\n onAddTime: (taskId: string) => void\n /** Same move the drag performs \u2014 optimistic update, lock header, rollback and all. */\n onMoveToColumn?: (taskId: string, targetStatusId: string) => void\n}\n\nconst EMPTY_MOVE_TARGETS: readonly KanbanMoveTarget[] = []\n\nfunction KanbanCardImpl({\n task,\n assigneeName,\n tags,\n subtasks,\n timerRunning,\n pending,\n isActiveDrag,\n moveTargets = EMPTY_MOVE_TARGETS,\n onOpen,\n onStartTimer,\n onStopTimer,\n onAddTime,\n onMoveToColumn,\n}: KanbanCardProps): React.ReactElement {\n const t = useT()\n const coarsePointer = useCoarsePointer()\n const { attributes, listeners, setNodeRef, isDragging } = useDraggable({\n id: task.id,\n data: { type: 'task', taskStatusId: task.taskStatusId },\n })\n const dimmed = isActiveDrag || isDragging\n\n // dnd-kit's draggable listeners take pointer capture on the card, which rewrites the\n // subsequent click's target to the card itself and swallows the inner buttons. Cutting\n // the pointerdown at the action row keeps Start / Stop / Add time clickable.\n const stopPointerDown = React.useCallback((event: React.PointerEvent) => {\n event.stopPropagation()\n }, [])\n\n const handleCardClick = React.useCallback(\n (event: React.MouseEvent<HTMLDivElement>) => {\n const target = event.target as HTMLElement\n if (target.closest('[data-card-action=\"true\"]')) return\n onOpen(task.id)\n },\n [onOpen, task.id],\n )\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key !== 'Enter') return\n const target = event.target as HTMLElement\n if (target.closest('[data-card-action=\"true\"]')) return\n event.preventDefault()\n onOpen(task.id)\n },\n [onOpen, task.id],\n )\n\n // Keyboard operability of the board does NOT go through dnd-kit: its keyboard sensor\n // would have to own Enter/Space on the card, and Enter is what opens the drawer. The\n // move is an explicit menu instead \u2014 same `onMoveToColumn` the drop calls, so a keyboard\n // move is the identical optimistic, version-locked request a drag issues.\n const [moveMenuOpen, setMoveMenuOpen] = React.useState(false)\n const moveTriggerRef = React.useRef<HTMLButtonElement | null>(null)\n const moveMenuRef = React.useRef<HTMLDivElement | null>(null)\n const moveItemRefs = React.useRef<(HTMLButtonElement | null)[]>([])\n const showMoveMenu = moveTargets.length > 0 && typeof onMoveToColumn === 'function'\n\n React.useEffect(() => {\n if (!moveMenuOpen) return\n moveItemRefs.current[0]?.focus()\n }, [moveMenuOpen])\n\n React.useEffect(() => {\n if (!moveMenuOpen) return\n const handleOutside = (event: MouseEvent) => {\n const target = event.target as Node\n if (moveMenuRef.current?.contains(target)) return\n if (moveTriggerRef.current?.contains(target)) return\n setMoveMenuOpen(false)\n }\n document.addEventListener('mousedown', handleOutside)\n return () => document.removeEventListener('mousedown', handleOutside)\n }, [moveMenuOpen])\n\n const closeMoveMenu = React.useCallback(() => {\n setMoveMenuOpen(false)\n moveTriggerRef.current?.focus()\n }, [])\n\n const handleMoveMenuKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'Escape') {\n event.preventDefault()\n closeMoveMenu()\n return\n }\n if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return\n event.preventDefault()\n const items = moveItemRefs.current.filter(\n (item): item is HTMLButtonElement => item !== null && item.isConnected,\n )\n if (items.length === 0) return\n const current = items.indexOf(document.activeElement as HTMLButtonElement)\n const step = event.key === 'ArrowDown' ? 1 : -1\n const next = current < 0 ? 0 : (current + step + items.length) % items.length\n items[next]?.focus()\n },\n [closeMoveMenu],\n )\n\n const handleMoveSelect = React.useCallback(\n (targetStatusId: string) => {\n setMoveMenuOpen(false)\n moveTriggerRef.current?.focus()\n onMoveToColumn?.(task.id, targetStatusId)\n },\n [onMoveToColumn, task.id],\n )\n\n const quickActionsVisible = coarsePointer\n ? 'opacity-100'\n : 'opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100'\n\n const cardInjectionContext = React.useMemo(\n () => ({\n entityId: TASK_ENTITY_ID,\n recordId: task.id,\n taskId: task.id,\n timeProjectId: task.timeProjectId ?? null,\n taskStatusId: task.taskStatusId ?? null,\n timerRunning,\n }),\n [task.id, task.taskStatusId, task.timeProjectId, timerRunning],\n )\n\n const subtaskPercent = subtasks && subtasks.total > 0\n ? Math.round((subtasks.done / subtasks.total) * 100)\n : 0\n\n return (\n <div\n ref={setNodeRef}\n {...attributes}\n {...listeners}\n data-task-card={task.id}\n aria-label={t('staff.time_tracking.board.card.aria', 'Task: {title}', { title: task.title })}\n onClick={handleCardClick}\n onKeyDown={handleKeyDown}\n className={`group relative flex w-full flex-col gap-2 rounded-lg border border-border bg-card px-4 py-3 shadow-xs transition-shadow ${\n dimmed ? 'cursor-grabbing opacity-30' : 'cursor-grab hover:shadow-sm active:cursor-grabbing'\n } ${pending ? 'opacity-70' : ''}`}\n >\n <div className=\"text-sm font-semibold leading-normal text-foreground\">{task.title}</div>\n\n {task.supportReference ? (\n <div className=\"flex flex-wrap items-center gap-1\" data-testid=\"task-card-support\">\n <Badge variant=\"brand\" size=\"sm\">\n {t('staff.time_tracking.board.card.customerRequest', 'Customer request')}\n </Badge>\n <span className=\"text-xs font-medium text-muted-foreground\">{task.supportReference}</span>\n </div>\n ) : null}\n\n {tags.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {tags.map((tag) => (\n <Badge key={tag.id} variant=\"info\" size=\"sm\">\n {tag.label}\n </Badge>\n ))}\n </div>\n ) : null}\n\n <InjectionSpot\n spotId={extensionPoints.hosts.taskBoardCardBadges.spotId}\n context={cardInjectionContext}\n data={task}\n />\n\n {subtasks && subtasks.total > 0 ? (\n <div className=\"flex flex-col gap-1\">\n <span className=\"flex items-center gap-1 text-xs font-medium leading-normal text-muted-foreground\">\n <Check className=\"size-3\" aria-hidden=\"true\" />\n {t('staff.time_tracking.board.card.subtasks', '{done}/{total} subtasks', {\n done: subtasks.done,\n total: subtasks.total,\n })}\n </span>\n <Progress value={subtaskPercent} size=\"sm\" tone=\"success\" />\n </div>\n ) : null}\n\n <div className=\"h-px w-full bg-border\" aria-hidden=\"true\" />\n\n <div className=\"flex items-center gap-2\">\n <Avatar\n size=\"xs\"\n label={initialsFromName(assigneeName)}\n ariaLabel={\n assigneeName\n ? t('staff.time_tracking.board.card.assignee', 'Assigned to {name}', { name: assigneeName })\n : t('staff.time_tracking.board.card.unassigned', 'Unassigned')\n }\n />\n {timerRunning ? (\n <Badge variant=\"success\" size=\"sm\" dot>\n {t('staff.time_tracking.board.card.timerRunning', 'timer')}\n </Badge>\n ) : null}\n <span className=\"grow\" />\n <span className=\"text-xs font-semibold tabular-nums leading-normal text-foreground\">\n {formatBoardMinutes(task.loggedMinutes)}\n </span>\n </div>\n\n <div\n data-card-action=\"true\"\n data-testid={`kanban-card-actions-${task.id}`}\n onPointerDown={stopPointerDown}\n className={`flex flex-wrap items-center gap-1.5 ${moveMenuOpen ? 'opacity-100' : quickActionsVisible}`}\n >\n {timerRunning ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"2xs\"\n aria-label={t('staff.time_tracking.board.card.stopTimerAria', 'Stop the timer for {title}', {\n title: task.title,\n })}\n onClick={() => onStopTimer(task.id)}\n >\n <Square className=\"size-3\" aria-hidden=\"true\" />\n {t('staff.time_tracking.board.card.stopTimer', 'Stop')}\n </Button>\n ) : (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"2xs\"\n aria-label={t('staff.time_tracking.board.card.startTimerAria', 'Start a timer for {title}', {\n title: task.title,\n })}\n onClick={() => onStartTimer(task.id)}\n >\n <Play className=\"size-3\" aria-hidden=\"true\" />\n {t('staff.time_tracking.board.card.startTimer', 'Start')}\n </Button>\n )}\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"2xs\"\n aria-label={t('staff.time_tracking.board.card.addTimeAria', 'Add time to {title}', {\n title: task.title,\n })}\n onClick={() => onAddTime(task.id)}\n >\n <Plus className=\"size-3\" aria-hidden=\"true\" />\n {t('staff.time_tracking.board.card.addTime', 'Add time')}\n </Button>\n {showMoveMenu ? (\n <div className=\"relative\">\n <Button\n ref={moveTriggerRef}\n type=\"button\"\n variant=\"outline\"\n size=\"2xs\"\n aria-haspopup=\"menu\"\n aria-expanded={moveMenuOpen}\n aria-label={t('staff.time_tracking.board.card.moveTo.aria', 'Move {title} to another column', {\n title: task.title,\n })}\n data-testid={`kanban-card-move-${task.id}`}\n onClick={() => setMoveMenuOpen((open) => !open)}\n onKeyDown={(event) => {\n if (event.key !== 'ArrowDown') return\n event.preventDefault()\n setMoveMenuOpen(true)\n }}\n >\n <ArrowRightLeft className=\"size-3\" aria-hidden=\"true\" />\n {t('staff.time_tracking.board.card.moveTo.cta', 'Move')}\n </Button>\n {moveMenuOpen ? (\n <div\n ref={moveMenuRef}\n role=\"menu\"\n aria-label={t('staff.time_tracking.board.card.moveTo.menu', 'Move {title} to', {\n title: task.title,\n })}\n data-testid={`kanban-card-move-menu-${task.id}`}\n onKeyDown={handleMoveMenuKeyDown}\n className=\"absolute bottom-full left-0 z-dropdown mb-1 flex min-w-40 flex-col gap-0.5 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md\"\n >\n {moveTargets.map((target, index) => (\n <Button\n key={target.id}\n ref={(node: HTMLButtonElement | null) => {\n moveItemRefs.current[index] = node\n }}\n type=\"button\"\n variant=\"ghost\"\n size=\"2xs\"\n role=\"menuitem\"\n className=\"justify-start\"\n data-testid={`kanban-card-move-option-${task.id}-${target.id}`}\n onClick={() => handleMoveSelect(target.id)}\n >\n {target.name}\n </Button>\n ))}\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n\n <InjectionSpot\n spotId={extensionPoints.hosts.taskBoardCardFooter.spotId}\n context={cardInjectionContext}\n data={task}\n />\n </div>\n )\n}\n\nconst DefaultKanbanCard = React.memo(KanbanCardImpl)\n\nconst kanbanTagOptionSchema = z.object({ id: z.string(), label: z.string() })\n\nconst kanbanMoveTargetSchema = z.object({ id: z.string(), name: z.string() })\n\nconst kanbanCardPropsSchema: z.ZodType<KanbanCardProps> = z.object({\n task: opaqueProp<BoardTask>(),\n assigneeName: z.string().nullable(),\n tags: z.array(kanbanTagOptionSchema),\n subtasks: opaqueProp<SubtaskProgress>().nullable(),\n timerRunning: z.boolean(),\n pending: z.boolean(),\n isActiveDrag: z.boolean(),\n moveTargets: z.array(kanbanMoveTargetSchema).optional(),\n onOpen: callbackProp<(taskId: string) => void>(),\n onStartTimer: callbackProp<(taskId: string) => void>(),\n onStopTimer: callbackProp<(taskId: string) => void>(),\n onAddTime: callbackProp<(taskId: string) => void>(),\n onMoveToColumn: optionalCallbackProp<(taskId: string, targetStatusId: string) => void>(),\n})\n\nregisterComponent<KanbanCardProps>({\n id: extensionPoints.hosts.kanbanCardComponent.componentId,\n component: DefaultKanbanCard,\n metadata: {\n module: 'staff',\n description: 'One task card on the time-tracking Kanban board.',\n propsSchema: kanbanCardPropsSchema,\n },\n})\n\nfunction KanbanCardHost(props: KanbanCardProps): React.ReactElement {\n const Resolved = useRegisteredComponent<KanbanCardProps>(\n extensionPoints.hosts.kanbanCardComponent.componentId,\n DefaultKanbanCard,\n )\n return <Resolved {...props} />\n}\n\nexport const KanbanCard = React.memo(KanbanCardHost)\n\nexport default KanbanCard\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|