@zeniai/client-epic-state 5.1.45-betaDI3 → 5.1.45

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.
Files changed (85) hide show
  1. package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
  2. package/lib/entity/snackbar/snackbarTypes.js +0 -3
  3. package/lib/entity/task/taskPayload.d.ts +1 -3
  4. package/lib/entity/task/taskPayload.js +2 -24
  5. package/lib/entity/task/taskReducer.d.ts +1 -8
  6. package/lib/entity/task/taskReducer.js +4 -29
  7. package/lib/entity/task/taskState.d.ts +0 -2
  8. package/lib/entity/tenant/epic/saveExternalConnectionEpic.d.ts +1 -2
  9. package/lib/entity/tenant/epic/saveExternalConnectionEpic.js +0 -17
  10. package/lib/epic.d.ts +3 -6
  11. package/lib/epic.js +3 -6
  12. package/lib/esm/entity/snackbar/snackbarTypes.js +0 -3
  13. package/lib/esm/entity/task/taskPayload.js +2 -24
  14. package/lib/esm/entity/task/taskReducer.js +3 -28
  15. package/lib/esm/entity/tenant/epic/saveExternalConnectionEpic.js +0 -17
  16. package/lib/esm/epic.js +3 -6
  17. package/lib/esm/index.js +8 -8
  18. package/lib/esm/view/aiCfoView/aiCfoViewReducer.js +1 -31
  19. package/lib/esm/view/aiCfoView/epics/fetchSkillsEpic.js +8 -33
  20. package/lib/esm/view/companyTaskManagerView/epics/fetchCompanyTaskManagerViewEpic.js +0 -3
  21. package/lib/esm/view/companyTaskManagerView/epics/fetchTaskManagerMetricsEpic.js +1 -7
  22. package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +0 -7
  23. package/lib/esm/view/taskManager/taskDetailView/epics/initializeTaskToLocalStoreEpic.js +0 -1
  24. package/lib/esm/view/taskManager/taskDetailView/epics/saveTaskDetailEpic.js +9 -70
  25. package/lib/esm/view/taskManager/taskDetailView/taskDetailReducer.js +2 -56
  26. package/lib/esm/view/taskManager/taskDetailView/taskDetailSelector.js +9 -35
  27. package/lib/esm/view/taskManager/taskListView/epics/fetchTaskListEpic.js +1 -3
  28. package/lib/esm/view/taskManager/taskListView/taskList.js +0 -1
  29. package/lib/esm/view/taskManager/taskListView/taskListReducer.js +18 -310
  30. package/lib/esm/view/taskManager/taskListView/taskListSelector.js +0 -1
  31. package/lib/esm/view/taskManager/taskListView/taskListViewHelpers.js +0 -58
  32. package/lib/esm/view/transactionDetail/epics/saveTransactionHelper.js +0 -22
  33. package/lib/esm/view/transactionDetail/epics/updateTransactionDetailEpic.js +3 -75
  34. package/lib/esm/view/transactionDetail/journalEntryLinesViewModel.js +0 -58
  35. package/lib/esm/view/transactionDetail/transactionDetailLocalDataHelper.js +8 -10
  36. package/lib/esm/view/transactionDetail/transactionDetailReducer.js +1 -66
  37. package/lib/index.d.ts +11 -12
  38. package/lib/index.js +28 -37
  39. package/lib/view/aiCfoView/aiCfoViewPayload.d.ts +0 -10
  40. package/lib/view/aiCfoView/aiCfoViewReducer.d.ts +2 -7
  41. package/lib/view/aiCfoView/aiCfoViewReducer.js +2 -32
  42. package/lib/view/aiCfoView/aiCfoViewState.d.ts +1 -5
  43. package/lib/view/aiCfoView/epics/fetchSkillsEpic.js +8 -33
  44. package/lib/view/companyTaskManagerView/epics/fetchCompanyTaskManagerViewEpic.js +0 -3
  45. package/lib/view/companyTaskManagerView/epics/fetchTaskManagerMetricsEpic.js +1 -7
  46. package/lib/view/companyView/types/cockpitTypes.d.ts +0 -3
  47. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
  48. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +0 -7
  49. package/lib/view/taskManager/taskDetailView/epics/initializeTaskToLocalStoreEpic.js +0 -1
  50. package/lib/view/taskManager/taskDetailView/epics/saveTaskDetailEpic.d.ts +2 -2
  51. package/lib/view/taskManager/taskDetailView/epics/saveTaskDetailEpic.js +8 -69
  52. package/lib/view/taskManager/taskDetailView/taskDetail.d.ts +0 -15
  53. package/lib/view/taskManager/taskDetailView/taskDetailReducer.d.ts +2 -14
  54. package/lib/view/taskManager/taskDetailView/taskDetailReducer.js +3 -57
  55. package/lib/view/taskManager/taskDetailView/taskDetailSelector.d.ts +1 -3
  56. package/lib/view/taskManager/taskDetailView/taskDetailSelector.js +7 -33
  57. package/lib/view/taskManager/taskListView/epics/fetchTaskListEpic.js +1 -3
  58. package/lib/view/taskManager/taskListView/taskList.d.ts +1 -1
  59. package/lib/view/taskManager/taskListView/taskList.js +0 -1
  60. package/lib/view/taskManager/taskListView/taskListPayload.d.ts +0 -4
  61. package/lib/view/taskManager/taskListView/taskListReducer.d.ts +2 -6
  62. package/lib/view/taskManager/taskListView/taskListReducer.js +19 -311
  63. package/lib/view/taskManager/taskListView/taskListSelector.js +0 -1
  64. package/lib/view/taskManager/taskListView/taskListViewHelpers.d.ts +0 -11
  65. package/lib/view/taskManager/taskListView/taskListViewHelpers.js +1 -60
  66. package/lib/view/transactionDetail/epics/saveTransactionHelper.js +0 -22
  67. package/lib/view/transactionDetail/epics/updateTransactionDetailEpic.d.ts +1 -5
  68. package/lib/view/transactionDetail/epics/updateTransactionDetailEpic.js +4 -77
  69. package/lib/view/transactionDetail/journalEntryLinesViewModel.d.ts +0 -27
  70. package/lib/view/transactionDetail/journalEntryLinesViewModel.js +1 -61
  71. package/lib/view/transactionDetail/transactionDetailLocalDataHelper.js +8 -10
  72. package/lib/view/transactionDetail/transactionDetailReducer.d.ts +2 -11
  73. package/lib/view/transactionDetail/transactionDetailReducer.js +2 -67
  74. package/lib/view/transactionDetail/transactionDetailState.d.ts +1 -8
  75. package/lib/view/transactionDetail/transactionDetailTypes.d.ts +0 -2
  76. package/package.json +1 -1
  77. package/lib/esm/view/aiCfoView/epics/submitFeedbackEpic.js +0 -39
  78. package/lib/esm/view/taskManager/taskDetailView/epics/createSubTaskEpic.js +0 -96
  79. package/lib/esm/view/taskManager/taskDetailView/epics/fetchSubTasksEpic.js +0 -53
  80. package/lib/view/aiCfoView/epics/submitFeedbackEpic.d.ts +0 -13
  81. package/lib/view/aiCfoView/epics/submitFeedbackEpic.js +0 -43
  82. package/lib/view/taskManager/taskDetailView/epics/createSubTaskEpic.d.ts +0 -9
  83. package/lib/view/taskManager/taskDetailView/epics/createSubTaskEpic.js +0 -100
  84. package/lib/view/taskManager/taskDetailView/epics/fetchSubTasksEpic.d.ts +0 -8
  85. package/lib/view/taskManager/taskDetailView/epics/fetchSubTasksEpic.js +0 -57
@@ -1,5 +1,4 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
- import { updateTasks } from '../../../entity/task/taskReducer';
3
2
  import { toPriorityCodeType, toTaskStatusCodeType, } from '../../../entity/task/taskState';
4
3
  import { date, dateNow } from '../../../zeniDayJS';
5
4
  import { toDueDateGroupKeyType, } from './taskList';
@@ -43,7 +42,6 @@ export const initialState = {
43
42
  currentTab: 'live',
44
43
  byTab: {
45
44
  live: { ...initialTabData },
46
- completed: { ...initialTabData },
47
45
  archived: { ...initialTabData },
48
46
  deleted: { ...initialTabData },
49
47
  snoozed: { ...initialTabData },
@@ -65,12 +63,14 @@ export const initialState = {
65
63
  },
66
64
  filters: {
67
65
  categoryCombinationOperator: 'AND',
68
- // Default to no filters. The Completed tab now owns the "hide resolved
69
- // tasks from the live list" responsibility via the byTab split in
70
- // updateTaskList / updateTaskFromListView / the updateTasks
71
- // extraReducer. Keeping the historical `status != resolved` filter
72
- // here would empty out the Completed tab.
73
- categories: [],
66
+ categories: [
67
+ {
68
+ field: 'status',
69
+ matchingOperator: 'not_equal',
70
+ values: ['resolved'],
71
+ valuesCombinationOperator: 'ANY',
72
+ },
73
+ ],
74
74
  },
75
75
  hasValidState() {
76
76
  return this.fetchState == 'Completed';
@@ -102,37 +102,21 @@ const taskList = createSlice({
102
102
  draft.error = undefined;
103
103
  },
104
104
  updateTaskList(draft, action) {
105
- const { data, archived, completed, deleted, snoozed } = action.payload;
105
+ const { data, archived, deleted, snoozed } = action.payload;
106
106
  draft.fetchState = 'Completed';
107
107
  draft.error = undefined;
108
- // Build the nested-subtask set from the UNION of every bucket in
109
- // this payload — not per-bucket. A resolved subtask can arrive in
110
- // `completed` while its parent (which claims it via `subtasks`)
111
- // is still in `data`; a per-bucket scan would miss the cross-tab
112
- // claim and surface the subtask as a phantom top-level row in
113
- // the Completed tab.
114
- const nestedSubtaskIds = collectNestedSubtaskIds([
115
- data,
116
- completed,
117
- archived,
118
- deleted,
119
- snoozed,
120
- ]);
121
108
  // Group live tasks
122
- const liveGrouped = getGroupedTaskIds(data, nestedSubtaskIds);
109
+ const liveGrouped = getGroupedTaskIds(data);
123
110
  draft.byTab.live = toTabData(liveGrouped);
124
111
  // Group other tabs if provided
125
- if (completed != null) {
126
- draft.byTab.completed = toTabData(getGroupedTaskIds(completed, nestedSubtaskIds));
127
- }
128
112
  if (archived != null) {
129
- draft.byTab.archived = toTabData(getGroupedTaskIds(archived, nestedSubtaskIds));
113
+ draft.byTab.archived = toTabData(getGroupedTaskIds(archived));
130
114
  }
131
115
  if (deleted != null) {
132
- draft.byTab.deleted = toTabData(getGroupedTaskIds(deleted, nestedSubtaskIds));
116
+ draft.byTab.deleted = toTabData(getGroupedTaskIds(deleted));
133
117
  }
134
118
  if (snoozed != null) {
135
- draft.byTab.snoozed = toTabData(getGroupedTaskIds(snoozed, nestedSubtaskIds));
119
+ draft.byTab.snoozed = toTabData(getGroupedTaskIds(snoozed));
136
120
  }
137
121
  // Copy active tab data to top-level fields
138
122
  const activeTabData = draft.byTab[draft.currentTab];
@@ -370,26 +354,9 @@ const taskList = createSlice({
370
354
  draft.taskIdsByAssignees[assigneeKey] = draft.taskIdsByAssignees[assigneeKey].filter((taskId) => taskId !== currentTaskState.id);
371
355
  draft.taskIdsByTags[tagsKey] = draft.taskIdsByTags[tagsKey].filter((taskId) => taskId !== currentTaskState.id);
372
356
  draft.taskIdsByDueDate[dueDateKey] = draft.taskIdsByDueDate[dueDateKey].filter((taskId) => taskId !== currentTaskState.id);
373
- // Remove from EVERY previous group bucket the task lived in, not
374
- // just the destination bucket. Otherwise a detail-page save that
375
- // moves a task from group A to group B leaves the row listed
376
- // under both A and B until refetch.
377
- const previousGroupIds = currentTaskState.taskGroupIds ?? [];
378
- const allGroupIdsToClean = new Set([
379
- ...previousGroupIds,
380
- taskGroupId,
381
- ]);
382
- allGroupIdsToClean.forEach((gid) => {
383
- if (draft.taskIdsByGroupsIds[gid] != null) {
384
- draft.taskIdsByGroupsIds[gid] = draft.taskIdsByGroupsIds[gid].filter((id) => id !== currentTaskState.id);
385
- }
386
- });
357
+ draft.taskIdsByGroupsIds[taskGroupId] = draft.taskIdsByGroupsIds[taskGroupId].filter((taskId) => taskId !== currentTaskState.id);
387
358
  }
388
359
  doUpdateTaskList(draft, task, taskGroupId);
389
- // Mirror top-level grouping into the active tab's byTab snapshot,
390
- // otherwise the next updateTaskListTab call copies stale byTab data
391
- // back over this optimistic update.
392
- syncTopLevelToActiveTab(draft);
393
360
  },
394
361
  updateTaskListOnCreateTaskFromTemplateSuccess(draft, action) {
395
362
  const { tasks } = action.payload;
@@ -413,7 +380,6 @@ const taskList = createSlice({
413
380
  const taskId = task.id;
414
381
  draft.updateTasksFetchState.fetchState = 'In-Progress';
415
382
  draft.updateTasksFetchState.error = undefined;
416
- let didMigrateTab = false;
417
383
  if (updates.priority != null) {
418
384
  const prevPriorityCode = toPriorityCodeType(task.priority.code);
419
385
  draft.taskIdsByPriority[prevPriorityCode] = draft.taskIdsByPriority[prevPriorityCode].filter((id) => id != taskId);
@@ -425,32 +391,6 @@ const taskList = createSlice({
425
391
  draft.taskIdsByStatus[prevStatusCode] = draft.taskIdsByStatus[prevStatusCode].filter((id) => id != taskId);
426
392
  const newStatusCode = toTaskStatusCodeType(updates.status.code);
427
393
  draft.taskIdsByStatus[newStatusCode].push(taskId);
428
- // Optimistically re-bucket between the live and completed tabs
429
- // when status transitions in/out of "resolved", so the task
430
- // moves to the Completed tab without waiting for the next
431
- // fetch. Mirrors backend split in task_handler.py: only
432
- // `resolved` counts as completed; `done` stays Active.
433
- //
434
- // "Top-level" here means: currently rendered as a standalone
435
- // row in the live or completed tab (i.e. present in
436
- // `byTab.<tab>.taskIds`). We use presence in byTab — NOT a
437
- // `parent_task_id == null` check — so orphan subtasks (parent
438
- // hidden from the customer's payload, so we render them flat)
439
- // ALSO migrate between tabs on status flip. This matches the
440
- // criterion `getGroupedTaskIds` uses when it builds byTab from
441
- // the initial fetch — a task is nested iff a visible parent's
442
- // `subtasks` array claims it, otherwise it's top-level here.
443
- const isTopLevelTask = draft.byTab.live.taskIds.includes(taskId) ||
444
- draft.byTab.completed.taskIds.includes(taskId);
445
- const wasCompleted = prevStatusCode === 'resolved';
446
- const isCompleted = newStatusCode === 'resolved';
447
- if (isTopLevelTask && wasCompleted !== isCompleted) {
448
- const from = isCompleted ? 'live' : 'completed';
449
- const to = isCompleted ? 'completed' : 'live';
450
- const updatedTask = { ...task, ...updates };
451
- migrateTaskBetweenTabs(draft, taskId, updatedTask, from, to);
452
- didMigrateTab = true;
453
- }
454
394
  }
455
395
  if (updates.assignees != null) {
456
396
  const prevGroupId = getAssigneesGroupKey(task.assignees);
@@ -490,18 +430,7 @@ const taskList = createSlice({
490
430
  };
491
431
  }
492
432
  }
493
- if (didMigrateTab) {
494
- // The migration moved the task out of the source byTab and into
495
- // the destination byTab. The active tab's grouping arrays are
496
- // now authoritative — mirror them back to the top-level so the
497
- // UI sees the task gone from Live (or freshly arrived in
498
- // Completed). Inverse of syncTopLevelToActiveTab, used only on
499
- // this path.
500
- copyTabDataToTopLevel(draft, draft.byTab[draft.currentTab]);
501
- }
502
- else {
503
- syncTopLevelToActiveTab(draft);
504
- }
433
+ syncTopLevelToActiveTab(draft);
505
434
  },
506
435
  updateTasksListOnUpdateTaskSuccess(draft) {
507
436
  draft.updateTasksFetchState.fetchState = 'Completed';
@@ -514,83 +443,12 @@ const taskList = createSlice({
514
443
  removeTaskFromList(draft, action) {
515
444
  removeTaskIds(draft, [action.payload]);
516
445
  },
517
- /**
518
- * Remove a task id from a single group bucket without touching tab
519
- * membership or other groupings. Used when the user clears a task's
520
- * group on the Detail page so the row stops appearing under the old
521
- * group while saveTaskDetailEpic skips the standard list re-insert.
522
- */
523
- removeTaskFromGroupBucket(draft, action) {
524
- const { taskId, taskGroupId } = action.payload;
525
- if (draft.taskIdsByGroupsIds[taskGroupId] != null) {
526
- draft.taskIdsByGroupsIds[taskGroupId] = draft.taskIdsByGroupsIds[taskGroupId].filter((id) => id !== taskId);
527
- }
528
- // Mirror to the active tab's grouping so the UI re-renders.
529
- const activeTab = draft.byTab[draft.currentTab];
530
- if (activeTab.taskIdsByGroupsIds[taskGroupId] != null) {
531
- activeTab.taskIdsByGroupsIds[taskGroupId] =
532
- activeTab.taskIdsByGroupsIds[taskGroupId].filter((id) => id !== taskId);
533
- }
534
- },
535
446
  clearTaskList(draft) {
536
447
  Object.assign(draft, initialState);
537
448
  },
538
449
  },
539
- extraReducers: (builder) => {
540
- // When the entity store receives updated tasks (e.g. from task detail
541
- // save, snooze/archive/unarchive, etc.), re-bucket top-level tasks
542
- // between the live and completed tabs based on the new status. Mirrors
543
- // the backend split in task_handler.py: only `resolved` counts as
544
- // completed.
545
- //
546
- // "Top-level" here means: currently rendered as a standalone row in
547
- // the live or completed tab (i.e. present in
548
- // `byTab.<tab>.taskIds`). Tasks that are nested under a parent's
549
- // expanded subtask row live outside byTab.taskIds and are silently
550
- // skipped by the "in-bucket" check below. This uses the same
551
- // criterion as `getGroupedTaskIds` — a task is nested iff a visible
552
- // parent's `subtasks` array claims it, otherwise it's top-level —
553
- // so orphan subtasks (parent hidden from the customer's payload)
554
- // migrate correctly on status flip via pusher / detail-save.
555
- builder.addCase(updateTasks, (draft, action) => {
556
- action.payload.forEach((taskPayload) => {
557
- const taskId = taskPayload.task_id;
558
- // `TaskPayload.status` is required per the type — every producer
559
- // in this repo sends it, and `mapTaskPayloadToTask` dereferences
560
- // it unguarded. The optional chain + null guard are defensive
561
- // only, in case a downstream consumer (web-app-ui / cockpit-ui)
562
- // ever dispatches `updateTasks` with a partial payload; in that
563
- // case we treat missing status as "no status change" and skip
564
- // re-bucketing so we don't migrate a Completed task back into
565
- // the Live tab.
566
- const statusCode = taskPayload.status?.code;
567
- if (statusCode == null) {
568
- return;
569
- }
570
- const isCompleted = statusCode === 'resolved';
571
- const targetTab = isCompleted ? 'completed' : 'live';
572
- const otherTab = isCompleted ? 'live' : 'completed';
573
- // Skip if the task isn't currently in either of these buckets
574
- // (archived/snoozed/deleted are managed by their own actions).
575
- const inLive = draft.byTab.live.taskIds.includes(taskId);
576
- const inCompleted = draft.byTab.completed.taskIds.includes(taskId);
577
- if (!inLive && !inCompleted) {
578
- return;
579
- }
580
- if (draft.byTab[otherTab].taskIds.includes(taskId)) {
581
- migrateTaskPayloadBetweenTabs(draft, taskId, taskPayload, otherTab, targetTab);
582
- }
583
- else if (!draft.byTab[targetTab].taskIds.includes(taskId)) {
584
- draft.byTab[targetTab].taskIds.push(taskId);
585
- }
586
- });
587
- // Re-sync the top-level taskIds / grouping arrays for whichever
588
- // tab the user is currently looking at.
589
- copyTabDataToTopLevel(draft, draft.byTab[draft.currentTab]);
590
- });
591
- },
592
450
  });
593
- export const { fetchTaskListPage, fetchTaskList, updateTaskList, updateTaskListTab, updateTaskListFetchStatus, updateTaskListSearchText, updateTaskListUIState, updateTaskFilters, updateTaskListOnNewGroupCreationSuccess, bulkUpdateTaskList, updateTasksListOnBulkUpdateTasksSuccess, updateTasksListOnBulkUpdateTasksFailure, initiateTaskListLocalData, updateTaskListLocalData, updateTaskListOnTaskGroupDeletion, dragNDropTasks, updateTaskListOnNewTaskCreationSuccess, updateTaskListOnTaskGroupNameUpdate, updateTaskFromListView, updateTasksListOnUpdateTaskSuccess, updateTasksListOnUpdateTaskFailure, updateTaskListOnCreateTaskFromTemplateSuccess, removeTaskFromList, removeTaskFromGroupBucket, clearTaskList, } = taskList.actions;
451
+ export const { fetchTaskListPage, fetchTaskList, updateTaskList, updateTaskListTab, updateTaskListFetchStatus, updateTaskListSearchText, updateTaskListUIState, updateTaskFilters, updateTaskListOnNewGroupCreationSuccess, bulkUpdateTaskList, updateTasksListOnBulkUpdateTasksSuccess, updateTasksListOnBulkUpdateTasksFailure, initiateTaskListLocalData, updateTaskListLocalData, updateTaskListOnTaskGroupDeletion, dragNDropTasks, updateTaskListOnNewTaskCreationSuccess, updateTaskListOnTaskGroupNameUpdate, updateTaskFromListView, updateTasksListOnUpdateTaskSuccess, updateTasksListOnUpdateTaskFailure, updateTaskListOnCreateTaskFromTemplateSuccess, removeTaskFromList, clearTaskList, } = taskList.actions;
594
452
  export default taskList.reducer;
595
453
  /**
596
454
  * helpers
@@ -697,115 +555,6 @@ const copyTabDataToTopLevel = (draft, tabData) => {
697
555
  draft.taskIdsByDueDate = tabData.taskIdsByDueDate;
698
556
  draft.taskIdsByTags = tabData.taskIdsByTags;
699
557
  };
700
- /**
701
- * Migrate a parent task between two `byTab` buckets (live ↔ completed)
702
- * when its status flips into / out of `resolved`. The destination tab
703
- * picks up the task in EVERY grouping dimension (status, priority,
704
- * group id, assignees, dueDate, tags) using the UPDATED task as the
705
- * source-of-truth for the new bucket keys. Otherwise the Completed tab
706
- * shows the task in the right tab-level list but the grouped views
707
- * (Group by Status, Group by Priority, etc.) miss it until the next
708
- * full fetch.
709
- */
710
- const migrateTaskBetweenTabs = (draft, taskId, updatedTask, from, to) => {
711
- const fromTab = draft.byTab[from];
712
- const toTab = draft.byTab[to];
713
- // Source tab: drop the task from every grouping dimension.
714
- removeIdsFromTabData(fromTab, [taskId]);
715
- // Destination tab: add the task to taskIds (deduped) plus the
716
- // appropriate bucket inside each grouping dimension based on the
717
- // updated task.
718
- if (!toTab.taskIds.includes(taskId)) {
719
- toTab.taskIds.push(taskId);
720
- }
721
- const priorityKey = toPriorityCodeType(updatedTask.priority.code);
722
- if (!toTab.taskIdsByPriority[priorityKey].includes(taskId)) {
723
- toTab.taskIdsByPriority[priorityKey].push(taskId);
724
- }
725
- const statusKey = toTaskStatusCodeType(updatedTask.status.code);
726
- if (!toTab.taskIdsByStatus[statusKey].includes(taskId)) {
727
- toTab.taskIdsByStatus[statusKey].push(taskId);
728
- }
729
- const dueDateKey = updatedTask.dueDate != null
730
- ? getDueDateGroupKey(updatedTask.dueDate.toString())
731
- : 'upcoming';
732
- if (!toTab.taskIdsByDueDate[dueDateKey].includes(taskId)) {
733
- toTab.taskIdsByDueDate[dueDateKey].push(taskId);
734
- }
735
- const assigneeKey = getAssigneesGroupKey(updatedTask.assignees);
736
- if (toTab.taskIdsByAssignees[assigneeKey] == null) {
737
- toTab.taskIdsByAssignees[assigneeKey] = [taskId];
738
- }
739
- else if (!toTab.taskIdsByAssignees[assigneeKey].includes(taskId)) {
740
- toTab.taskIdsByAssignees[assigneeKey].push(taskId);
741
- }
742
- const tagIds = [...updatedTask.tagIds].sort();
743
- const tagsKey = tagIds.join(',');
744
- if (toTab.taskIdsByTags[tagsKey] == null) {
745
- toTab.taskIdsByTags[tagsKey] = [taskId];
746
- }
747
- else if (!toTab.taskIdsByTags[tagsKey].includes(taskId)) {
748
- toTab.taskIdsByTags[tagsKey].push(taskId);
749
- }
750
- (updatedTask.taskGroupIds ?? []).forEach((groupId) => {
751
- if (toTab.taskIdsByGroupsIds[groupId] == null) {
752
- toTab.taskIdsByGroupsIds[groupId] = [taskId];
753
- }
754
- else if (!toTab.taskIdsByGroupsIds[groupId].includes(taskId)) {
755
- toTab.taskIdsByGroupsIds[groupId].push(taskId);
756
- }
757
- });
758
- };
759
- /**
760
- * `updateTasks` extraReducer variant — same migration semantics but
761
- * keyed off the raw `TaskPayload` instead of the rich `Task` entity.
762
- */
763
- const migrateTaskPayloadBetweenTabs = (draft, taskId, payload, from, to) => {
764
- const fromTab = draft.byTab[from];
765
- const toTab = draft.byTab[to];
766
- removeIdsFromTabData(fromTab, [taskId]);
767
- if (!toTab.taskIds.includes(taskId)) {
768
- toTab.taskIds.push(taskId);
769
- }
770
- if (payload.priority?.code != null) {
771
- const priorityKey = toPriorityCodeType(payload.priority.code);
772
- if (!toTab.taskIdsByPriority[priorityKey].includes(taskId)) {
773
- toTab.taskIdsByPriority[priorityKey].push(taskId);
774
- }
775
- }
776
- if (payload.status?.code != null) {
777
- const statusKey = toTaskStatusCodeType(payload.status.code);
778
- if (!toTab.taskIdsByStatus[statusKey].includes(taskId)) {
779
- toTab.taskIdsByStatus[statusKey].push(taskId);
780
- }
781
- }
782
- const dueDateKey = getDueDateGroupKey(payload.due_date);
783
- if (!toTab.taskIdsByDueDate[dueDateKey].includes(taskId)) {
784
- toTab.taskIdsByDueDate[dueDateKey].push(taskId);
785
- }
786
- const assigneeKey = getAssigneesGroupKey(payload.assignees ?? []);
787
- if (toTab.taskIdsByAssignees[assigneeKey] == null) {
788
- toTab.taskIdsByAssignees[assigneeKey] = [taskId];
789
- }
790
- else if (!toTab.taskIdsByAssignees[assigneeKey].includes(taskId)) {
791
- toTab.taskIdsByAssignees[assigneeKey].push(taskId);
792
- }
793
- const tagsKey = getTagsGroupKey(payload.tags ?? []);
794
- if (toTab.taskIdsByTags[tagsKey] == null) {
795
- toTab.taskIdsByTags[tagsKey] = [taskId];
796
- }
797
- else if (!toTab.taskIdsByTags[tagsKey].includes(taskId)) {
798
- toTab.taskIdsByTags[tagsKey].push(taskId);
799
- }
800
- (payload.task_group_ids ?? []).forEach((groupId) => {
801
- if (toTab.taskIdsByGroupsIds[groupId] == null) {
802
- toTab.taskIdsByGroupsIds[groupId] = [taskId];
803
- }
804
- else if (!toTab.taskIdsByGroupsIds[groupId].includes(taskId)) {
805
- toTab.taskIdsByGroupsIds[groupId].push(taskId);
806
- }
807
- });
808
- };
809
558
  const removeIdsFromTabData = (tabData, taskIds) => {
810
559
  const removedSet = new Set(taskIds);
811
560
  const isRemoved = (id) => removedSet.has(id);
@@ -833,39 +582,7 @@ const removeTaskIds = (draft, taskIds) => {
833
582
  removeIdsFromTabData(draft, taskIds);
834
583
  removeIdsFromTabData(draft.byTab[draft.currentTab], taskIds);
835
584
  };
836
- // Collect the union of subtask ids claimed by any parent across every
837
- // bucket in the payload. See the callsite in `updateTaskList` for why
838
- // this must be cross-bucket rather than per-bucket.
839
- const collectNestedSubtaskIds = (buckets) => {
840
- const nestedSubtaskIds = new Set();
841
- buckets.forEach((bucket) => {
842
- if (bucket == null) {
843
- return;
844
- }
845
- bucket.forEach((task) => {
846
- (task.subtasks ?? []).forEach((subtaskId) => nestedSubtaskIds.add(subtaskId));
847
- });
848
- });
849
- return nestedSubtaskIds;
850
- };
851
- // A task is a "nested subtask" (hidden from the flat top-level list +
852
- // dropped from the tab count) iff a parent in the same payload claims
853
- // it via its `subtasks: [...]` array. Match this criterion exactly to
854
- // WC's `allSubtaskIds` union in TaskManagementPage.tsx — otherwise
855
- // the tab count and the visible top-level row count disagree
856
- // whenever the BE emits `parent_task_id` on a subtask but omits it
857
- // from the parent's `subtasks` array (a real inconsistency this UI
858
- // sees today).
859
- //
860
- // Using `parent_task_id` here would sometimes drop a task from the
861
- // count that WC still renders as a flat top-level row — that's the
862
- // bug we're fixing.
863
- //
864
- // `nestedSubtaskIds` is passed in from the caller and covers subtasks
865
- // claimed by parents in ANY bucket (live/completed/archived/deleted/
866
- // snoozed), so a subtask resolved into `completed` while its parent
867
- // stays in `data` is still correctly recognized as nested.
868
- const getGroupedTaskIds = (tasks, nestedSubtaskIds) => {
585
+ const getGroupedTaskIds = (tasks) => {
869
586
  const taskIds = [];
870
587
  const taskIdsByGroupIds = {};
871
588
  const taskIdsByAssignees = {};
@@ -880,21 +597,12 @@ const getGroupedTaskIds = (tasks, nestedSubtaskIds) => {
880
597
  };
881
598
  const taskIdsByTags = {};
882
599
  tasks.forEach((task) => {
883
- const isNestedSubtask = nestedSubtaskIds.has(task.task_id);
884
- // Subtasks stay in every groupBy array so the UI can render them
885
- // nested under their parent (WC's TaskManagementPage builds
886
- // `subtasksByParentId` from `group.tasks` and then filters them
887
- // out of top-level rows via `filterOutSubtasks`). Only the flat
888
- // top-level `taskIds` array — which drives the Active/Completed/…
889
- // tab counts — must exclude nested subtasks.
600
+ taskIds.push(task.task_id);
890
601
  const priorityKey = toPriorityCodeType(task.priority.code);
891
602
  const statusKey = toTaskStatusCodeType(task.status.code);
892
603
  const assigneeKey = getAssigneesGroupKey(task.assignees);
893
604
  const dueDateKey = getDueDateGroupKey(task.due_date);
894
605
  const tagsKey = getTagsGroupKey(task.tags);
895
- if (!isNestedSubtask) {
896
- taskIds.push(task.task_id);
897
- }
898
606
  taskIdsGroupedByPriority[priorityKey] = [
899
607
  ...taskIdsGroupedByPriority[priorityKey],
900
608
  task.task_id,
@@ -334,7 +334,6 @@ export const getAllTasks = createSelector((state) => state.taskListState, (state
334
334
  const taskGroupTemplates = getTaskGroupTemplateByIds(taskGroupTemplateState, taskGroupTemplateViewState.taskGroupTemplateIds);
335
335
  const tabCounts = {
336
336
  live: taskListState.byTab.live.taskIds.length,
337
- completed: taskListState.byTab.completed.taskIds.length,
338
337
  archived: taskListState.byTab.archived.taskIds.length,
339
338
  deleted: taskListState.byTab.deleted.taskIds.length,
340
339
  snoozed: taskListState.byTab.snoozed.taskIds.length,
@@ -48,61 +48,3 @@ export const getTaskUpdates = (groupByKey, groupId) => {
48
48
  taskGroupIds: [groupId],
49
49
  };
50
50
  };
51
- /**
52
- * Sort a subtask list by one of the sortable columns. Pure — no state
53
- * dependency. `statusOrder` is derived from `allTaskStatus` so any new
54
- * status added upstream slots in by its enum position instead of
55
- * falling into a catch-all bucket.
56
- */
57
- export const sortSubtasks = (subtasks, sortKey, sortOrder, userList, tagList) => {
58
- if (sortKey == null) {
59
- return subtasks;
60
- }
61
- const statusOrder = allTaskStatus.reduce((acc, status, index) => {
62
- acc[status.code] = index;
63
- return acc;
64
- }, {});
65
- const unknownStatusRank = allTaskStatus.length;
66
- const cmpStr = (a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' });
67
- const keyFn = (task) => {
68
- if (sortKey === 'name') {
69
- return task.name ?? '';
70
- }
71
- if (sortKey === 'status') {
72
- return statusOrder[task.status.code] ?? unknownStatusRank;
73
- }
74
- if (sortKey === 'assignee') {
75
- const firstId = task.assignees?.[0];
76
- if (firstId == null) {
77
- return '';
78
- }
79
- const user = userList.find((candidate) => candidate.userId === firstId);
80
- return user
81
- ? `${user.firstName ?? ''} ${user.lastName ?? ''}`.trim()
82
- : '';
83
- }
84
- if (sortKey === 'tag') {
85
- const firstTagId = task.tagIds?.[0];
86
- if (firstTagId == null) {
87
- return '';
88
- }
89
- const tag = tagList.find((candidate) => candidate.tagId === firstTagId);
90
- return tag?.name ?? '';
91
- }
92
- return '';
93
- };
94
- const copy = [...subtasks];
95
- copy.sort((a, b) => {
96
- const av = keyFn(a);
97
- const bv = keyFn(b);
98
- let cmp;
99
- if (typeof av === 'number' && typeof bv === 'number') {
100
- cmp = av - bv;
101
- }
102
- else {
103
- cmp = cmpStr(String(av), String(bv));
104
- }
105
- return sortOrder === 'asc' ? cmp : -cmp;
106
- });
107
- return copy;
108
- };
@@ -45,15 +45,7 @@ export const toTransactionUpdatesFromTransactionDetailLocalData = (transactionDe
45
45
  let lastUpdatedCategory;
46
46
  let lastUpdatedClass;
47
47
  // single recommendation to just pick the last recommendation in the list of lines
48
- const existingLineIds = new Set(transaction.lines?.map((l) => l.id) ?? []);
49
- const deletedLineIds = new Set((transactionDetailLocalData.deletedLineIds ?? []).filter((id) => existingLineIds.has(id)));
50
- // Collect new lines — in lineItemById but not yet in transaction.lines.
51
- const addedLines = Object.values(transactionDetailLocalData.lineItemById).filter((item) => !existingLineIds.has(item.id));
52
48
  transaction.lines?.forEach((line) => {
53
- // Skip lines the user deleted in edit mode.
54
- if (deletedLineIds.has(line.id)) {
55
- return;
56
- }
57
49
  const lineItem = transactionDetailLocalData.lineItemById[line.id];
58
50
  if (line.type === 'transaction_with_account_and_class_line' ||
59
51
  line.type === 'transaction_with_product_or_service_line' ||
@@ -64,7 +56,6 @@ export const toTransactionUpdatesFromTransactionDetailLocalData = (transactionDe
64
56
  const entityDetails = getEntityDetails(selectedEntity, lineItem, transactionDetailLocalData.vendorUpdates, transactionDetailLocalData.customerUpdates);
65
57
  updatesByLineId[line.id] = {
66
58
  lineDescription: lineItem.lineDescription,
67
- amount: lineItem.amount,
68
59
  class: lineItem.class,
69
60
  account: lineItem.account,
70
61
  // Project is at the transaction level for invoice/credit_memo; omit from lines.
@@ -86,17 +77,6 @@ export const toTransactionUpdatesFromTransactionDetailLocalData = (transactionDe
86
77
  currentSelectedEntityLine.type === 'vendor'
87
78
  ? lineItem.vendor
88
79
  : undefined,
89
- // Carry the JE debit/credit side so a split edit that flips an
90
- // existing line's posting side persists through the line patch.
91
- ...(line.type === 'journal_entry_transaction_line'
92
- ? {
93
- postingType: lineItem.postingType,
94
- postingTypeName: lineItem.postingType != null
95
- ? lineItem.postingType.charAt(0).toUpperCase() +
96
- lineItem.postingType.slice(1)
97
- : lineItem.postingTypeName,
98
- }
99
- : {}),
100
80
  };
101
81
  }
102
82
  let hasCategoryChanged = false;
@@ -183,8 +163,6 @@ export const toTransactionUpdatesFromTransactionDetailLocalData = (transactionDe
183
163
  and all the line items are NOT uncategorized*/
184
164
  markAsMiscatUpdates: isTransactionMisCategorized,
185
165
  anyBulkUpdates: isBulkUpdate,
186
- addedLines: addedLines.length > 0 ? addedLines : undefined,
187
- deletedLineIds: deletedLineIds.size > 0 ? [...deletedLineIds] : undefined,
188
166
  };
189
167
  };
190
168
  const doUpdateEntityUpdates = (entityUpdates, entityId, entityType, recommendation, isBulkUpdate = false, updatedEntityId, entityName) => {