@open-mercato/core 0.10.1-develop.7717.1.2befe9c634 → 0.10.1-develop.7721.1.b5c4a00a68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/staff_support_routing_rule/index.js +21 -0
- package/dist/generated/entities/staff_support_routing_rule/index.js.map +7 -0
- package/dist/generated/entities/staff_support_ticket/index.js +37 -0
- package/dist/generated/entities/staff_support_ticket/index.js.map +7 -0
- package/dist/generated/entities/staff_time_project/index.js +2 -0
- package/dist/generated/entities/staff_time_project/index.js.map +2 -2
- package/dist/generated/entities/staff_time_project_member/index.js +2 -0
- package/dist/generated/entities/staff_time_project_member/index.js.map +2 -2
- package/dist/generated/entities/staff_time_task/index.js +4 -0
- package/dist/generated/entities/staff_time_task/index.js.map +2 -2
- package/dist/generated/entities/staff_time_task_comment/index.js +9 -1
- package/dist/generated/entities/staff_time_task_comment/index.js.map +2 -2
- package/dist/generated/entities.ids.generated.js +2 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +38 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/auth.js +1 -0
- package/dist/helpers/integration/auth.js.map +2 -2
- package/dist/helpers/integration/communicationChannelsFixtures.js +20 -0
- package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
- package/dist/modules/catalog/api/bulk-delete/route.js +12 -2
- package/dist/modules/catalog/api/bulk-delete/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/test-seed/route.js +17 -3
- package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
- package/dist/modules/communication_channels/lib/test-seed.js +38 -5
- package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
- package/dist/modules/messages/commands/messages.js +2 -3
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
- package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js +299 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js +91 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/route.js +146 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/options/route.js +60 -0
- package/dist/modules/staff/api/portal/support-tickets/options/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/route.js +220 -0
- package/dist/modules/staff/api/portal/support-tickets/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/settings/route.js +7 -1
- package/dist/modules/staff/api/timesheets/settings/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js +122 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js +180 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/route.js +147 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/support-tickets/options/route.js +133 -0
- package/dist/modules/staff/api/timesheets/support-tickets/options/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js +33 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js +25 -7
- package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js +32 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/route.js +94 -47
- package/dist/modules/staff/api/timesheets/tasks/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +3 -0
- package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/route.js +3 -0
- package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +1 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +54 -4
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js +21 -5
- package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/settings/page.js +319 -254
- package/dist/modules/staff/backend/staff/time-tracking/settings/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/support/page.js +363 -0
- package/dist/modules/staff/backend/staff/time-tracking/support/page.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js +18 -0
- package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js.map +7 -0
- package/dist/modules/staff/cli.js +27 -0
- package/dist/modules/staff/cli.js.map +2 -2
- package/dist/modules/staff/commands/index.js +3 -0
- package/dist/modules/staff/commands/index.js.map +2 -2
- package/dist/modules/staff/commands/shared.js +10 -1
- package/dist/modules/staff/commands/shared.js.map +2 -2
- package/dist/modules/staff/commands/support-routing-rules.js +505 -0
- package/dist/modules/staff/commands/support-routing-rules.js.map +7 -0
- package/dist/modules/staff/commands/support-tickets.js +513 -0
- package/dist/modules/staff/commands/support-tickets.js.map +7 -0
- package/dist/modules/staff/commands/timesheets-projects.js +72 -14
- package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-task-assignment.js +270 -0
- package/dist/modules/staff/commands/timesheets-task-assignment.js.map +7 -0
- package/dist/modules/staff/commands/timesheets-task-comments.js +58 -12
- package/dist/modules/staff/commands/timesheets-task-comments.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-tasks.js +91 -9
- package/dist/modules/staff/commands/timesheets-tasks.js.map +3 -3
- package/dist/modules/staff/data/enrichers.js +141 -0
- package/dist/modules/staff/data/enrichers.js.map +2 -2
- package/dist/modules/staff/data/entities.js +143 -2
- package/dist/modules/staff/data/entities.js.map +2 -2
- package/dist/modules/staff/data/extensions.js +8 -0
- package/dist/modules/staff/data/extensions.js.map +2 -2
- package/dist/modules/staff/data/validators.js +34 -5
- package/dist/modules/staff/data/validators.js.map +2 -2
- package/dist/modules/staff/encryption.js +6 -0
- package/dist/modules/staff/encryption.js.map +2 -2
- package/dist/modules/staff/events.js +25 -0
- package/dist/modules/staff/events.js.map +2 -2
- package/dist/modules/staff/events.payloads.js +54 -0
- package/dist/modules/staff/events.payloads.js.map +2 -2
- package/dist/modules/staff/extension-points.js +1 -0
- package/dist/modules/staff/extension-points.js.map +2 -2
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js +468 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js +12 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js +316 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js +12 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js +190 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js +19 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js.map +7 -0
- package/dist/modules/staff/index.js +1 -1
- package/dist/modules/staff/index.js.map +2 -2
- package/dist/modules/staff/lib/crud.js +2 -0
- package/dist/modules/staff/lib/crud.js.map +2 -2
- package/dist/modules/staff/lib/support/attachments.js +36 -0
- package/dist/modules/staff/lib/support/attachments.js.map +7 -0
- package/dist/modules/staff/lib/support/dictionaries.js +87 -0
- package/dist/modules/staff/lib/support/dictionaries.js.map +7 -0
- package/dist/modules/staff/lib/support/notifications.js +111 -0
- package/dist/modules/staff/lib/support/notifications.js.map +7 -0
- package/dist/modules/staff/lib/support/portalAttachments.js +100 -0
- package/dist/modules/staff/lib/support/portalAttachments.js.map +7 -0
- package/dist/modules/staff/lib/support/portalBroadcast.js +117 -0
- package/dist/modules/staff/lib/support/portalBroadcast.js.map +7 -0
- package/dist/modules/staff/lib/support/portalRoute.js +99 -0
- package/dist/modules/staff/lib/support/portalRoute.js.map +7 -0
- package/dist/modules/staff/lib/support/portalStatus.js +12 -0
- package/dist/modules/staff/lib/support/portalStatus.js.map +7 -0
- package/dist/modules/staff/lib/support/portalTickets.js +148 -0
- package/dist/modules/staff/lib/support/portalTickets.js.map +7 -0
- package/dist/modules/staff/lib/support/routing.js +81 -0
- package/dist/modules/staff/lib/support/routing.js.map +7 -0
- package/dist/modules/staff/lib/support/routingRuleDraft.js +25 -0
- package/dist/modules/staff/lib/support/routingRuleDraft.js.map +7 -0
- package/dist/modules/staff/lib/support/seedDefaults.js +114 -0
- package/dist/modules/staff/lib/support/seedDefaults.js.map +7 -0
- package/dist/modules/staff/lib/support/supportActivity.js +26 -0
- package/dist/modules/staff/lib/support/supportActivity.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js +23 -0
- package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js +112 -0
- package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/portalPendingUploads.js +45 -0
- package/dist/modules/staff/lib/support-ui/portalPendingUploads.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/portalSupport.js +51 -0
- package/dist/modules/staff/lib/support-ui/portalSupport.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/access.js +3 -39
- package/dist/modules/staff/lib/time-tracking/access.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/listProjectAccess.js +54 -0
- package/dist/modules/staff/lib/time-tracking/listProjectAccess.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/portalRecipients.js +92 -2
- package/dist/modules/staff/lib/time-tracking/portalRecipients.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/projectAssignment.js +112 -0
- package/dist/modules/staff/lib/time-tracking/projectAssignment.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/settingKeys.js +14 -0
- package/dist/modules/staff/lib/time-tracking/settingKeys.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/settings.js +28 -1
- package/dist/modules/staff/lib/time-tracking/settings.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js +143 -0
- package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/workRecipients.js +92 -0
- package/dist/modules/staff/lib/time-tracking/workRecipients.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js +32 -3
- package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js +29 -0
- package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js +4 -0
- package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js +141 -1
- package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js +153 -0
- package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js +67 -0
- package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js +3 -1
- package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js +80 -17
- package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js +230 -41
- package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js +21 -1
- package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js +16 -4
- package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js +3 -1
- package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js +68 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js +50 -0
- package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js +13 -1
- package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js +36 -2
- package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js.map +2 -2
- package/dist/modules/staff/migrations/Migration20261009180543_staff.js +42 -0
- package/dist/modules/staff/migrations/Migration20261009180543_staff.js.map +7 -0
- package/dist/modules/staff/notifications.js +92 -0
- package/dist/modules/staff/notifications.js.map +2 -2
- package/dist/modules/staff/setup.js +6 -2
- package/dist/modules/staff/setup.js.map +2 -2
- package/dist/modules/staff/subscribers/support-portal-comment-created.js +26 -0
- package/dist/modules/staff/subscribers/support-portal-comment-created.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-comment-deleted.js +26 -0
- package/dist/modules/staff/subscribers/support-portal-comment-deleted.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-comment-updated.js +26 -0
- package/dist/modules/staff/subscribers/support-portal-comment-updated.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-status-column-updated.js +24 -0
- package/dist/modules/staff/subscribers/support-portal-status-column-updated.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-task-status-changed.js +25 -0
- package/dist/modules/staff/subscribers/support-portal-task-status-changed.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-task-updated.js +25 -0
- package/dist/modules/staff/subscribers/support-portal-task-updated.js.map +7 -0
- package/dist/modules/staff/subscribers/support-ticket-created-notification.js +25 -0
- package/dist/modules/staff/subscribers/support-ticket-created-notification.js.map +7 -0
- package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js +25 -0
- package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js.map +7 -0
- package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js +26 -0
- package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js.map +7 -0
- package/dist/modules/staff/subscribers/time-task-assignee-notification.js +83 -0
- package/dist/modules/staff/subscribers/time-task-assignee-notification.js.map +7 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js +112 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js.map +7 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js +17 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js.map +7 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js +870 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js.map +7 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js +17 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js.map +7 -0
- package/dist/modules/staff/widgets/injection-table.js +16 -1
- package/dist/modules/staff/widgets/injection-table.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +4 -18
- package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLotDetailPage.js +4 -18
- package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +4 -18
- package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +2 -2
- package/dist/modules/wms/lib/inventoryCsvExport.js +19 -0
- package/dist/modules/wms/lib/inventoryCsvExport.js.map +7 -0
- package/generated/entities/staff_support_routing_rule/index.ts +9 -0
- package/generated/entities/staff_support_ticket/index.ts +17 -0
- package/generated/entities/staff_time_project/index.ts +1 -0
- package/generated/entities/staff_time_project_member/index.ts +1 -0
- package/generated/entities/staff_time_task/index.ts +2 -0
- package/generated/entities/staff_time_task_comment/index.ts +4 -0
- package/generated/entities.ids.generated.ts +2 -0
- package/generated/entity-fields-registry.ts +38 -0
- package/package.json +8 -8
- package/src/helpers/integration/auth.ts +1 -1
- package/src/helpers/integration/communicationChannelsFixtures.ts +60 -2
- package/src/modules/catalog/api/bulk-delete/route.ts +12 -2
- package/src/modules/communication_channels/api/post/test-seed/route.ts +29 -12
- package/src/modules/communication_channels/lib/test-seed.ts +49 -10
- package/src/modules/messages/commands/messages.ts +8 -10
- package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +2 -2
- package/src/modules/staff/AGENTS.md +22 -6
- package/src/modules/staff/api/portal/support-tickets/[id]/attachments/route.ts +327 -0
- package/src/modules/staff/api/portal/support-tickets/[id]/messages/route.ts +94 -0
- package/src/modules/staff/api/portal/support-tickets/[id]/route.ts +160 -0
- package/src/modules/staff/api/portal/support-tickets/options/route.ts +60 -0
- package/src/modules/staff/api/portal/support-tickets/route.ts +253 -0
- package/src/modules/staff/api/timesheets/settings/route.ts +7 -1
- package/src/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.ts +131 -0
- package/src/modules/staff/api/timesheets/support-routing-rules/preview/route.ts +248 -0
- package/src/modules/staff/api/timesheets/support-routing-rules/route.ts +150 -0
- package/src/modules/staff/api/timesheets/support-tickets/options/route.ts +181 -0
- package/src/modules/staff/api/timesheets/tasks/[id]/claim/route.ts +31 -0
- package/src/modules/staff/api/timesheets/tasks/[id]/comments/route.ts +25 -7
- package/src/modules/staff/api/timesheets/tasks/[id]/release/route.ts +30 -0
- package/src/modules/staff/api/timesheets/tasks/route.ts +119 -67
- package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +3 -0
- package/src/modules/staff/api/timesheets/time-projects/route.ts +3 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +1 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +67 -2
- package/src/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.ts +22 -3
- package/src/modules/staff/backend/staff/time-tracking/settings/page.tsx +405 -308
- package/src/modules/staff/backend/staff/time-tracking/support/page.meta.ts +14 -0
- package/src/modules/staff/backend/staff/time-tracking/support/page.tsx +428 -0
- package/src/modules/staff/cli.ts +28 -0
- package/src/modules/staff/commands/index.ts +3 -0
- package/src/modules/staff/commands/shared.ts +22 -1
- package/src/modules/staff/commands/support-routing-rules.ts +595 -0
- package/src/modules/staff/commands/support-tickets.ts +580 -0
- package/src/modules/staff/commands/timesheets-projects.ts +84 -13
- package/src/modules/staff/commands/timesheets-task-assignment.ts +319 -0
- package/src/modules/staff/commands/timesheets-task-comments.ts +70 -15
- package/src/modules/staff/commands/timesheets-tasks.ts +106 -15
- package/src/modules/staff/data/enrichers.ts +203 -0
- package/src/modules/staff/data/entities.ts +133 -2
- package/src/modules/staff/data/extensions.ts +9 -1
- package/src/modules/staff/data/validators.ts +36 -0
- package/src/modules/staff/encryption.ts +6 -0
- package/src/modules/staff/events.payloads.ts +87 -1
- package/src/modules/staff/events.ts +26 -0
- package/src/modules/staff/extension-points.ts +1 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.ts +10 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.tsx +553 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.ts +10 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.tsx +346 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/page.meta.ts +17 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/page.tsx +253 -0
- package/src/modules/staff/i18n/de.json +291 -0
- package/src/modules/staff/i18n/en.json +291 -0
- package/src/modules/staff/i18n/es.json +291 -0
- package/src/modules/staff/i18n/ko.json +291 -0
- package/src/modules/staff/i18n/pl.json +291 -0
- package/src/modules/staff/index.ts +1 -1
- package/src/modules/staff/lib/crud.ts +2 -1
- package/src/modules/staff/lib/support/attachments.ts +43 -0
- package/src/modules/staff/lib/support/dictionaries.ts +120 -0
- package/src/modules/staff/lib/support/notifications.ts +138 -0
- package/src/modules/staff/lib/support/portalAttachments.ts +140 -0
- package/src/modules/staff/lib/support/portalBroadcast.ts +161 -0
- package/src/modules/staff/lib/support/portalRoute.ts +108 -0
- package/src/modules/staff/lib/support/portalStatus.ts +15 -0
- package/src/modules/staff/lib/support/portalTickets.ts +233 -0
- package/src/modules/staff/lib/support/routing.ts +117 -0
- package/src/modules/staff/lib/support/routingRuleDraft.ts +45 -0
- package/src/modules/staff/lib/support/seedDefaults.ts +151 -0
- package/src/modules/staff/lib/support/supportActivity.ts +39 -0
- package/src/modules/staff/lib/support-ui/PortalSupportNotLinked.tsx +20 -0
- package/src/modules/staff/lib/support-ui/portalAttachmentValidation.ts +125 -0
- package/src/modules/staff/lib/support-ui/portalPendingUploads.ts +49 -0
- package/src/modules/staff/lib/support-ui/portalSupport.ts +116 -0
- package/src/modules/staff/lib/time-tracking/access.ts +2 -62
- package/src/modules/staff/lib/time-tracking/listProjectAccess.ts +78 -0
- package/src/modules/staff/lib/time-tracking/portalRecipients.ts +173 -5
- package/src/modules/staff/lib/time-tracking/projectAssignment.ts +172 -0
- package/src/modules/staff/lib/time-tracking/settingKeys.ts +11 -0
- package/src/modules/staff/lib/time-tracking/settings.ts +46 -1
- package/src/modules/staff/lib/time-tracking/taskAssignmentRoute.ts +165 -0
- package/src/modules/staff/lib/time-tracking/workRecipients.ts +183 -0
- package/src/modules/staff/lib/time-tracking-ui/BoardFilterChips.tsx +36 -1
- package/src/modules/staff/lib/time-tracking-ui/KanbanBoard.tsx +30 -0
- package/src/modules/staff/lib/time-tracking-ui/KanbanCard.tsx +9 -0
- package/src/modules/staff/lib/time-tracking-ui/ProjectFormSections.tsx +185 -6
- package/src/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.tsx +188 -0
- package/src/modules/staff/lib/time-tracking-ui/SupportRequestPanel.tsx +102 -0
- package/src/modules/staff/lib/time-tracking-ui/TaskBoardScreen.tsx +2 -0
- package/src/modules/staff/lib/time-tracking-ui/TaskCommentThread.tsx +105 -22
- package/src/modules/staff/lib/time-tracking-ui/TaskDrawer.tsx +278 -43
- package/src/modules/staff/lib/time-tracking-ui/boardDirectory.ts +21 -0
- package/src/modules/staff/lib/time-tracking-ui/boardFilters.ts +16 -2
- package/src/modules/staff/lib/time-tracking-ui/kanbanBoardData.ts +4 -0
- package/src/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.ts +117 -0
- package/src/modules/staff/lib/time-tracking-ui/settingsTabs.ts +17 -0
- package/src/modules/staff/lib/time-tracking-ui/supportTicketData.ts +71 -0
- package/src/modules/staff/lib/time-tracking-ui/taskDrawerData.ts +15 -0
- package/src/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.ts +48 -1
- package/src/modules/staff/migrations/.snapshot-open-mercato.json +714 -0
- package/src/modules/staff/migrations/Migration20261009180543_staff.ts +50 -0
- package/src/modules/staff/notifications.ts +92 -0
- package/src/modules/staff/setup.ts +6 -2
- package/src/modules/staff/subscribers/support-portal-comment-created.ts +35 -0
- package/src/modules/staff/subscribers/support-portal-comment-deleted.ts +35 -0
- package/src/modules/staff/subscribers/support-portal-comment-updated.ts +35 -0
- package/src/modules/staff/subscribers/support-portal-status-column-updated.ts +36 -0
- package/src/modules/staff/subscribers/support-portal-task-status-changed.ts +34 -0
- package/src/modules/staff/subscribers/support-portal-task-updated.ts +34 -0
- package/src/modules/staff/subscribers/support-ticket-created-notification.ts +27 -0
- package/src/modules/staff/subscribers/support-ticket-customer-replied-notification.ts +27 -0
- package/src/modules/staff/subscribers/support-ticket-unnotified-clear.ts +29 -0
- package/src/modules/staff/subscribers/time-task-assignee-notification.ts +102 -0
- package/src/modules/staff/widgets/injection/portal-support-tickets/widget.client.tsx +151 -0
- package/src/modules/staff/widgets/injection/portal-support-tickets/widget.ts +16 -0
- package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.tsx +1058 -0
- package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.ts +17 -0
- package/src/modules/staff/widgets/injection-table.ts +15 -0
- package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +4 -20
- package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +4 -20
- package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +4 -19
- package/src/modules/wms/lib/inventoryCsvExport.ts +18 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const EMPTY_BOARD_FILTERS = {
|
|
2
2
|
assigneeStaffMemberId: null,
|
|
3
3
|
taskStatusId: null,
|
|
4
|
-
tagIds: []
|
|
4
|
+
tagIds: [],
|
|
5
|
+
unassigned: false
|
|
5
6
|
};
|
|
6
7
|
function readOptionalId(value) {
|
|
7
8
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
@@ -14,15 +15,18 @@ function normalizeBoardFilters(raw) {
|
|
|
14
15
|
source.tagIds.map((entry) => readOptionalId(entry)).filter((entry) => entry !== null)
|
|
15
16
|
)
|
|
16
17
|
) : [];
|
|
18
|
+
const assigneeStaffMemberId = readOptionalId(source.assigneeStaffMemberId);
|
|
17
19
|
return {
|
|
18
|
-
assigneeStaffMemberId
|
|
20
|
+
assigneeStaffMemberId,
|
|
19
21
|
taskStatusId: readOptionalId(source.taskStatusId),
|
|
20
|
-
tagIds
|
|
22
|
+
tagIds,
|
|
23
|
+
unassigned: assigneeStaffMemberId === null && source.unassigned === true
|
|
21
24
|
};
|
|
22
25
|
}
|
|
23
26
|
function countActiveBoardFilters(filters) {
|
|
24
27
|
let count = 0;
|
|
25
28
|
if (filters.assigneeStaffMemberId) count += 1;
|
|
29
|
+
if (filters.unassigned) count += 1;
|
|
26
30
|
if (filters.taskStatusId) count += 1;
|
|
27
31
|
return count + filters.tagIds.length;
|
|
28
32
|
}
|
|
@@ -31,6 +35,7 @@ function hasActiveBoardFilters(filters) {
|
|
|
31
35
|
}
|
|
32
36
|
function boardServerFilterKey(filters, scope = {}) {
|
|
33
37
|
const parts = [`assignee:${filters.assigneeStaffMemberId ?? ""}`];
|
|
38
|
+
if (filters.unassigned) parts.push("unassigned");
|
|
34
39
|
if (scope.includeStatus) parts.push(`status:${filters.taskStatusId ?? ""}`);
|
|
35
40
|
parts.push(`tags:${[...filters.tagIds].sort((a, b) => a < b ? -1 : a > b ? 1 : 0).join(",")}`);
|
|
36
41
|
return parts.join("|");
|
|
@@ -39,6 +44,8 @@ function boardServerFilterParams(filters, scope = {}) {
|
|
|
39
44
|
let params = "";
|
|
40
45
|
if (filters.assigneeStaffMemberId) {
|
|
41
46
|
params += `&assigneeStaffMemberId=${encodeURIComponent(filters.assigneeStaffMemberId)}`;
|
|
47
|
+
} else if (filters.unassigned) {
|
|
48
|
+
params += "&unassigned=true";
|
|
42
49
|
}
|
|
43
50
|
if (scope.includeStatus && filters.taskStatusId) {
|
|
44
51
|
params += `&taskStatusId=${encodeURIComponent(filters.taskStatusId)}`;
|
|
@@ -53,7 +60,11 @@ function visibleBoardStatusIds(statuses, filters) {
|
|
|
53
60
|
return statuses.filter((status) => status.id === filters.taskStatusId);
|
|
54
61
|
}
|
|
55
62
|
function withAssigneeFilter(filters, staffMemberId) {
|
|
56
|
-
|
|
63
|
+
const assigneeStaffMemberId = readOptionalId(staffMemberId);
|
|
64
|
+
return { ...filters, assigneeStaffMemberId, unassigned: assigneeStaffMemberId ? false : filters.unassigned };
|
|
65
|
+
}
|
|
66
|
+
function withUnassignedFilter(filters, unassigned) {
|
|
67
|
+
return { ...filters, unassigned, assigneeStaffMemberId: unassigned ? null : filters.assigneeStaffMemberId };
|
|
57
68
|
}
|
|
58
69
|
function withStatusFilter(filters, taskStatusId) {
|
|
59
70
|
return { ...filters, taskStatusId: readOptionalId(taskStatusId) };
|
|
@@ -77,6 +88,7 @@ export {
|
|
|
77
88
|
withAssigneeFilter,
|
|
78
89
|
withStatusFilter,
|
|
79
90
|
withTagFilter,
|
|
91
|
+
withUnassignedFilter,
|
|
80
92
|
withoutTagFilter
|
|
81
93
|
};
|
|
82
94
|
//# sourceMappingURL=boardFilters.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/staff/lib/time-tracking-ui/boardFilters.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Board filter state (T3.8) \u2014 the `dt-chips` row of screen 6.\n *\n * Three filters, and they are deliberately not all the same kind of filter:\n *\n * - **assignee** narrows on the server. `/api/staff/timesheets/tasks` accepts\n * `assigneeStaffMemberId`, so the filter goes into the request and the column\n * totals stay honest.\n * - **status** does not narrow a request at all \u2014 it picks which columns render.\n * The board already asks one request per column keyed by `taskStatusId`, so\n * filtering by status is \"render this column only\", not \"add a parameter\".\n * The flat list view has no columns, so there it *is* a request parameter.\n * - **tag** narrows on the server too, since T7.6 gave `/api/staff/timesheets/tasks`\n * a `tagIds` parameter (W9). It used to filter loaded rows in the browser, which\n * made a match on page two invisible and the column badge count depend on how far\n * the board had been paged. The parameter means **every** listed tag, which is\n * what the chips have always meant.\n *\n * The split matters for caching: only the server-narrowed part may appear in a\n * react-query key, otherwise flipping a chip would evict and refetch pages whose\n * bytes did not change. `boardServerFilterKey` is that part.\n */\n\nexport type BoardFilters = {\n assigneeStaffMemberId: string | null\n taskStatusId: string | null\n tagIds: string[]\n}\n\nexport const EMPTY_BOARD_FILTERS: BoardFilters = {\n assigneeStaffMemberId: null,\n taskStatusId: null,\n tagIds: [],\n}\n\nfunction readOptionalId(value: unknown): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null\n}\n\n/** Parses whatever came out of storage, so a hand-edited or stale entry cannot wedge the board. */\nexport function normalizeBoardFilters(raw: unknown): BoardFilters {\n if (!raw || typeof raw !== 'object') return { ...EMPTY_BOARD_FILTERS }\n const source = raw as Record<string, unknown>\n const tagIds = Array.isArray(source.tagIds)\n ? Array.from(\n new Set(\n source.tagIds\n .map((entry) => readOptionalId(entry))\n .filter((entry): entry is string => entry !== null),\n ),\n )\n : []\n
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\n * Board filter state (T3.8) \u2014 the `dt-chips` row of screen 6.\n *\n * Three filters, and they are deliberately not all the same kind of filter:\n *\n * - **assignee** narrows on the server. `/api/staff/timesheets/tasks` accepts\n * `assigneeStaffMemberId`, so the filter goes into the request and the column\n * totals stay honest.\n * - **status** does not narrow a request at all \u2014 it picks which columns render.\n * The board already asks one request per column keyed by `taskStatusId`, so\n * filtering by status is \"render this column only\", not \"add a parameter\".\n * The flat list view has no columns, so there it *is* a request parameter.\n * - **tag** narrows on the server too, since T7.6 gave `/api/staff/timesheets/tasks`\n * a `tagIds` parameter (W9). It used to filter loaded rows in the browser, which\n * made a match on page two invisible and the column badge count depend on how far\n * the board had been paged. The parameter means **every** listed tag, which is\n * what the chips have always meant.\n *\n * The split matters for caching: only the server-narrowed part may appear in a\n * react-query key, otherwise flipping a chip would evict and refetch pages whose\n * bytes did not change. `boardServerFilterKey` is that part.\n */\n\nexport type BoardFilters = {\n assigneeStaffMemberId: string | null\n taskStatusId: string | null\n tagIds: string[]\n /** Only tasks with no assignee \u2014 the claimable work. Mutually exclusive with the assignee filter. */\n unassigned: boolean\n}\n\nexport const EMPTY_BOARD_FILTERS: BoardFilters = {\n assigneeStaffMemberId: null,\n taskStatusId: null,\n tagIds: [],\n unassigned: false,\n}\n\nfunction readOptionalId(value: unknown): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null\n}\n\n/** Parses whatever came out of storage, so a hand-edited or stale entry cannot wedge the board. */\nexport function normalizeBoardFilters(raw: unknown): BoardFilters {\n if (!raw || typeof raw !== 'object') return { ...EMPTY_BOARD_FILTERS }\n const source = raw as Record<string, unknown>\n const tagIds = Array.isArray(source.tagIds)\n ? Array.from(\n new Set(\n source.tagIds\n .map((entry) => readOptionalId(entry))\n .filter((entry): entry is string => entry !== null),\n ),\n )\n : []\n const assigneeStaffMemberId = readOptionalId(source.assigneeStaffMemberId)\n return {\n assigneeStaffMemberId,\n taskStatusId: readOptionalId(source.taskStatusId),\n tagIds,\n unassigned: assigneeStaffMemberId === null && source.unassigned === true,\n }\n}\n\nexport function countActiveBoardFilters(filters: BoardFilters): number {\n let count = 0\n if (filters.assigneeStaffMemberId) count += 1\n if (filters.unassigned) count += 1\n if (filters.taskStatusId) count += 1\n return count + filters.tagIds.length\n}\n\nexport function hasActiveBoardFilters(filters: BoardFilters): boolean {\n return countActiveBoardFilters(filters) > 0\n}\n\nexport type BoardFilterScope = {\n /** The flat list view sends the status as a request parameter; the board picks columns instead. */\n includeStatus?: boolean\n}\n\n/**\n * The part of the filter state a request actually depends on \u2014 and therefore the only\n * part allowed into a react-query key. Tags are ordered so that picking the same two\n * chips in either order shares one cache entry.\n */\nexport function boardServerFilterKey(filters: BoardFilters, scope: BoardFilterScope = {}): string {\n const parts = [`assignee:${filters.assigneeStaffMemberId ?? ''}`]\n if (filters.unassigned) parts.push('unassigned')\n if (scope.includeStatus) parts.push(`status:${filters.taskStatusId ?? ''}`)\n parts.push(`tags:${[...filters.tagIds].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join(',')}`)\n return parts.join('|')\n}\n\n/** URL fragment appended to a tasks list request; always starts with `&` or is empty. */\nexport function boardServerFilterParams(filters: BoardFilters, scope: BoardFilterScope = {}): string {\n let params = ''\n if (filters.assigneeStaffMemberId) {\n params += `&assigneeStaffMemberId=${encodeURIComponent(filters.assigneeStaffMemberId)}`\n } else if (filters.unassigned) {\n params += '&unassigned=true'\n }\n if (scope.includeStatus && filters.taskStatusId) {\n params += `&taskStatusId=${encodeURIComponent(filters.taskStatusId)}`\n }\n if (filters.tagIds.length > 0) {\n params += `&tagIds=${encodeURIComponent([...filters.tagIds].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join(','))}`\n }\n return params\n}\n\n/** Which columns the board renders under the current status filter. */\nexport function visibleBoardStatusIds<T extends { id: string }>(\n statuses: readonly T[],\n filters: BoardFilters,\n): T[] {\n if (!filters.taskStatusId) return [...statuses]\n return statuses.filter((status) => status.id === filters.taskStatusId)\n}\n\nexport function withAssigneeFilter(filters: BoardFilters, staffMemberId: string | null): BoardFilters {\n const assigneeStaffMemberId = readOptionalId(staffMemberId)\n return { ...filters, assigneeStaffMemberId, unassigned: assigneeStaffMemberId ? false : filters.unassigned }\n}\n\nexport function withUnassignedFilter(filters: BoardFilters, unassigned: boolean): BoardFilters {\n return { ...filters, unassigned, assigneeStaffMemberId: unassigned ? null : filters.assigneeStaffMemberId }\n}\n\nexport function withStatusFilter(filters: BoardFilters, taskStatusId: string | null): BoardFilters {\n return { ...filters, taskStatusId: readOptionalId(taskStatusId) }\n}\n\nexport function withTagFilter(filters: BoardFilters, tagId: string): BoardFilters {\n if (filters.tagIds.includes(tagId)) return filters\n return { ...filters, tagIds: [...filters.tagIds, tagId] }\n}\n\nexport function withoutTagFilter(filters: BoardFilters, tagId: string): BoardFilters {\n if (!filters.tagIds.includes(tagId)) return filters\n return { ...filters, tagIds: filters.tagIds.filter((entry) => entry !== tagId) }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,7 @@ import { formatDuration } from "../time-tracking/duration.js";
|
|
|
2
2
|
import { sumTaskLoggedMinutes } from "../timesheets-tasks/taskHoursTotals.js";
|
|
3
3
|
import { STATUS_POSITION_GAP } from "../timesheets-tasks/statusPositions.js";
|
|
4
4
|
import { resolveProjectColorHex } from "../timesheets-ui/colors.js";
|
|
5
|
+
import { readSupportTicket } from "./supportTicketData.js";
|
|
5
6
|
function readString(row, ...keys) {
|
|
6
7
|
for (const key of keys) {
|
|
7
8
|
const value = row[key];
|
|
@@ -68,7 +69,8 @@ function toBoardTask(row) {
|
|
|
68
69
|
childCount: readNumber(row, "childCount", "child_count") ?? 0,
|
|
69
70
|
closedAt: readString(row, "closed_at", "closedAt"),
|
|
70
71
|
updatedAt: readString(row, "updated_at", "updatedAt"),
|
|
71
|
-
tagIds: readStringArray(row, "tagIds", "tag_ids")
|
|
72
|
+
tagIds: readStringArray(row, "tagIds", "tag_ids"),
|
|
73
|
+
supportReference: readSupportTicket(row)?.supportReference ?? null
|
|
72
74
|
};
|
|
73
75
|
}
|
|
74
76
|
function readItems(payload) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/staff/lib/time-tracking-ui/kanbanBoardData.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Row parsing and board arithmetic for the Kanban board of screen 6 (T3.6).\n *\n * Kept apart from `KanbanBoard.tsx` so the parts that decide what a card says \u2014\n * which rows become cards, what a column's hours are, where a dropped card lands \u2014\n * are unit-testable without a DOM, and so the board component stays about drag,\n * optimistic writes and rollback.\n *\n * The column total deliberately delegates to `sumTaskLoggedMinutes` rather than\n * folding `loggedMinutes` inline: that helper drops any row whose parent is present\n * in the same set, which is the only guard against risk R10 (a parent's rollup\n * already contains its children's minutes, so adding both double-counts).\n */\n\nimport { formatDuration } from '../time-tracking/duration'\nimport { sumTaskLoggedMinutes } from '../timesheets-tasks/taskHoursTotals'\nimport { STATUS_POSITION_GAP } from '../timesheets-tasks/statusPositions'\nimport { resolveProjectColorHex } from '../timesheets-ui/colors'\n\nexport type BoardApiRow = Record<string, unknown>\n\nexport type BoardStatus = {\n id: string\n name: string\n slug: string | null\n color: string | null\n position: number\n isDefault: boolean\n isDone: boolean\n}\n\nexport type BoardTask = {\n id: string\n title: string\n reference: string | null\n timeProjectId: string | null\n parentTaskId: string | null\n taskStatusId: string | null\n assigneeStaffMemberId: string | null\n position: number\n ownMinutes: number\n loggedMinutes: number\n childCount: number\n closedAt: string | null\n updatedAt: string | null\n tagIds: string[]\n}\n\nexport type SubtaskProgress = {\n total: number\n done: number\n}\n\nexport function readString(row: BoardApiRow, ...keys: string[]): string | null {\n for (const key of keys) {\n const value = row[key]\n if (typeof value === 'string' && value.length > 0) return value\n }\n return null\n}\n\nexport function readNumber(row: BoardApiRow, ...keys: string[]): number | null {\n for (const key of keys) {\n const value = row[key]\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string' && value.trim().length > 0) {\n const parsed = Number(value)\n if (Number.isFinite(parsed)) return parsed\n }\n }\n return null\n}\n\nexport function readBoolean(row: BoardApiRow, ...keys: string[]): boolean {\n for (const key of keys) {\n const value = row[key]\n if (typeof value === 'boolean') return value\n if (value === 'true') return true\n if (value === 'false') return false\n }\n return false\n}\n\nfunction readStringArray(row: BoardApiRow, ...keys: string[]): string[] {\n for (const key of keys) {\n const value = row[key]\n if (!Array.isArray(value)) continue\n const ids = value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)\n if (ids.length > 0) return ids\n }\n return []\n}\n\nexport function toBoardStatus(row: BoardApiRow): BoardStatus | null {\n const id = readString(row, 'id')\n if (!id) return null\n return {\n id,\n name: readString(row, 'name') ?? id,\n slug: readString(row, 'slug'),\n color: readString(row, 'color'),\n position: readNumber(row, 'position') ?? 0,\n isDefault: readBoolean(row, 'is_default', 'isDefault'),\n isDone: readBoolean(row, 'is_done', 'isDone'),\n }\n}\n\nexport function toBoardTask(row: BoardApiRow): BoardTask | null {\n const id = readString(row, 'id')\n if (!id) return null\n return {\n id,\n title: readString(row, 'title') ?? id,\n reference: readString(row, 'reference'),\n timeProjectId: readString(row, 'time_project_id', 'timeProjectId'),\n parentTaskId: readString(row, 'parent_task_id', 'parentTaskId'),\n taskStatusId: readString(row, 'task_status_id', 'taskStatusId'),\n assigneeStaffMemberId: readString(row, 'assignee_staff_member_id', 'assigneeStaffMemberId'),\n position: readNumber(row, 'position') ?? 0,\n ownMinutes: readNumber(row, 'ownMinutes', 'own_minutes') ?? 0,\n loggedMinutes: readNumber(row, 'loggedMinutes', 'logged_minutes') ?? 0,\n childCount: readNumber(row, 'childCount', 'child_count') ?? 0,\n closedAt: readString(row, 'closed_at', 'closedAt'),\n updatedAt: readString(row, 'updated_at', 'updatedAt'),\n tagIds: readStringArray(row, 'tagIds', 'tag_ids'),\n }\n}\n\nexport function readItems(payload: unknown): BoardApiRow[] {\n if (!payload || typeof payload !== 'object') return []\n const items = (payload as { items?: unknown }).items\n if (!Array.isArray(items)) return []\n return items.filter((item): item is BoardApiRow => !!item && typeof item === 'object')\n}\n\nexport function readTotal(payload: unknown, fallback: number): number {\n if (!payload || typeof payload !== 'object') return fallback\n const total = (payload as { total?: unknown }).total\n return typeof total === 'number' && Number.isFinite(total) ? total : fallback\n}\n\n/** `22:45` \u2014 the card and column-header hours format of the mockup. */\nexport function formatBoardMinutes(minutes: number): string {\n return formatDuration(Number.isFinite(minutes) ? minutes : 0, 'clock')\n}\n\n/**\n * A column's summed hours (mockup note 4). Never fold `loggedMinutes` by hand \u2014\n * see the module docblock and `taskHoursTotals.ts` on risk R10.\n */\nexport function columnLoggedMinutes(tasks: readonly BoardTask[]): number {\n return sumTaskLoggedMinutes(\n tasks.map((task) => ({\n id: task.id,\n parentTaskId: task.parentTaskId,\n loggedMinutes: task.loggedMinutes,\n })),\n )\n}\n\n/**\n * Subtask progress per parent (`3/5 podzada\u0144`).\n *\n * The board only ever renders top-level cards, so children arrive from a separate\n * sweep. A child counts as done when it sits in a column flagged `is_done`, with\n * `closed_at` as the fallback for a child whose status is no longer on the board.\n */\nexport function computeSubtaskProgress(\n children: readonly BoardTask[],\n doneStatusIds: ReadonlySet<string>,\n): Map<string, SubtaskProgress> {\n const progress = new Map<string, SubtaskProgress>()\n for (const child of children) {\n if (!child.parentTaskId) continue\n const entry = progress.get(child.parentTaskId) ?? { total: 0, done: 0 }\n entry.total += 1\n const isDone = (child.taskStatusId && doneStatusIds.has(child.taskStatusId)) || !!child.closedAt\n if (isDone) entry.done += 1\n progress.set(child.parentTaskId, entry)\n }\n return progress\n}\n\n/**\n * The slot a card dropped on a column takes.\n *\n * Cards land at the top of the target column, matching where the optimistic update\n * puts them, so the server order and the rendered order agree without a refetch.\n * Positions stay on the gap grid the server seeds, so a drop writes one row.\n */\nexport function nextPositionAtColumnTop(tasks: readonly BoardTask[]): number {\n let lowest: number | null = null\n for (const task of tasks) {\n if (!Number.isFinite(task.position)) continue\n if (lowest === null || task.position < lowest) lowest = task.position\n }\n if (lowest === null) return STATUS_POSITION_GAP\n const candidate = Math.floor(lowest / 2)\n return candidate > 0 ? candidate : 1\n}\n\n/** Two-letter avatar initials, matching the mockup's `AN` / `PZ` chips. */\nexport function initialsFromName(name: string | null | undefined): string {\n const safe = (name ?? '').trim()\n if (!safe) return '?'\n const parts = safe.split(/\\s+/).filter((part) => part.length > 0)\n if (parts.length === 0) return '?'\n if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()\n return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase()\n}\n\n/**\n * The column accent bar colour.\n *\n * Statuses store a `PROJECT_COLOR_KEYS` token, never raw CSS, so the board resolves\n * the key through the shared palette and paints the 6px bar with an inline\n * background. No status-colour Tailwind class is involved.\n */\nexport function statusAccentColor(status: BoardStatus): string {\n return resolveProjectColorHex(status.color, status.name)\n}\n\nexport function sortByPosition<T extends { position: number; id: string }>(rows: readonly T[]): T[] {\n return [...rows].sort((left, right) => {\n if (left.position !== right.position) return left.position - right.position\n return left.id < right.id ? -1 : left.id > right.id ? 1 : 0\n })\n}\n"],
|
|
5
|
-
"mappings": "AAcA,SAAS,sBAAsB;AAC/B,SAAS,4BAA4B;AACrC,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;
|
|
4
|
+
"sourcesContent": ["/**\n * Row parsing and board arithmetic for the Kanban board of screen 6 (T3.6).\n *\n * Kept apart from `KanbanBoard.tsx` so the parts that decide what a card says \u2014\n * which rows become cards, what a column's hours are, where a dropped card lands \u2014\n * are unit-testable without a DOM, and so the board component stays about drag,\n * optimistic writes and rollback.\n *\n * The column total deliberately delegates to `sumTaskLoggedMinutes` rather than\n * folding `loggedMinutes` inline: that helper drops any row whose parent is present\n * in the same set, which is the only guard against risk R10 (a parent's rollup\n * already contains its children's minutes, so adding both double-counts).\n */\n\nimport { formatDuration } from '../time-tracking/duration'\nimport { sumTaskLoggedMinutes } from '../timesheets-tasks/taskHoursTotals'\nimport { STATUS_POSITION_GAP } from '../timesheets-tasks/statusPositions'\nimport { resolveProjectColorHex } from '../timesheets-ui/colors'\nimport { readSupportTicket } from './supportTicketData'\n\nexport type BoardApiRow = Record<string, unknown>\n\nexport type BoardStatus = {\n id: string\n name: string\n slug: string | null\n color: string | null\n position: number\n isDefault: boolean\n isDone: boolean\n}\n\nexport type BoardTask = {\n id: string\n title: string\n reference: string | null\n timeProjectId: string | null\n parentTaskId: string | null\n taskStatusId: string | null\n assigneeStaffMemberId: string | null\n position: number\n ownMinutes: number\n loggedMinutes: number\n childCount: number\n closedAt: string | null\n updatedAt: string | null\n tagIds: string[]\n /** Set when the task is a customer portal support ticket. */\n supportReference?: string | null\n}\n\nexport type SubtaskProgress = {\n total: number\n done: number\n}\n\nexport function readString(row: BoardApiRow, ...keys: string[]): string | null {\n for (const key of keys) {\n const value = row[key]\n if (typeof value === 'string' && value.length > 0) return value\n }\n return null\n}\n\nexport function readNumber(row: BoardApiRow, ...keys: string[]): number | null {\n for (const key of keys) {\n const value = row[key]\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string' && value.trim().length > 0) {\n const parsed = Number(value)\n if (Number.isFinite(parsed)) return parsed\n }\n }\n return null\n}\n\nexport function readBoolean(row: BoardApiRow, ...keys: string[]): boolean {\n for (const key of keys) {\n const value = row[key]\n if (typeof value === 'boolean') return value\n if (value === 'true') return true\n if (value === 'false') return false\n }\n return false\n}\n\nfunction readStringArray(row: BoardApiRow, ...keys: string[]): string[] {\n for (const key of keys) {\n const value = row[key]\n if (!Array.isArray(value)) continue\n const ids = value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)\n if (ids.length > 0) return ids\n }\n return []\n}\n\nexport function toBoardStatus(row: BoardApiRow): BoardStatus | null {\n const id = readString(row, 'id')\n if (!id) return null\n return {\n id,\n name: readString(row, 'name') ?? id,\n slug: readString(row, 'slug'),\n color: readString(row, 'color'),\n position: readNumber(row, 'position') ?? 0,\n isDefault: readBoolean(row, 'is_default', 'isDefault'),\n isDone: readBoolean(row, 'is_done', 'isDone'),\n }\n}\n\nexport function toBoardTask(row: BoardApiRow): BoardTask | null {\n const id = readString(row, 'id')\n if (!id) return null\n return {\n id,\n title: readString(row, 'title') ?? id,\n reference: readString(row, 'reference'),\n timeProjectId: readString(row, 'time_project_id', 'timeProjectId'),\n parentTaskId: readString(row, 'parent_task_id', 'parentTaskId'),\n taskStatusId: readString(row, 'task_status_id', 'taskStatusId'),\n assigneeStaffMemberId: readString(row, 'assignee_staff_member_id', 'assigneeStaffMemberId'),\n position: readNumber(row, 'position') ?? 0,\n ownMinutes: readNumber(row, 'ownMinutes', 'own_minutes') ?? 0,\n loggedMinutes: readNumber(row, 'loggedMinutes', 'logged_minutes') ?? 0,\n childCount: readNumber(row, 'childCount', 'child_count') ?? 0,\n closedAt: readString(row, 'closed_at', 'closedAt'),\n updatedAt: readString(row, 'updated_at', 'updatedAt'),\n tagIds: readStringArray(row, 'tagIds', 'tag_ids'),\n supportReference: readSupportTicket(row)?.supportReference ?? null,\n }\n}\n\nexport function readItems(payload: unknown): BoardApiRow[] {\n if (!payload || typeof payload !== 'object') return []\n const items = (payload as { items?: unknown }).items\n if (!Array.isArray(items)) return []\n return items.filter((item): item is BoardApiRow => !!item && typeof item === 'object')\n}\n\nexport function readTotal(payload: unknown, fallback: number): number {\n if (!payload || typeof payload !== 'object') return fallback\n const total = (payload as { total?: unknown }).total\n return typeof total === 'number' && Number.isFinite(total) ? total : fallback\n}\n\n/** `22:45` \u2014 the card and column-header hours format of the mockup. */\nexport function formatBoardMinutes(minutes: number): string {\n return formatDuration(Number.isFinite(minutes) ? minutes : 0, 'clock')\n}\n\n/**\n * A column's summed hours (mockup note 4). Never fold `loggedMinutes` by hand \u2014\n * see the module docblock and `taskHoursTotals.ts` on risk R10.\n */\nexport function columnLoggedMinutes(tasks: readonly BoardTask[]): number {\n return sumTaskLoggedMinutes(\n tasks.map((task) => ({\n id: task.id,\n parentTaskId: task.parentTaskId,\n loggedMinutes: task.loggedMinutes,\n })),\n )\n}\n\n/**\n * Subtask progress per parent (`3/5 podzada\u0144`).\n *\n * The board only ever renders top-level cards, so children arrive from a separate\n * sweep. A child counts as done when it sits in a column flagged `is_done`, with\n * `closed_at` as the fallback for a child whose status is no longer on the board.\n */\nexport function computeSubtaskProgress(\n children: readonly BoardTask[],\n doneStatusIds: ReadonlySet<string>,\n): Map<string, SubtaskProgress> {\n const progress = new Map<string, SubtaskProgress>()\n for (const child of children) {\n if (!child.parentTaskId) continue\n const entry = progress.get(child.parentTaskId) ?? { total: 0, done: 0 }\n entry.total += 1\n const isDone = (child.taskStatusId && doneStatusIds.has(child.taskStatusId)) || !!child.closedAt\n if (isDone) entry.done += 1\n progress.set(child.parentTaskId, entry)\n }\n return progress\n}\n\n/**\n * The slot a card dropped on a column takes.\n *\n * Cards land at the top of the target column, matching where the optimistic update\n * puts them, so the server order and the rendered order agree without a refetch.\n * Positions stay on the gap grid the server seeds, so a drop writes one row.\n */\nexport function nextPositionAtColumnTop(tasks: readonly BoardTask[]): number {\n let lowest: number | null = null\n for (const task of tasks) {\n if (!Number.isFinite(task.position)) continue\n if (lowest === null || task.position < lowest) lowest = task.position\n }\n if (lowest === null) return STATUS_POSITION_GAP\n const candidate = Math.floor(lowest / 2)\n return candidate > 0 ? candidate : 1\n}\n\n/** Two-letter avatar initials, matching the mockup's `AN` / `PZ` chips. */\nexport function initialsFromName(name: string | null | undefined): string {\n const safe = (name ?? '').trim()\n if (!safe) return '?'\n const parts = safe.split(/\\s+/).filter((part) => part.length > 0)\n if (parts.length === 0) return '?'\n if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()\n return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase()\n}\n\n/**\n * The column accent bar colour.\n *\n * Statuses store a `PROJECT_COLOR_KEYS` token, never raw CSS, so the board resolves\n * the key through the shared palette and paints the 6px bar with an inline\n * background. No status-colour Tailwind class is involved.\n */\nexport function statusAccentColor(status: BoardStatus): string {\n return resolveProjectColorHex(status.color, status.name)\n}\n\nexport function sortByPosition<T extends { position: number; id: string }>(rows: readonly T[]): T[] {\n return [...rows].sort((left, right) => {\n if (left.position !== right.position) return left.position - right.position\n return left.id < right.id ? -1 : left.id > right.id ? 1 : 0\n })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { resolveAssignmentState } from "./projectTeamAssignment.js";
|
|
2
|
+
const DEFAULT_ASSIGNEE_PAGE_SIZE = 100;
|
|
3
|
+
const DEFAULT_ASSIGNEE_MAX_PAGES = 20;
|
|
4
|
+
function readString(value) {
|
|
5
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
6
|
+
}
|
|
7
|
+
function readMembershipStaffMemberId(row) {
|
|
8
|
+
return readString(row.staffMemberId) ?? readString(row.staff_member_id);
|
|
9
|
+
}
|
|
10
|
+
function isUsableDefaultAssigneeMembership(row, graceDays, now = /* @__PURE__ */ new Date()) {
|
|
11
|
+
if (readString(row.status) !== "active") return false;
|
|
12
|
+
const startDate = readString(row.assignedStartDate) ?? readString(row.assigned_start_date);
|
|
13
|
+
const endDate = readString(row.assignedEndDate) ?? readString(row.assigned_end_date);
|
|
14
|
+
return resolveAssignmentState({ startDate, endDate }, graceDays, now) === "active";
|
|
15
|
+
}
|
|
16
|
+
function readProjectMembershipPage(response) {
|
|
17
|
+
if (!response.ok) throw new Error("[internal] project membership page request failed");
|
|
18
|
+
const items = response.result?.items;
|
|
19
|
+
if (!Array.isArray(items)) throw new Error("[internal] project membership page has no rows");
|
|
20
|
+
const totalPages = typeof response.result?.totalPages === "number" ? response.result.totalPages : null;
|
|
21
|
+
const rows = items.filter((item) => typeof item === "object" && item !== null);
|
|
22
|
+
return { items: rows, totalPages };
|
|
23
|
+
}
|
|
24
|
+
async function collectProjectMemberships(fetchPage, options = {}) {
|
|
25
|
+
const pageSize = options.pageSize ?? DEFAULT_ASSIGNEE_PAGE_SIZE;
|
|
26
|
+
const maxPages = options.maxPages ?? DEFAULT_ASSIGNEE_MAX_PAGES;
|
|
27
|
+
const rows = [];
|
|
28
|
+
for (let page = 1; page <= maxPages; page += 1) {
|
|
29
|
+
const result = await fetchPage(page);
|
|
30
|
+
rows.push(...result.items);
|
|
31
|
+
const lastPage = result.totalPages !== null ? page >= result.totalPages : result.items.length < pageSize;
|
|
32
|
+
if (lastPage || result.items.length === 0) return { rows, truncated: false };
|
|
33
|
+
}
|
|
34
|
+
return { rows, truncated: true };
|
|
35
|
+
}
|
|
36
|
+
function classifyDefaultAssigneeCandidates(rows, graceDays, now = /* @__PURE__ */ new Date()) {
|
|
37
|
+
const byMember = /* @__PURE__ */ new Map();
|
|
38
|
+
for (const row of rows) {
|
|
39
|
+
const staffMemberId = readMembershipStaffMemberId(row);
|
|
40
|
+
if (!staffMemberId) continue;
|
|
41
|
+
const active = isUsableDefaultAssigneeMembership(row, graceDays, now);
|
|
42
|
+
byMember.set(staffMemberId, (byMember.get(staffMemberId) ?? false) || active);
|
|
43
|
+
}
|
|
44
|
+
return Array.from(byMember, ([staffMemberId, active]) => ({ staffMemberId, active }));
|
|
45
|
+
}
|
|
46
|
+
function chunkIds(ids, size = DEFAULT_ASSIGNEE_PAGE_SIZE) {
|
|
47
|
+
const batches = [];
|
|
48
|
+
for (let index = 0; index < ids.length; index += size) batches.push(ids.slice(index, index + size));
|
|
49
|
+
return batches;
|
|
50
|
+
}
|
|
51
|
+
function resolveSelectedDefaultAssignee(candidates, value, truncated) {
|
|
52
|
+
if (!value) return { candidate: null, unusable: false };
|
|
53
|
+
const candidate = candidates.find((entry) => entry.staffMemberId === value) ?? null;
|
|
54
|
+
if (candidate) return { candidate, unusable: !candidate.active };
|
|
55
|
+
return { candidate: null, unusable: !truncated };
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
DEFAULT_ASSIGNEE_MAX_PAGES,
|
|
59
|
+
DEFAULT_ASSIGNEE_PAGE_SIZE,
|
|
60
|
+
chunkIds,
|
|
61
|
+
classifyDefaultAssigneeCandidates,
|
|
62
|
+
collectProjectMemberships,
|
|
63
|
+
isUsableDefaultAssigneeMembership,
|
|
64
|
+
readMembershipStaffMemberId,
|
|
65
|
+
readProjectMembershipPage,
|
|
66
|
+
resolveSelectedDefaultAssignee
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=projectDefaultAssignee.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.ts"],
|
|
4
|
+
"sourcesContent": ["import { resolveAssignmentState } from './projectTeamAssignment'\n\nexport const DEFAULT_ASSIGNEE_PAGE_SIZE = 100\nexport const DEFAULT_ASSIGNEE_MAX_PAGES = 20\n\nexport type ProjectMemberRow = {\n id?: unknown\n staffMemberId?: unknown\n staff_member_id?: unknown\n status?: unknown\n assigned_start_date?: unknown\n assignedStartDate?: unknown\n assigned_end_date?: unknown\n assignedEndDate?: unknown\n}\n\nexport type ProjectMembershipPage = {\n items: ProjectMemberRow[]\n totalPages: number | null\n}\n\nexport type ProjectMembershipCollection = {\n rows: ProjectMemberRow[]\n truncated: boolean\n}\n\nexport type DefaultAssigneeCandidate = {\n staffMemberId: string\n active: boolean\n}\n\nfunction readString(value: unknown): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null\n}\n\nexport function readMembershipStaffMemberId(row: ProjectMemberRow): string | null {\n return readString(row.staffMemberId) ?? readString(row.staff_member_id)\n}\n\n/**\n * Whether a membership can still receive a portal ticket as the default assignee \u2014\n * the browser twin of `resolveUsableDefaultAssignee`: an `active` membership inside\n * its assignment window plus the tenant's grace days.\n */\nexport function isUsableDefaultAssigneeMembership(row: ProjectMemberRow, graceDays: number, now: Date = new Date()): boolean {\n if (readString(row.status) !== 'active') return false\n const startDate = readString(row.assignedStartDate) ?? readString(row.assigned_start_date)\n const endDate = readString(row.assignedEndDate) ?? readString(row.assigned_end_date)\n return resolveAssignmentState({ startDate, endDate }, graceDays, now) === 'active'\n}\n\nexport type ProjectMembershipPageResponse = {\n ok: boolean\n result?: { items?: unknown; totalPages?: unknown } | null\n}\n\n/**\n * Reads one employees page, refusing anything but a successful response with a\n * row list: an error page carries no rows and no total, and reading it as an empty\n * last page would mark every member beyond it as gone.\n */\nexport function readProjectMembershipPage(response: ProjectMembershipPageResponse): ProjectMembershipPage {\n if (!response.ok) throw new Error('[internal] project membership page request failed')\n const items = response.result?.items\n if (!Array.isArray(items)) throw new Error('[internal] project membership page has no rows')\n const totalPages = typeof response.result?.totalPages === 'number' ? response.result.totalPages : null\n const rows = items.filter((item): item is ProjectMemberRow => typeof item === 'object' && item !== null)\n return { items: rows, totalPages }\n}\n\nexport async function collectProjectMemberships(\n fetchPage: (page: number) => Promise<ProjectMembershipPage>,\n options: { pageSize?: number; maxPages?: number } = {},\n): Promise<ProjectMembershipCollection> {\n const pageSize = options.pageSize ?? DEFAULT_ASSIGNEE_PAGE_SIZE\n const maxPages = options.maxPages ?? DEFAULT_ASSIGNEE_MAX_PAGES\n const rows: ProjectMemberRow[] = []\n for (let page = 1; page <= maxPages; page += 1) {\n const result = await fetchPage(page)\n rows.push(...result.items)\n const lastPage = result.totalPages !== null ? page >= result.totalPages : result.items.length < pageSize\n if (lastPage || result.items.length === 0) return { rows, truncated: false }\n }\n return { rows, truncated: true }\n}\n\nexport function classifyDefaultAssigneeCandidates(\n rows: ProjectMemberRow[],\n graceDays: number,\n now: Date = new Date(),\n): DefaultAssigneeCandidate[] {\n const byMember = new Map<string, boolean>()\n for (const row of rows) {\n const staffMemberId = readMembershipStaffMemberId(row)\n if (!staffMemberId) continue\n const active = isUsableDefaultAssigneeMembership(row, graceDays, now)\n byMember.set(staffMemberId, (byMember.get(staffMemberId) ?? false) || active)\n }\n return Array.from(byMember, ([staffMemberId, active]) => ({ staffMemberId, active }))\n}\n\nexport function chunkIds(ids: string[], size: number = DEFAULT_ASSIGNEE_PAGE_SIZE): string[][] {\n const batches: string[][] = []\n for (let index = 0; index < ids.length; index += size) batches.push(ids.slice(index, index + size))\n return batches\n}\n\nexport function resolveSelectedDefaultAssignee(\n candidates: DefaultAssigneeCandidate[],\n value: string | null,\n truncated: boolean,\n): { candidate: DefaultAssigneeCandidate | null; unusable: boolean } {\n if (!value) return { candidate: null, unusable: false }\n const candidate = candidates.find((entry) => entry.staffMemberId === value) ?? null\n if (candidate) return { candidate, unusable: !candidate.active }\n return { candidate: null, unusable: !truncated }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,8BAA8B;AAEhC,MAAM,6BAA6B;AACnC,MAAM,6BAA6B;AA4B1C,SAAS,WAAW,OAA+B;AACjD,SAAO,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,IAAI,MAAM,KAAK,IAAI;AAC/E;AAEO,SAAS,4BAA4B,KAAsC;AAChF,SAAO,WAAW,IAAI,aAAa,KAAK,WAAW,IAAI,eAAe;AACxE;AAOO,SAAS,kCAAkC,KAAuB,WAAmB,MAAY,oBAAI,KAAK,GAAY;AAC3H,MAAI,WAAW,IAAI,MAAM,MAAM,SAAU,QAAO;AAChD,QAAM,YAAY,WAAW,IAAI,iBAAiB,KAAK,WAAW,IAAI,mBAAmB;AACzF,QAAM,UAAU,WAAW,IAAI,eAAe,KAAK,WAAW,IAAI,iBAAiB;AACnF,SAAO,uBAAuB,EAAE,WAAW,QAAQ,GAAG,WAAW,GAAG,MAAM;AAC5E;AAYO,SAAS,0BAA0B,UAAgE;AACxG,MAAI,CAAC,SAAS,GAAI,OAAM,IAAI,MAAM,mDAAmD;AACrF,QAAM,QAAQ,SAAS,QAAQ;AAC/B,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,OAAM,IAAI,MAAM,gDAAgD;AAC3F,QAAM,aAAa,OAAO,SAAS,QAAQ,eAAe,WAAW,SAAS,OAAO,aAAa;AAClG,QAAM,OAAO,MAAM,OAAO,CAAC,SAAmC,OAAO,SAAS,YAAY,SAAS,IAAI;AACvG,SAAO,EAAE,OAAO,MAAM,WAAW;AACnC;AAEA,eAAsB,0BACpB,WACA,UAAoD,CAAC,GACf;AACtC,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,OAA2B,CAAC;AAClC,WAAS,OAAO,GAAG,QAAQ,UAAU,QAAQ,GAAG;AAC9C,UAAM,SAAS,MAAM,UAAU,IAAI;AACnC,SAAK,KAAK,GAAG,OAAO,KAAK;AACzB,UAAM,WAAW,OAAO,eAAe,OAAO,QAAQ,OAAO,aAAa,OAAO,MAAM,SAAS;AAChG,QAAI,YAAY,OAAO,MAAM,WAAW,EAAG,QAAO,EAAE,MAAM,WAAW,MAAM;AAAA,EAC7E;AACA,SAAO,EAAE,MAAM,WAAW,KAAK;AACjC;AAEO,SAAS,kCACd,MACA,WACA,MAAY,oBAAI,KAAK,GACO;AAC5B,QAAM,WAAW,oBAAI,IAAqB;AAC1C,aAAW,OAAO,MAAM;AACtB,UAAM,gBAAgB,4BAA4B,GAAG;AACrD,QAAI,CAAC,cAAe;AACpB,UAAM,SAAS,kCAAkC,KAAK,WAAW,GAAG;AACpE,aAAS,IAAI,gBAAgB,SAAS,IAAI,aAAa,KAAK,UAAU,MAAM;AAAA,EAC9E;AACA,SAAO,MAAM,KAAK,UAAU,CAAC,CAAC,eAAe,MAAM,OAAO,EAAE,eAAe,OAAO,EAAE;AACtF;AAEO,SAAS,SAAS,KAAe,OAAe,4BAAwC;AAC7F,QAAM,UAAsB,CAAC;AAC7B,WAAS,QAAQ,GAAG,QAAQ,IAAI,QAAQ,SAAS,KAAM,SAAQ,KAAK,IAAI,MAAM,OAAO,QAAQ,IAAI,CAAC;AAClG,SAAO;AACT;AAEO,SAAS,+BACd,YACA,OACA,WACmE;AACnE,MAAI,CAAC,MAAO,QAAO,EAAE,WAAW,MAAM,UAAU,MAAM;AACtD,QAAM,YAAY,WAAW,KAAK,CAAC,UAAU,MAAM,kBAAkB,KAAK,KAAK;AAC/E,MAAI,UAAW,QAAO,EAAE,WAAW,UAAU,CAAC,UAAU,OAAO;AAC/D,SAAO,EAAE,WAAW,MAAM,UAAU,CAAC,UAAU;AACjD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/staff/lib/time-tracking-ui/settingsTabs.ts"],
|
|
4
|
+
"sourcesContent": ["import type { TimeTrackingSettings } from '../time-tracking/settings'\n\nexport const TIME_TRACKING_SETTINGS_GENERAL_TAB = 'general'\nexport const TIME_TRACKING_SETTINGS_TAB_PARAM = 'tab'\n\n/**\n * Context of the `staff.time_tracking.settings:tabs` spot. A tab owns its saves and does\n * not sit under the page's Save bar. When it writes the settings record itself it builds\n * the body from `settings` (the last saved record, never the page's unsaved draft) and\n * hands the response to `onSettingsSaved`, which rebases the page draft onto it.\n */\nexport type TimeTrackingSettingsTabContext = {\n moduleId: string\n canManage: boolean\n settings: TimeTrackingSettings\n onSettingsSaved: (settings: TimeTrackingSettings) => void\n}\n"],
|
|
5
|
+
"mappings": "AAEO,MAAM,qCAAqC;AAC3C,MAAM,mCAAmC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const SUPPORT_PORTAL_STATUS_VARIANT = {
|
|
2
|
+
received: "info",
|
|
3
|
+
in_progress: "warning",
|
|
4
|
+
resolved: "success"
|
|
5
|
+
};
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function readText(source, key) {
|
|
10
|
+
const value = source?.[key];
|
|
11
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
12
|
+
}
|
|
13
|
+
function readPortalStatus(value) {
|
|
14
|
+
return value === "resolved" || value === "in_progress" ? value : "received";
|
|
15
|
+
}
|
|
16
|
+
function readSupportTicket(row) {
|
|
17
|
+
if (!row) return null;
|
|
18
|
+
const enrichment = row._staffSupport;
|
|
19
|
+
if (!isRecord(enrichment)) return null;
|
|
20
|
+
const ticket = enrichment.ticket;
|
|
21
|
+
if (!isRecord(ticket)) return null;
|
|
22
|
+
const id = readText(ticket, "id");
|
|
23
|
+
const supportReference = readText(ticket, "supportReference");
|
|
24
|
+
if (!id || !supportReference) return null;
|
|
25
|
+
const category = isRecord(ticket.category) ? ticket.category : null;
|
|
26
|
+
const customer = isRecord(ticket.customerSnapshot) ? ticket.customerSnapshot : null;
|
|
27
|
+
const request = isRecord(ticket.customerRequest) ? ticket.customerRequest : null;
|
|
28
|
+
return {
|
|
29
|
+
id,
|
|
30
|
+
supportReference,
|
|
31
|
+
categoryValue: readText(category, "value") ?? "",
|
|
32
|
+
categoryLabel: readText(category, "label") ?? readText(category, "value") ?? "",
|
|
33
|
+
routedVia: ticket.routedVia === "category" ? "category" : "fallback",
|
|
34
|
+
portalStatus: readPortalStatus(ticket.portalStatus),
|
|
35
|
+
customerName: readText(customer, "displayName"),
|
|
36
|
+
customerKind: readText(customer, "kind"),
|
|
37
|
+
reporterName: readText(customer, "reporterDisplayName"),
|
|
38
|
+
reportedByCustomerUserId: readText(ticket, "reportedByCustomerUserId"),
|
|
39
|
+
unnotifiedAt: readText(ticket, "unnotifiedAt"),
|
|
40
|
+
requestSubject: readText(request, "subject"),
|
|
41
|
+
requestBody: typeof request?.body === "string" ? request.body : null
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const SUPPORT_ACTIVITY_EVENT = "staff.support_ticket.activity_recorded";
|
|
45
|
+
export {
|
|
46
|
+
SUPPORT_ACTIVITY_EVENT,
|
|
47
|
+
SUPPORT_PORTAL_STATUS_VARIANT,
|
|
48
|
+
readSupportTicket
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=supportTicketData.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/staff/lib/time-tracking-ui/supportTicketData.ts"],
|
|
4
|
+
"sourcesContent": ["import type { StatusBadgeVariant } from '@open-mercato/ui/primitives/status-badge'\nimport type { BoardApiRow } from './kanbanBoardData'\n\nexport type SupportPortalStatus = 'received' | 'in_progress' | 'resolved'\n\nexport type SupportTicketView = {\n id: string\n supportReference: string\n categoryValue: string\n categoryLabel: string\n routedVia: 'category' | 'fallback'\n portalStatus: SupportPortalStatus\n customerName: string | null\n customerKind: string | null\n reporterName: string | null\n reportedByCustomerUserId: string | null\n unnotifiedAt: string | null\n requestSubject: string | null\n requestBody: string | null\n}\n\nexport const SUPPORT_PORTAL_STATUS_VARIANT: Record<SupportPortalStatus, StatusBadgeVariant> = {\n received: 'info',\n in_progress: 'warning',\n resolved: 'success',\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction readText(source: Record<string, unknown> | null | undefined, key: string): string | null {\n const value = source?.[key]\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nfunction readPortalStatus(value: unknown): SupportPortalStatus {\n return value === 'resolved' || value === 'in_progress' ? value : 'received'\n}\n\n/** Reads the `_staffSupport.ticket` enrichment off a tasks API row. Internal tasks answer `null`. */\nexport function readSupportTicket(row: BoardApiRow | null | undefined): SupportTicketView | null {\n if (!row) return null\n const enrichment = (row as Record<string, unknown>)._staffSupport\n if (!isRecord(enrichment)) return null\n const ticket = enrichment.ticket\n if (!isRecord(ticket)) return null\n const id = readText(ticket, 'id')\n const supportReference = readText(ticket, 'supportReference')\n if (!id || !supportReference) return null\n const category = isRecord(ticket.category) ? ticket.category : null\n const customer = isRecord(ticket.customerSnapshot) ? ticket.customerSnapshot : null\n const request = isRecord(ticket.customerRequest) ? ticket.customerRequest : null\n return {\n id,\n supportReference,\n categoryValue: readText(category, 'value') ?? '',\n categoryLabel: readText(category, 'label') ?? readText(category, 'value') ?? '',\n routedVia: ticket.routedVia === 'category' ? 'category' : 'fallback',\n portalStatus: readPortalStatus(ticket.portalStatus),\n customerName: readText(customer, 'displayName'),\n customerKind: readText(customer, 'kind'),\n reporterName: readText(customer, 'reporterDisplayName'),\n reportedByCustomerUserId: readText(ticket, 'reportedByCustomerUserId'),\n unnotifiedAt: readText(ticket, 'unnotifiedAt'),\n requestSubject: readText(request, 'subject'),\n requestBody: typeof request?.body === 'string' ? (request.body as string) : null,\n }\n}\n\nexport const SUPPORT_ACTIVITY_EVENT = 'staff.support_ticket.activity_recorded'\n"],
|
|
5
|
+
"mappings": "AAqBO,MAAM,gCAAiF;AAAA,EAC5F,UAAU;AAAA,EACV,aAAa;AAAA,EACb,UAAU;AACZ;AAEA,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,SAAS,QAAoD,KAA4B;AAChG,QAAM,QAAQ,SAAS,GAAG;AAC1B,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ;AACjE;AAEA,SAAS,iBAAiB,OAAqC;AAC7D,SAAO,UAAU,cAAc,UAAU,gBAAgB,QAAQ;AACnE;AAGO,SAAS,kBAAkB,KAA+D;AAC/F,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,aAAc,IAAgC;AACpD,MAAI,CAAC,SAAS,UAAU,EAAG,QAAO;AAClC,QAAM,SAAS,WAAW;AAC1B,MAAI,CAAC,SAAS,MAAM,EAAG,QAAO;AAC9B,QAAM,KAAK,SAAS,QAAQ,IAAI;AAChC,QAAM,mBAAmB,SAAS,QAAQ,kBAAkB;AAC5D,MAAI,CAAC,MAAM,CAAC,iBAAkB,QAAO;AACrC,QAAM,WAAW,SAAS,OAAO,QAAQ,IAAI,OAAO,WAAW;AAC/D,QAAM,WAAW,SAAS,OAAO,gBAAgB,IAAI,OAAO,mBAAmB;AAC/E,QAAM,UAAU,SAAS,OAAO,eAAe,IAAI,OAAO,kBAAkB;AAC5E,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,eAAe,SAAS,UAAU,OAAO,KAAK;AAAA,IAC9C,eAAe,SAAS,UAAU,OAAO,KAAK,SAAS,UAAU,OAAO,KAAK;AAAA,IAC7E,WAAW,OAAO,cAAc,aAAa,aAAa;AAAA,IAC1D,cAAc,iBAAiB,OAAO,YAAY;AAAA,IAClD,cAAc,SAAS,UAAU,aAAa;AAAA,IAC9C,cAAc,SAAS,UAAU,MAAM;AAAA,IACvC,cAAc,SAAS,UAAU,qBAAqB;AAAA,IACtD,0BAA0B,SAAS,QAAQ,0BAA0B;AAAA,IACrE,cAAc,SAAS,QAAQ,cAAc;AAAA,IAC7C,gBAAgB,SAAS,SAAS,SAAS;AAAA,IAC3C,aAAa,OAAO,SAAS,SAAS,WAAY,QAAQ,OAAkB;AAAA,EAC9E;AACF;AAEO,MAAM,yBAAyB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { formatDuration } from "../time-tracking/duration.js";
|
|
2
2
|
import { readNumber, readString } from "./kanbanBoardData.js";
|
|
3
|
+
const TASK_DRAWER_QUERY_ROOT = ["staff", "time-tracking", "task-drawer"];
|
|
3
4
|
function toDrawerComment(row) {
|
|
4
5
|
const id = readString(row, "id");
|
|
5
6
|
const body = readString(row, "body");
|
|
@@ -10,7 +11,10 @@ function toDrawerComment(row) {
|
|
|
10
11
|
authorUserId: readString(row, "authorUserId", "author_user_id"),
|
|
11
12
|
authorName: readString(row, "authorName", "author_name"),
|
|
12
13
|
authorEmail: readString(row, "authorEmail", "author_email"),
|
|
13
|
-
createdAt: readString(row, "createdAt", "created_at")
|
|
14
|
+
createdAt: readString(row, "createdAt", "created_at"),
|
|
15
|
+
visibility: readString(row, "visibility") === "customer" ? "customer" : "internal",
|
|
16
|
+
authorKind: readString(row, "authorKind", "author_kind") === "customer" ? "customer" : "staff",
|
|
17
|
+
updatedAt: readString(row, "updatedAt", "updated_at")
|
|
14
18
|
};
|
|
15
19
|
}
|
|
16
20
|
function toDrawerEntry(row) {
|
|
@@ -96,11 +100,19 @@ function buildEntryClocks({ date, startClock, endClock, minutes }) {
|
|
|
96
100
|
endedAt: `${date}T${minutesToClock(resolvedEnd)}`
|
|
97
101
|
};
|
|
98
102
|
}
|
|
103
|
+
const TASK_ASSIGNMENT_CHANGED_EVENT = "staff.timesheets.time_task.assignee_changed";
|
|
104
|
+
function isAssignmentEventForTask(payload, taskId) {
|
|
105
|
+
if (!taskId || typeof payload !== "object" || payload === null) return false;
|
|
106
|
+
return payload.id === taskId;
|
|
107
|
+
}
|
|
99
108
|
export {
|
|
109
|
+
TASK_ASSIGNMENT_CHANGED_EVENT,
|
|
110
|
+
TASK_DRAWER_QUERY_ROOT,
|
|
100
111
|
buildEntryClocks,
|
|
101
112
|
childMinutes,
|
|
102
113
|
formatElapsed,
|
|
103
114
|
formatSplitMinutes,
|
|
115
|
+
isAssignmentEventForTask,
|
|
104
116
|
resolveDefaultStatusId,
|
|
105
117
|
resolveDoneStatusId,
|
|
106
118
|
summarizeEntries,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/staff/lib/time-tracking-ui/taskDrawerData.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Row parsing and arithmetic for the task drawer of screen 7 (T3.7).\n *\n * Split from `TaskDrawer.tsx` for the same reason `kanbanBoardData.ts` is split\n * from the board: the decisions that shape what the drawer *says* \u2014 which column\n * a ticked subtask moves to, how the billable split is summed, whether that sum\n * is complete \u2014 are unit-testable without a DOM, and the component stays about\n * fetching, mutating and rendering.\n *\n * Board helpers are reused rather than re-derived (`toBoardTask`,\n * `formatBoardMinutes`, `computeSubtaskProgress`), so a task row means the same\n * thing on the card and inside the drawer.\n */\n\nimport { formatDuration } from '../time-tracking/duration'\nimport { readNumber, readString, type BoardApiRow, type BoardStatus } from './kanbanBoardData'\n\nexport type DrawerComment = {\n id: string\n body: string\n authorUserId: string | null\n authorName: string | null\n authorEmail: string | null\n createdAt: string | null\n}\n\nexport type DrawerEntry = {\n id: string\n taskId: string | null\n date: string | null\n description: string | null\n durationMinutes: number\n isBillable: boolean\n /** Present only for a caller holding `staff.timesheets.rates.view`. */\n cost: number | null\n currencyCode: string | null\n}\n\nexport type EntryTotals = {\n billableMinutes: number\n nonBillableMinutes: number\n /** `null` when the caller may not see money, or when no entry carried a cost. */\n cost: number | null\n currencyCode: string | null\n /** The entries scan hit its page bound, so the split is a lower bound. */\n partial: boolean\n}\n\nexport function toDrawerComment(row: BoardApiRow): DrawerComment | null {\n const id = readString(row, 'id')\n const body = readString(row, 'body')\n if (!id || !body) return null\n return {\n id,\n body,\n authorUserId: readString(row, 'authorUserId', 'author_user_id'),\n authorName: readString(row, 'authorName', 'author_name'),\n authorEmail: readString(row, 'authorEmail', 'author_email'),\n createdAt: readString(row, 'createdAt', 'created_at'),\n }\n}\n\nexport function toDrawerEntry(row: BoardApiRow): DrawerEntry | null {\n const id = readString(row, 'id')\n if (!id) return null\n const billable = row.is_billable ?? row.isBillable\n const cost = readNumber(row, 'cost')\n return {\n id,\n taskId: readString(row, 'task_id', 'taskId'),\n date: readString(row, 'date'),\n description: readString(row, 'description', 'notes'),\n durationMinutes: readNumber(row, 'duration_minutes', 'durationMinutes') ?? 0,\n isBillable: billable !== false,\n cost: cost === null ? null : cost,\n currencyCode: readString(row, 'currencyCode', 'rate_currency_code'),\n }\n}\n\n/**\n * The billable / non-billable / cost split of screen 7 (note 5).\n *\n * Money only exists in the rows when the caller holds\n * `staff.timesheets.rates.view` \u2014 the entries route omits the keys entirely for\n * everyone else \u2014 so an absent `cost` stays `null` here instead of becoming a\n * zero that would read as \"this work is free\".\n */\nexport function summarizeEntries(entries: readonly DrawerEntry[], partial: boolean): EntryTotals {\n let billableMinutes = 0\n let nonBillableMinutes = 0\n let cost: number | null = null\n let currencyCode: string | null = null\n for (const entry of entries) {\n if (entry.isBillable) billableMinutes += entry.durationMinutes\n else nonBillableMinutes += entry.durationMinutes\n if (entry.cost === null) continue\n cost = (cost ?? 0) + entry.cost\n currencyCode = currencyCode ?? entry.currencyCode\n }\n return { billableMinutes, nonBillableMinutes, cost, currencyCode, partial }\n}\n\n/**\n * Ticking a subtask is a status move, not a boolean (D-2): the child goes to the\n * project's first done column, and un-ticking returns it to the default one.\n * A project with no done column cannot express \"done\" at all, which is why the\n * checkbox is disabled rather than silently writing nothing.\n */\nexport function resolveDoneStatusId(statuses: readonly BoardStatus[]): string | null {\n return statuses.find((status) => status.isDone)?.id ?? null\n}\n\nexport function resolveDefaultStatusId(statuses: readonly BoardStatus[]): string | null {\n const flagged = statuses.find((status) => status.isDefault)\n if (flagged) return flagged.id\n return statuses.find((status) => !status.isDone)?.id ?? null\n}\n\n/** `01:14:02` \u2014 the running-timer label of the mockup's stop button. */\nexport function formatElapsed(startedAt: string | null | undefined, now: number): string {\n if (!startedAt) return '00:00:00'\n const started = Date.parse(startedAt)\n if (!Number.isFinite(started)) return '00:00:00'\n const seconds = Math.max(0, Math.floor((now - started) / 1000))\n const hours = Math.floor(seconds / 3600)\n const minutes = Math.floor((seconds % 3600) / 60)\n const rest = seconds % 60\n return [hours, minutes, rest].map((part) => String(part).padStart(2, '0')).join(':')\n}\n\n/** `1h 40m` \u2014 the own/children split reads as a duration, not as a clock. */\nexport function formatSplitMinutes(minutes: number): string {\n return formatDuration(Number.isFinite(minutes) && minutes > 0 ? minutes : 0, 'hm')\n}\n\n/** The minutes a task's children contributed to its inclusive rollup (D-2). */\nexport function childMinutes(loggedMinutes: number, ownMinutes: number): number {\n const delta = loggedMinutes - ownMinutes\n return delta > 0 ? delta : 0\n}\n\nexport function todayIsoDate(now: Date = new Date()): string {\n const month = String(now.getMonth() + 1).padStart(2, '0')\n const day = String(now.getDate()).padStart(2, '0')\n return `${now.getFullYear()}-${month}-${day}`\n}\n\nexport type EntryClockInput = {\n date: string\n startClock: string | null\n endClock: string | null\n minutes: number\n}\n\nexport type EntryClocks = { startedAt?: string; endedAt?: string }\n\nfunction clockToMinutes(clock: string): number | null {\n const match = /^(\\d{1,2}):(\\d{2})$/.exec(clock.trim())\n if (!match) return null\n const hours = Number(match[1])\n const mins = Number(match[2])\n if (!Number.isFinite(hours) || !Number.isFinite(mins) || hours > 23 || mins > 59) return null\n return hours * 60 + mins\n}\n\nfunction minutesToClock(total: number): string {\n const wrapped = ((total % 1440) + 1440) % 1440\n return `${String(Math.floor(wrapped / 60)).padStart(2, '0')}:${String(wrapped % 60).padStart(2, '0')}`\n}\n\n/**\n * Resolves the quick log's optional clocks into a pair, or nothing at all.\n *\n * A lone `started_at` is not a partial record \u2014 it is a *running timer*, because\n * `started_at IS NOT NULL AND ended_at IS NULL` is exactly how the entries list\n * identifies one. An entry saved that way reads as a timer running since that\n * moment and cannot be stopped, since no timer segment was ever opened for it.\n *\n * So the missing half is derived from the duration, which is authoritative in the\n * quick log. Both blank means a duration-only entry, which is a legitimate shape.\n */\nexport function buildEntryClocks({ date, startClock, endClock, minutes }: EntryClockInput): EntryClocks {\n const start = startClock ? clockToMinutes(startClock) : null\n const end = endClock ? clockToMinutes(endClock) : null\n if (start === null && end === null) return {}\n\n const resolvedStart = start !== null ? start : (end as number) - minutes\n const resolvedEnd = end !== null ? end : (start as number) + minutes\n\n return {\n startedAt: `${date}T${minutesToClock(resolvedStart)}`,\n endedAt: `${date}T${minutesToClock(resolvedEnd)}`,\n }\n}\n"],
|
|
5
|
-
"mappings": "AAcA,SAAS,sBAAsB;AAC/B,SAAS,YAAY,kBAAsD;
|
|
4
|
+
"sourcesContent": ["/**\n * Row parsing and arithmetic for the task drawer of screen 7 (T3.7).\n *\n * Split from `TaskDrawer.tsx` for the same reason `kanbanBoardData.ts` is split\n * from the board: the decisions that shape what the drawer *says* \u2014 which column\n * a ticked subtask moves to, how the billable split is summed, whether that sum\n * is complete \u2014 are unit-testable without a DOM, and the component stays about\n * fetching, mutating and rendering.\n *\n * Board helpers are reused rather than re-derived (`toBoardTask`,\n * `formatBoardMinutes`, `computeSubtaskProgress`), so a task row means the same\n * thing on the card and inside the drawer.\n */\n\nimport { formatDuration } from '../time-tracking/duration'\nimport { readNumber, readString, type BoardApiRow, type BoardStatus } from './kanbanBoardData'\n\nexport const TASK_DRAWER_QUERY_ROOT = ['staff', 'time-tracking', 'task-drawer'] as const\n\nexport type DrawerComment = {\n id: string\n body: string\n authorUserId: string | null\n authorName: string | null\n authorEmail: string | null\n createdAt: string | null\n visibility: 'internal' | 'customer'\n authorKind: 'staff' | 'customer'\n updatedAt: string | null\n}\n\nexport type DrawerEntry = {\n id: string\n taskId: string | null\n date: string | null\n description: string | null\n durationMinutes: number\n isBillable: boolean\n /** Present only for a caller holding `staff.timesheets.rates.view`. */\n cost: number | null\n currencyCode: string | null\n}\n\nexport type EntryTotals = {\n billableMinutes: number\n nonBillableMinutes: number\n /** `null` when the caller may not see money, or when no entry carried a cost. */\n cost: number | null\n currencyCode: string | null\n /** The entries scan hit its page bound, so the split is a lower bound. */\n partial: boolean\n}\n\nexport function toDrawerComment(row: BoardApiRow): DrawerComment | null {\n const id = readString(row, 'id')\n const body = readString(row, 'body')\n if (!id || !body) return null\n return {\n id,\n body,\n authorUserId: readString(row, 'authorUserId', 'author_user_id'),\n authorName: readString(row, 'authorName', 'author_name'),\n authorEmail: readString(row, 'authorEmail', 'author_email'),\n createdAt: readString(row, 'createdAt', 'created_at'),\n visibility: readString(row, 'visibility') === 'customer' ? 'customer' : 'internal',\n authorKind: readString(row, 'authorKind', 'author_kind') === 'customer' ? 'customer' : 'staff',\n updatedAt: readString(row, 'updatedAt', 'updated_at'),\n }\n}\n\nexport function toDrawerEntry(row: BoardApiRow): DrawerEntry | null {\n const id = readString(row, 'id')\n if (!id) return null\n const billable = row.is_billable ?? row.isBillable\n const cost = readNumber(row, 'cost')\n return {\n id,\n taskId: readString(row, 'task_id', 'taskId'),\n date: readString(row, 'date'),\n description: readString(row, 'description', 'notes'),\n durationMinutes: readNumber(row, 'duration_minutes', 'durationMinutes') ?? 0,\n isBillable: billable !== false,\n cost: cost === null ? null : cost,\n currencyCode: readString(row, 'currencyCode', 'rate_currency_code'),\n }\n}\n\n/**\n * The billable / non-billable / cost split of screen 7 (note 5).\n *\n * Money only exists in the rows when the caller holds\n * `staff.timesheets.rates.view` \u2014 the entries route omits the keys entirely for\n * everyone else \u2014 so an absent `cost` stays `null` here instead of becoming a\n * zero that would read as \"this work is free\".\n */\nexport function summarizeEntries(entries: readonly DrawerEntry[], partial: boolean): EntryTotals {\n let billableMinutes = 0\n let nonBillableMinutes = 0\n let cost: number | null = null\n let currencyCode: string | null = null\n for (const entry of entries) {\n if (entry.isBillable) billableMinutes += entry.durationMinutes\n else nonBillableMinutes += entry.durationMinutes\n if (entry.cost === null) continue\n cost = (cost ?? 0) + entry.cost\n currencyCode = currencyCode ?? entry.currencyCode\n }\n return { billableMinutes, nonBillableMinutes, cost, currencyCode, partial }\n}\n\n/**\n * Ticking a subtask is a status move, not a boolean (D-2): the child goes to the\n * project's first done column, and un-ticking returns it to the default one.\n * A project with no done column cannot express \"done\" at all, which is why the\n * checkbox is disabled rather than silently writing nothing.\n */\nexport function resolveDoneStatusId(statuses: readonly BoardStatus[]): string | null {\n return statuses.find((status) => status.isDone)?.id ?? null\n}\n\nexport function resolveDefaultStatusId(statuses: readonly BoardStatus[]): string | null {\n const flagged = statuses.find((status) => status.isDefault)\n if (flagged) return flagged.id\n return statuses.find((status) => !status.isDone)?.id ?? null\n}\n\n/** `01:14:02` \u2014 the running-timer label of the mockup's stop button. */\nexport function formatElapsed(startedAt: string | null | undefined, now: number): string {\n if (!startedAt) return '00:00:00'\n const started = Date.parse(startedAt)\n if (!Number.isFinite(started)) return '00:00:00'\n const seconds = Math.max(0, Math.floor((now - started) / 1000))\n const hours = Math.floor(seconds / 3600)\n const minutes = Math.floor((seconds % 3600) / 60)\n const rest = seconds % 60\n return [hours, minutes, rest].map((part) => String(part).padStart(2, '0')).join(':')\n}\n\n/** `1h 40m` \u2014 the own/children split reads as a duration, not as a clock. */\nexport function formatSplitMinutes(minutes: number): string {\n return formatDuration(Number.isFinite(minutes) && minutes > 0 ? minutes : 0, 'hm')\n}\n\n/** The minutes a task's children contributed to its inclusive rollup (D-2). */\nexport function childMinutes(loggedMinutes: number, ownMinutes: number): number {\n const delta = loggedMinutes - ownMinutes\n return delta > 0 ? delta : 0\n}\n\nexport function todayIsoDate(now: Date = new Date()): string {\n const month = String(now.getMonth() + 1).padStart(2, '0')\n const day = String(now.getDate()).padStart(2, '0')\n return `${now.getFullYear()}-${month}-${day}`\n}\n\nexport type EntryClockInput = {\n date: string\n startClock: string | null\n endClock: string | null\n minutes: number\n}\n\nexport type EntryClocks = { startedAt?: string; endedAt?: string }\n\nfunction clockToMinutes(clock: string): number | null {\n const match = /^(\\d{1,2}):(\\d{2})$/.exec(clock.trim())\n if (!match) return null\n const hours = Number(match[1])\n const mins = Number(match[2])\n if (!Number.isFinite(hours) || !Number.isFinite(mins) || hours > 23 || mins > 59) return null\n return hours * 60 + mins\n}\n\nfunction minutesToClock(total: number): string {\n const wrapped = ((total % 1440) + 1440) % 1440\n return `${String(Math.floor(wrapped / 60)).padStart(2, '0')}:${String(wrapped % 60).padStart(2, '0')}`\n}\n\n/**\n * Resolves the quick log's optional clocks into a pair, or nothing at all.\n *\n * A lone `started_at` is not a partial record \u2014 it is a *running timer*, because\n * `started_at IS NOT NULL AND ended_at IS NULL` is exactly how the entries list\n * identifies one. An entry saved that way reads as a timer running since that\n * moment and cannot be stopped, since no timer segment was ever opened for it.\n *\n * So the missing half is derived from the duration, which is authoritative in the\n * quick log. Both blank means a duration-only entry, which is a legitimate shape.\n */\nexport function buildEntryClocks({ date, startClock, endClock, minutes }: EntryClockInput): EntryClocks {\n const start = startClock ? clockToMinutes(startClock) : null\n const end = endClock ? clockToMinutes(endClock) : null\n if (start === null && end === null) return {}\n\n const resolvedStart = start !== null ? start : (end as number) - minutes\n const resolvedEnd = end !== null ? end : (start as number) + minutes\n\n return {\n startedAt: `${date}T${minutesToClock(resolvedStart)}`,\n endedAt: `${date}T${minutesToClock(resolvedEnd)}`,\n }\n}\n\nexport const TASK_ASSIGNMENT_CHANGED_EVENT = 'staff.timesheets.time_task.assignee_changed'\n\nexport function isAssignmentEventForTask(payload: unknown, taskId: string): boolean {\n if (!taskId || typeof payload !== 'object' || payload === null) return false\n return (payload as { id?: unknown }).id === taskId\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { formatDuration } from "../time-tracking/duration.js";
|
|
2
2
|
import { roundMinutes } from "../time-tracking/rounding.js";
|
|
3
|
-
import { contributedTimeTrackingSettingKeys } from "../time-tracking/settingKeys.js";
|
|
3
|
+
import { DEFAULT_SUPPORT_REFERENCE_PREFIX, contributedTimeTrackingSettingKeys } from "../time-tracking/settingKeys.js";
|
|
4
4
|
import { readTimeTrackingSettingValue } from "../time-tracking/settings.js";
|
|
5
5
|
const ROUNDING_UNIT_OPTIONS = [0, 5, 10, 15];
|
|
6
6
|
const ROUNDING_EXAMPLE_MINUTES = [62, 76, 3, 120];
|
|
@@ -33,6 +33,7 @@ function toSettingsDraft(settings) {
|
|
|
33
33
|
warningsOverlap: settings.warnings.overlap,
|
|
34
34
|
warningsRunningTimer: settings.warnings.runningTimer,
|
|
35
35
|
assignmentGraceDaysText: String(settings.access.assignmentGraceDays),
|
|
36
|
+
supportReferencePrefix: settings.support?.referencePrefix ?? DEFAULT_SUPPORT_REFERENCE_PREFIX,
|
|
36
37
|
contributed: Object.fromEntries(
|
|
37
38
|
contributedTimeTrackingSettingKeys().map((entry) => [
|
|
38
39
|
entry.id,
|
|
@@ -76,7 +77,8 @@ function toSettingsPayload(draft) {
|
|
|
76
77
|
},
|
|
77
78
|
targets: { dailyHours },
|
|
78
79
|
warnings: { overlap: draft.warningsOverlap, runningTimer: draft.warningsRunningTimer },
|
|
79
|
-
access: { assignmentGraceDays }
|
|
80
|
+
access: { assignmentGraceDays },
|
|
81
|
+
support: { referencePrefix: draft.supportReferencePrefix }
|
|
80
82
|
};
|
|
81
83
|
for (const entry of contributedTimeTrackingSettingKeys()) {
|
|
82
84
|
const group = payload[entry.group] ?? {};
|
|
@@ -85,6 +87,37 @@ function toSettingsPayload(draft) {
|
|
|
85
87
|
}
|
|
86
88
|
return payload;
|
|
87
89
|
}
|
|
90
|
+
function rebaseSettingsDraft(draft, previousBaseline, nextBaseline) {
|
|
91
|
+
const pick = (current, previous, next) => Object.is(current, previous) ? next : current;
|
|
92
|
+
const contributed = { ...nextBaseline.contributed };
|
|
93
|
+
for (const [id, value] of Object.entries(draft.contributed)) {
|
|
94
|
+
contributed[id] = pick(value, previousBaseline.contributed[id], nextBaseline.contributed[id]);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
roundingUnitMinutes: pick(draft.roundingUnitMinutes, previousBaseline.roundingUnitMinutes, nextBaseline.roundingUnitMinutes),
|
|
98
|
+
roundingDirection: pick(draft.roundingDirection, previousBaseline.roundingDirection, nextBaseline.roundingDirection),
|
|
99
|
+
defaultsBillable: pick(draft.defaultsBillable, previousBaseline.defaultsBillable, nextBaseline.defaultsBillable),
|
|
100
|
+
defaultsChainStartFromPreviousEnd: pick(
|
|
101
|
+
draft.defaultsChainStartFromPreviousEnd,
|
|
102
|
+
previousBaseline.defaultsChainStartFromPreviousEnd,
|
|
103
|
+
nextBaseline.defaultsChainStartFromPreviousEnd
|
|
104
|
+
),
|
|
105
|
+
dailyHoursText: pick(draft.dailyHoursText, previousBaseline.dailyHoursText, nextBaseline.dailyHoursText),
|
|
106
|
+
warningsOverlap: pick(draft.warningsOverlap, previousBaseline.warningsOverlap, nextBaseline.warningsOverlap),
|
|
107
|
+
warningsRunningTimer: pick(draft.warningsRunningTimer, previousBaseline.warningsRunningTimer, nextBaseline.warningsRunningTimer),
|
|
108
|
+
assignmentGraceDaysText: pick(
|
|
109
|
+
draft.assignmentGraceDaysText,
|
|
110
|
+
previousBaseline.assignmentGraceDaysText,
|
|
111
|
+
nextBaseline.assignmentGraceDaysText
|
|
112
|
+
),
|
|
113
|
+
supportReferencePrefix: pick(
|
|
114
|
+
draft.supportReferencePrefix,
|
|
115
|
+
previousBaseline.supportReferencePrefix,
|
|
116
|
+
nextBaseline.supportReferencePrefix
|
|
117
|
+
),
|
|
118
|
+
contributed
|
|
119
|
+
};
|
|
120
|
+
}
|
|
88
121
|
function isSettingsDraftDirty(draft, baseline) {
|
|
89
122
|
return draft.roundingUnitMinutes !== baseline.roundingUnitMinutes || draft.roundingDirection !== baseline.roundingDirection || draft.defaultsBillable !== baseline.defaultsBillable || draft.defaultsChainStartFromPreviousEnd !== baseline.defaultsChainStartFromPreviousEnd || draft.dailyHoursText.trim() !== baseline.dailyHoursText.trim() || draft.warningsOverlap !== baseline.warningsOverlap || draft.warningsRunningTimer !== baseline.warningsRunningTimer || draft.assignmentGraceDaysText.trim() !== baseline.assignmentGraceDaysText.trim() || contributedTimeTrackingSettingKeys().some(
|
|
90
123
|
(entry) => !Object.is(draft.contributed[entry.id], baseline.contributed[entry.id])
|
|
@@ -98,6 +131,7 @@ export {
|
|
|
98
131
|
draftRounding,
|
|
99
132
|
formatSignedMinutes,
|
|
100
133
|
isSettingsDraftDirty,
|
|
134
|
+
rebaseSettingsDraft,
|
|
101
135
|
settingsDraftErrors,
|
|
102
136
|
toSettingsDraft,
|
|
103
137
|
toSettingsPayload
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Pure state for the module settings screen (T7.1, screen 16).\n *\n * The screen edits two numbers that are legitimately empty-able or bounded\n * (`targets.dailyHours` may be blank = no target; `access.assignmentGraceDays` is\n * an integer day count), so the draft keeps them as the TEXT the user typed and\n * converts only at the edges. Stripping a half-typed number back into the field is\n * the same defect `DurationInput` was built to avoid.\n *\n * `buildRoundingExamples` is the screen's most load-bearing function: note 1 of the\n * mockup requires the worked examples to recompute live, because rounding is a rule\n * people misread and its effect is otherwise invisible until an invoice. It calls\n * `roundMinutes` \u2014 the same function the write path calls \u2014 so a preview that\n * disagreed with reality is not expressible here.\n */\n\nimport { formatDuration } from '../time-tracking/duration'\nimport { roundMinutes, type RoundingSettings, type RoundingUnitMinutes } from '../time-tracking/rounding'\nimport { contributedTimeTrackingSettingKeys } from '../time-tracking/settingKeys'\nimport { readTimeTrackingSettingValue, type TimeTrackingSettings } from '../time-tracking/settings'\n\nexport const ROUNDING_UNIT_OPTIONS: readonly RoundingUnitMinutes[] = [0, 5, 10, 15]\n\n/**\n * The four cases from the mockup: just over a unit, past the halfway point, a\n * minimum-charge case, and one that is already exact and must not move.\n */\nexport const ROUNDING_EXAMPLE_MINUTES: readonly number[] = [62, 76, 3, 120]\n\nexport type RoundingExample = {\n rawMinutes: number\n roundedMinutes: number\n rawLabel: string\n roundedLabel: string\n}\n\nexport function buildRoundingExamples(rounding: RoundingSettings): RoundingExample[] {\n return ROUNDING_EXAMPLE_MINUTES.map((rawMinutes) => {\n const roundedMinutes = roundMinutes(rawMinutes, rounding)\n return {\n rawMinutes,\n roundedMinutes,\n rawLabel: formatDuration(rawMinutes, 'clock'),\n roundedLabel: formatDuration(roundedMinutes, 'clock'),\n }\n })\n}\n\n/** `+38:15` / `\u221212:30` / `0:00`, for the impact card's delta against raw time. */\nexport function formatSignedMinutes(minutes: number): string {\n const safe = Number.isFinite(minutes) ? Math.round(minutes) : 0\n const magnitude = formatDuration(Math.abs(safe), 'clock')\n if (safe > 0) return `+${magnitude}`\n if (safe < 0) return `\u2212${magnitude}`\n return magnitude\n}\n\nexport type TimeTrackingSettingsDraft = {\n roundingUnitMinutes: RoundingUnitMinutes\n roundingDirection: RoundingSettings['direction']\n defaultsBillable: boolean\n defaultsChainStartFromPreviousEnd: boolean\n /** Free text: empty means \"no daily target\", which is a real, storable value. */\n dailyHoursText: string\n warningsOverlap: boolean\n warningsRunningTimer: boolean\n assignmentGraceDaysText: string\n /**\n * EP-42 \u2014 every contributed setting key, by its `<group>.<key>` id, carried through\n * the draft untouched so the page's own Save round-trips a contribution it knows\n * nothing about. The eight built-ins are NOT in here: the page renders those itself.\n */\n contributed: Record<string, unknown>\n}\n\n\nexport const MAX_ASSIGNMENT_GRACE_DAYS_INPUT = 365\n\nexport function toSettingsDraft(settings: TimeTrackingSettings): TimeTrackingSettingsDraft {\n return {\n roundingUnitMinutes: settings.rounding.unitMinutes,\n roundingDirection: settings.rounding.direction,\n defaultsBillable: settings.defaults.billable,\n defaultsChainStartFromPreviousEnd: settings.defaults.chainStartFromPreviousEnd,\n dailyHoursText: settings.targets.dailyHours === null ? '' : String(settings.targets.dailyHours),\n warningsOverlap: settings.warnings.overlap,\n warningsRunningTimer: settings.warnings.runningTimer,\n assignmentGraceDaysText: String(settings.access.assignmentGraceDays),\n contributed: Object.fromEntries(\n contributedTimeTrackingSettingKeys().map((entry) => [\n entry.id,\n readTimeTrackingSettingValue(settings, entry.id),\n ]),\n ),\n }\n}\n\nexport type SettingsDraftFieldError = 'dailyHours' | 'assignmentGraceDays'\n\nfunction parseDailyHours(text: string): number | null | undefined {\n const trimmed = text.trim()\n if (trimmed.length === 0) return null\n const parsed = Number(trimmed.replace(',', '.'))\n if (!Number.isFinite(parsed) || parsed < 0 || parsed > 24) return undefined\n return parsed\n}\n\nfunction parseGraceDays(text: string): number | undefined {\n const trimmed = text.trim()\n if (trimmed.length === 0) return undefined\n const parsed = Number(trimmed)\n if (!Number.isInteger(parsed) || parsed < 0 || parsed > MAX_ASSIGNMENT_GRACE_DAYS_INPUT) return undefined\n return parsed\n}\n\n/** Which fields currently hold text that cannot be saved. Empty means the draft is valid. */\nexport function settingsDraftErrors(draft: TimeTrackingSettingsDraft): SettingsDraftFieldError[] {\n const errors: SettingsDraftFieldError[] = []\n if (parseDailyHours(draft.dailyHoursText) === undefined) errors.push('dailyHours')\n if (parseGraceDays(draft.assignmentGraceDaysText) === undefined) errors.push('assignmentGraceDays')\n return errors\n}\n\nexport function draftRounding(draft: TimeTrackingSettingsDraft): RoundingSettings {\n return { unitMinutes: draft.roundingUnitMinutes, direction: draft.roundingDirection }\n}\n\n/** The PUT body. Returns null when the draft still holds unsaveable text. */\nexport function toSettingsPayload(draft: TimeTrackingSettingsDraft): TimeTrackingSettings | null {\n const dailyHours = parseDailyHours(draft.dailyHoursText)\n const assignmentGraceDays = parseGraceDays(draft.assignmentGraceDaysText)\n if (dailyHours === undefined || assignmentGraceDays === undefined) return null\n const payload: TimeTrackingSettings = {\n rounding: draftRounding(draft),\n defaults: {\n billable: draft.defaultsBillable,\n chainStartFromPreviousEnd: draft.defaultsChainStartFromPreviousEnd,\n },\n targets: { dailyHours },\n warnings: { overlap: draft.warningsOverlap, runningTimer: draft.warningsRunningTimer },\n access: { assignmentGraceDays },\n }\n for (const entry of contributedTimeTrackingSettingKeys()) {\n const group = (payload[entry.group] ?? {}) as Record<string, unknown>\n group[entry.key] = draft.contributed[entry.id]\n payload[entry.group] = group\n }\n return payload\n}\n\nexport function isSettingsDraftDirty(\n draft: TimeTrackingSettingsDraft,\n baseline: TimeTrackingSettingsDraft,\n): boolean {\n return (\n draft.roundingUnitMinutes !== baseline.roundingUnitMinutes ||\n draft.roundingDirection !== baseline.roundingDirection ||\n draft.defaultsBillable !== baseline.defaultsBillable ||\n draft.defaultsChainStartFromPreviousEnd !== baseline.defaultsChainStartFromPreviousEnd ||\n draft.dailyHoursText.trim() !== baseline.dailyHoursText.trim() ||\n draft.warningsOverlap !== baseline.warningsOverlap ||\n draft.warningsRunningTimer !== baseline.warningsRunningTimer ||\n draft.assignmentGraceDaysText.trim() !== baseline.assignmentGraceDaysText.trim() ||\n contributedTimeTrackingSettingKeys().some(\n (entry) => !Object.is(draft.contributed[entry.id], baseline.contributed[entry.id]),\n )\n )\n}\n"],
|
|
5
|
-
"mappings": "AAgBA,SAAS,sBAAsB;AAC/B,SAAS,oBAAqE;AAC9E,SAAS,0CAA0C;
|
|
4
|
+
"sourcesContent": ["/**\n * Pure state for the module settings screen (T7.1, screen 16).\n *\n * The screen edits two numbers that are legitimately empty-able or bounded\n * (`targets.dailyHours` may be blank = no target; `access.assignmentGraceDays` is\n * an integer day count), so the draft keeps them as the TEXT the user typed and\n * converts only at the edges. Stripping a half-typed number back into the field is\n * the same defect `DurationInput` was built to avoid.\n *\n * `buildRoundingExamples` is the screen's most load-bearing function: note 1 of the\n * mockup requires the worked examples to recompute live, because rounding is a rule\n * people misread and its effect is otherwise invisible until an invoice. It calls\n * `roundMinutes` \u2014 the same function the write path calls \u2014 so a preview that\n * disagreed with reality is not expressible here.\n */\n\nimport { formatDuration } from '../time-tracking/duration'\nimport { roundMinutes, type RoundingSettings, type RoundingUnitMinutes } from '../time-tracking/rounding'\nimport { DEFAULT_SUPPORT_REFERENCE_PREFIX, contributedTimeTrackingSettingKeys } from '../time-tracking/settingKeys'\nimport { readTimeTrackingSettingValue, type TimeTrackingSettings } from '../time-tracking/settings'\n\nexport const ROUNDING_UNIT_OPTIONS: readonly RoundingUnitMinutes[] = [0, 5, 10, 15]\n\n/**\n * The four cases from the mockup: just over a unit, past the halfway point, a\n * minimum-charge case, and one that is already exact and must not move.\n */\nexport const ROUNDING_EXAMPLE_MINUTES: readonly number[] = [62, 76, 3, 120]\n\nexport type RoundingExample = {\n rawMinutes: number\n roundedMinutes: number\n rawLabel: string\n roundedLabel: string\n}\n\nexport function buildRoundingExamples(rounding: RoundingSettings): RoundingExample[] {\n return ROUNDING_EXAMPLE_MINUTES.map((rawMinutes) => {\n const roundedMinutes = roundMinutes(rawMinutes, rounding)\n return {\n rawMinutes,\n roundedMinutes,\n rawLabel: formatDuration(rawMinutes, 'clock'),\n roundedLabel: formatDuration(roundedMinutes, 'clock'),\n }\n })\n}\n\n/** `+38:15` / `\u221212:30` / `0:00`, for the impact card's delta against raw time. */\nexport function formatSignedMinutes(minutes: number): string {\n const safe = Number.isFinite(minutes) ? Math.round(minutes) : 0\n const magnitude = formatDuration(Math.abs(safe), 'clock')\n if (safe > 0) return `+${magnitude}`\n if (safe < 0) return `\u2212${magnitude}`\n return magnitude\n}\n\nexport type TimeTrackingSettingsDraft = {\n roundingUnitMinutes: RoundingUnitMinutes\n roundingDirection: RoundingSettings['direction']\n defaultsBillable: boolean\n defaultsChainStartFromPreviousEnd: boolean\n /** Free text: empty means \"no daily target\", which is a real, storable value. */\n dailyHoursText: string\n warningsOverlap: boolean\n warningsRunningTimer: boolean\n assignmentGraceDaysText: string\n /** Edited on the Support settings tab; carried through so this page's Save never resets it. */\n supportReferencePrefix: string\n /**\n * EP-42 \u2014 every contributed setting key, by its `<group>.<key>` id, carried through\n * the draft untouched so the page's own Save round-trips a contribution it knows\n * nothing about. The eight built-ins are NOT in here: the page renders those itself.\n */\n contributed: Record<string, unknown>\n}\n\n\nexport const MAX_ASSIGNMENT_GRACE_DAYS_INPUT = 365\n\nexport function toSettingsDraft(settings: TimeTrackingSettings): TimeTrackingSettingsDraft {\n return {\n roundingUnitMinutes: settings.rounding.unitMinutes,\n roundingDirection: settings.rounding.direction,\n defaultsBillable: settings.defaults.billable,\n defaultsChainStartFromPreviousEnd: settings.defaults.chainStartFromPreviousEnd,\n dailyHoursText: settings.targets.dailyHours === null ? '' : String(settings.targets.dailyHours),\n warningsOverlap: settings.warnings.overlap,\n warningsRunningTimer: settings.warnings.runningTimer,\n assignmentGraceDaysText: String(settings.access.assignmentGraceDays),\n supportReferencePrefix: settings.support?.referencePrefix ?? DEFAULT_SUPPORT_REFERENCE_PREFIX,\n contributed: Object.fromEntries(\n contributedTimeTrackingSettingKeys().map((entry) => [\n entry.id,\n readTimeTrackingSettingValue(settings, entry.id),\n ]),\n ),\n }\n}\n\nexport type SettingsDraftFieldError = 'dailyHours' | 'assignmentGraceDays'\n\nfunction parseDailyHours(text: string): number | null | undefined {\n const trimmed = text.trim()\n if (trimmed.length === 0) return null\n const parsed = Number(trimmed.replace(',', '.'))\n if (!Number.isFinite(parsed) || parsed < 0 || parsed > 24) return undefined\n return parsed\n}\n\nfunction parseGraceDays(text: string): number | undefined {\n const trimmed = text.trim()\n if (trimmed.length === 0) return undefined\n const parsed = Number(trimmed)\n if (!Number.isInteger(parsed) || parsed < 0 || parsed > MAX_ASSIGNMENT_GRACE_DAYS_INPUT) return undefined\n return parsed\n}\n\n/** Which fields currently hold text that cannot be saved. Empty means the draft is valid. */\nexport function settingsDraftErrors(draft: TimeTrackingSettingsDraft): SettingsDraftFieldError[] {\n const errors: SettingsDraftFieldError[] = []\n if (parseDailyHours(draft.dailyHoursText) === undefined) errors.push('dailyHours')\n if (parseGraceDays(draft.assignmentGraceDaysText) === undefined) errors.push('assignmentGraceDays')\n return errors\n}\n\nexport function draftRounding(draft: TimeTrackingSettingsDraft): RoundingSettings {\n return { unitMinutes: draft.roundingUnitMinutes, direction: draft.roundingDirection }\n}\n\n/** The PUT body. Returns null when the draft still holds unsaveable text. */\nexport function toSettingsPayload(draft: TimeTrackingSettingsDraft): TimeTrackingSettings | null {\n const dailyHours = parseDailyHours(draft.dailyHoursText)\n const assignmentGraceDays = parseGraceDays(draft.assignmentGraceDaysText)\n if (dailyHours === undefined || assignmentGraceDays === undefined) return null\n const payload: TimeTrackingSettings = {\n rounding: draftRounding(draft),\n defaults: {\n billable: draft.defaultsBillable,\n chainStartFromPreviousEnd: draft.defaultsChainStartFromPreviousEnd,\n },\n targets: { dailyHours },\n warnings: { overlap: draft.warningsOverlap, runningTimer: draft.warningsRunningTimer },\n access: { assignmentGraceDays },\n support: { referencePrefix: draft.supportReferencePrefix },\n }\n for (const entry of contributedTimeTrackingSettingKeys()) {\n const group = (payload[entry.group] ?? {}) as Record<string, unknown>\n group[entry.key] = draft.contributed[entry.id]\n payload[entry.group] = group\n }\n return payload\n}\n\n/**\n * A settings tab that saves the record itself moves the stored baseline under the\n * page's draft. Rebase the draft onto the new record: a field the user has not touched\n * takes the saved value, an edited field keeps the edit, so the page's Save neither\n * reverts the tab's write nor drops pending changes.\n */\nexport function rebaseSettingsDraft(\n draft: TimeTrackingSettingsDraft,\n previousBaseline: TimeTrackingSettingsDraft,\n nextBaseline: TimeTrackingSettingsDraft,\n): TimeTrackingSettingsDraft {\n const pick = <TValue>(current: TValue, previous: TValue, next: TValue): TValue =>\n Object.is(current, previous) ? next : current\n const contributed: Record<string, unknown> = { ...nextBaseline.contributed }\n for (const [id, value] of Object.entries(draft.contributed)) {\n contributed[id] = pick(value, previousBaseline.contributed[id], nextBaseline.contributed[id])\n }\n return {\n roundingUnitMinutes: pick(draft.roundingUnitMinutes, previousBaseline.roundingUnitMinutes, nextBaseline.roundingUnitMinutes),\n roundingDirection: pick(draft.roundingDirection, previousBaseline.roundingDirection, nextBaseline.roundingDirection),\n defaultsBillable: pick(draft.defaultsBillable, previousBaseline.defaultsBillable, nextBaseline.defaultsBillable),\n defaultsChainStartFromPreviousEnd: pick(\n draft.defaultsChainStartFromPreviousEnd,\n previousBaseline.defaultsChainStartFromPreviousEnd,\n nextBaseline.defaultsChainStartFromPreviousEnd,\n ),\n dailyHoursText: pick(draft.dailyHoursText, previousBaseline.dailyHoursText, nextBaseline.dailyHoursText),\n warningsOverlap: pick(draft.warningsOverlap, previousBaseline.warningsOverlap, nextBaseline.warningsOverlap),\n warningsRunningTimer: pick(draft.warningsRunningTimer, previousBaseline.warningsRunningTimer, nextBaseline.warningsRunningTimer),\n assignmentGraceDaysText: pick(\n draft.assignmentGraceDaysText,\n previousBaseline.assignmentGraceDaysText,\n nextBaseline.assignmentGraceDaysText,\n ),\n supportReferencePrefix: pick(\n draft.supportReferencePrefix,\n previousBaseline.supportReferencePrefix,\n nextBaseline.supportReferencePrefix,\n ),\n contributed,\n }\n}\n\nexport function isSettingsDraftDirty(\n draft: TimeTrackingSettingsDraft,\n baseline: TimeTrackingSettingsDraft,\n): boolean {\n return (\n draft.roundingUnitMinutes !== baseline.roundingUnitMinutes ||\n draft.roundingDirection !== baseline.roundingDirection ||\n draft.defaultsBillable !== baseline.defaultsBillable ||\n draft.defaultsChainStartFromPreviousEnd !== baseline.defaultsChainStartFromPreviousEnd ||\n draft.dailyHoursText.trim() !== baseline.dailyHoursText.trim() ||\n draft.warningsOverlap !== baseline.warningsOverlap ||\n draft.warningsRunningTimer !== baseline.warningsRunningTimer ||\n draft.assignmentGraceDaysText.trim() !== baseline.assignmentGraceDaysText.trim() ||\n contributedTimeTrackingSettingKeys().some(\n (entry) => !Object.is(draft.contributed[entry.id], baseline.contributed[entry.id]),\n )\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|