@zeniai/client-epic-state 5.1.45-betaDI2 → 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 +1 -16
  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 +1 -16
  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 -2
  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 -32
  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 +13 -266
  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 -2
  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 -16
  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 -2
  56. package/lib/view/taskManager/taskDetailView/taskDetailSelector.js +7 -30
  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 +14 -267
  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,16 +102,13 @@ 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
108
  // Group live tasks
109
109
  const liveGrouped = getGroupedTaskIds(data);
110
110
  draft.byTab.live = toTabData(liveGrouped);
111
111
  // Group other tabs if provided
112
- if (completed != null) {
113
- draft.byTab.completed = toTabData(getGroupedTaskIds(completed));
114
- }
115
112
  if (archived != null) {
116
113
  draft.byTab.archived = toTabData(getGroupedTaskIds(archived));
117
114
  }
@@ -357,26 +354,9 @@ const taskList = createSlice({
357
354
  draft.taskIdsByAssignees[assigneeKey] = draft.taskIdsByAssignees[assigneeKey].filter((taskId) => taskId !== currentTaskState.id);
358
355
  draft.taskIdsByTags[tagsKey] = draft.taskIdsByTags[tagsKey].filter((taskId) => taskId !== currentTaskState.id);
359
356
  draft.taskIdsByDueDate[dueDateKey] = draft.taskIdsByDueDate[dueDateKey].filter((taskId) => taskId !== currentTaskState.id);
360
- // Remove from EVERY previous group bucket the task lived in, not
361
- // just the destination bucket. Otherwise a detail-page save that
362
- // moves a task from group A to group B leaves the row listed
363
- // under both A and B until refetch.
364
- const previousGroupIds = currentTaskState.taskGroupIds ?? [];
365
- const allGroupIdsToClean = new Set([
366
- ...previousGroupIds,
367
- taskGroupId,
368
- ]);
369
- allGroupIdsToClean.forEach((gid) => {
370
- if (draft.taskIdsByGroupsIds[gid] != null) {
371
- draft.taskIdsByGroupsIds[gid] = draft.taskIdsByGroupsIds[gid].filter((id) => id !== currentTaskState.id);
372
- }
373
- });
357
+ draft.taskIdsByGroupsIds[taskGroupId] = draft.taskIdsByGroupsIds[taskGroupId].filter((taskId) => taskId !== currentTaskState.id);
374
358
  }
375
359
  doUpdateTaskList(draft, task, taskGroupId);
376
- // Mirror top-level grouping into the active tab's byTab snapshot,
377
- // otherwise the next updateTaskListTab call copies stale byTab data
378
- // back over this optimistic update.
379
- syncTopLevelToActiveTab(draft);
380
360
  },
381
361
  updateTaskListOnCreateTaskFromTemplateSuccess(draft, action) {
382
362
  const { tasks } = action.payload;
@@ -400,7 +380,6 @@ const taskList = createSlice({
400
380
  const taskId = task.id;
401
381
  draft.updateTasksFetchState.fetchState = 'In-Progress';
402
382
  draft.updateTasksFetchState.error = undefined;
403
- let didMigrateTab = false;
404
383
  if (updates.priority != null) {
405
384
  const prevPriorityCode = toPriorityCodeType(task.priority.code);
406
385
  draft.taskIdsByPriority[prevPriorityCode] = draft.taskIdsByPriority[prevPriorityCode].filter((id) => id != taskId);
@@ -412,25 +391,6 @@ const taskList = createSlice({
412
391
  draft.taskIdsByStatus[prevStatusCode] = draft.taskIdsByStatus[prevStatusCode].filter((id) => id != taskId);
413
392
  const newStatusCode = toTaskStatusCodeType(updates.status.code);
414
393
  draft.taskIdsByStatus[newStatusCode].push(taskId);
415
- // Optimistically re-bucket between the live and completed tabs
416
- // when status transitions in/out of "resolved", so the task
417
- // moves to the Completed tab without waiting for the next
418
- // fetch. Mirrors backend split in task_handler.py: only
419
- // `resolved` counts as completed; `done` stays Active.
420
- //
421
- // Subtasks travel with their parent — they appear nested under
422
- // the parent row, not as standalone entries in byTab.taskIds.
423
- // So we only rebucket parent tasks (tasks with no parentTaskId).
424
- const isTopLevelTask = task.parentTaskId == null;
425
- const wasCompleted = prevStatusCode === 'resolved';
426
- const isCompleted = newStatusCode === 'resolved';
427
- if (isTopLevelTask && wasCompleted !== isCompleted) {
428
- const from = isCompleted ? 'live' : 'completed';
429
- const to = isCompleted ? 'completed' : 'live';
430
- const updatedTask = { ...task, ...updates };
431
- migrateTaskBetweenTabs(draft, taskId, updatedTask, from, to);
432
- didMigrateTab = true;
433
- }
434
394
  }
435
395
  if (updates.assignees != null) {
436
396
  const prevGroupId = getAssigneesGroupKey(task.assignees);
@@ -470,18 +430,7 @@ const taskList = createSlice({
470
430
  };
471
431
  }
472
432
  }
473
- if (didMigrateTab) {
474
- // The migration moved the task out of the source byTab and into
475
- // the destination byTab. The active tab's grouping arrays are
476
- // now authoritative — mirror them back to the top-level so the
477
- // UI sees the task gone from Live (or freshly arrived in
478
- // Completed). Inverse of syncTopLevelToActiveTab, used only on
479
- // this path.
480
- copyTabDataToTopLevel(draft, draft.byTab[draft.currentTab]);
481
- }
482
- else {
483
- syncTopLevelToActiveTab(draft);
484
- }
433
+ syncTopLevelToActiveTab(draft);
485
434
  },
486
435
  updateTasksListOnUpdateTaskSuccess(draft) {
487
436
  draft.updateTasksFetchState.fetchState = 'Completed';
@@ -494,80 +443,12 @@ const taskList = createSlice({
494
443
  removeTaskFromList(draft, action) {
495
444
  removeTaskIds(draft, [action.payload]);
496
445
  },
497
- /**
498
- * Remove a task id from a single group bucket without touching tab
499
- * membership or other groupings. Used when the user clears a task's
500
- * group on the Detail page so the row stops appearing under the old
501
- * group while saveTaskDetailEpic skips the standard list re-insert.
502
- */
503
- removeTaskFromGroupBucket(draft, action) {
504
- const { taskId, taskGroupId } = action.payload;
505
- if (draft.taskIdsByGroupsIds[taskGroupId] != null) {
506
- draft.taskIdsByGroupsIds[taskGroupId] = draft.taskIdsByGroupsIds[taskGroupId].filter((id) => id !== taskId);
507
- }
508
- // Mirror to the active tab's grouping so the UI re-renders.
509
- const activeTab = draft.byTab[draft.currentTab];
510
- if (activeTab.taskIdsByGroupsIds[taskGroupId] != null) {
511
- activeTab.taskIdsByGroupsIds[taskGroupId] =
512
- activeTab.taskIdsByGroupsIds[taskGroupId].filter((id) => id !== taskId);
513
- }
514
- },
515
446
  clearTaskList(draft) {
516
447
  Object.assign(draft, initialState);
517
448
  },
518
449
  },
519
- extraReducers: (builder) => {
520
- // When the entity store receives updated tasks (e.g. from task detail
521
- // save, snooze/archive/unarchive, etc.), re-bucket parent tasks between
522
- // the live and completed tabs based on the new status. Mirrors the
523
- // backend split in task_handler.py: only `resolved` parents count as
524
- // completed. Subtasks are nested under their parent row, so they
525
- // never move between tabs on their own.
526
- builder.addCase(updateTasks, (draft, action) => {
527
- action.payload.forEach((taskPayload) => {
528
- // Only re-bucket parents; subtasks (parent_task_id is a non-empty
529
- // string) are nested under their parent row and don't move tabs.
530
- if (taskPayload.parent_task_id != null &&
531
- taskPayload.parent_task_id !== '') {
532
- return;
533
- }
534
- const taskId = taskPayload.task_id;
535
- // `TaskPayload.status` is required per the type — every producer
536
- // in this repo sends it, and `mapTaskPayloadToTask` dereferences
537
- // it unguarded. The optional chain + null guard are defensive
538
- // only, in case a downstream consumer (web-app-ui / cockpit-ui)
539
- // ever dispatches `updateTasks` with a partial payload; in that
540
- // case we treat missing status as "no status change" and skip
541
- // re-bucketing so we don't migrate a Completed task back into
542
- // the Live tab.
543
- const statusCode = taskPayload.status?.code;
544
- if (statusCode == null) {
545
- return;
546
- }
547
- const isCompleted = statusCode === 'resolved';
548
- const targetTab = isCompleted ? 'completed' : 'live';
549
- const otherTab = isCompleted ? 'live' : 'completed';
550
- // Skip if the task isn't currently in either of these buckets
551
- // (archived/snoozed/deleted are managed by their own actions).
552
- const inLive = draft.byTab.live.taskIds.includes(taskId);
553
- const inCompleted = draft.byTab.completed.taskIds.includes(taskId);
554
- if (!inLive && !inCompleted) {
555
- return;
556
- }
557
- if (draft.byTab[otherTab].taskIds.includes(taskId)) {
558
- migrateTaskPayloadBetweenTabs(draft, taskId, taskPayload, otherTab, targetTab);
559
- }
560
- else if (!draft.byTab[targetTab].taskIds.includes(taskId)) {
561
- draft.byTab[targetTab].taskIds.push(taskId);
562
- }
563
- });
564
- // Re-sync the top-level taskIds / grouping arrays for whichever
565
- // tab the user is currently looking at.
566
- copyTabDataToTopLevel(draft, draft.byTab[draft.currentTab]);
567
- });
568
- },
569
450
  });
570
- 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;
571
452
  export default taskList.reducer;
572
453
  /**
573
454
  * helpers
@@ -674,115 +555,6 @@ const copyTabDataToTopLevel = (draft, tabData) => {
674
555
  draft.taskIdsByDueDate = tabData.taskIdsByDueDate;
675
556
  draft.taskIdsByTags = tabData.taskIdsByTags;
676
557
  };
677
- /**
678
- * Migrate a parent task between two `byTab` buckets (live ↔ completed)
679
- * when its status flips into / out of `resolved`. The destination tab
680
- * picks up the task in EVERY grouping dimension (status, priority,
681
- * group id, assignees, dueDate, tags) using the UPDATED task as the
682
- * source-of-truth for the new bucket keys. Otherwise the Completed tab
683
- * shows the task in the right tab-level list but the grouped views
684
- * (Group by Status, Group by Priority, etc.) miss it until the next
685
- * full fetch.
686
- */
687
- const migrateTaskBetweenTabs = (draft, taskId, updatedTask, from, to) => {
688
- const fromTab = draft.byTab[from];
689
- const toTab = draft.byTab[to];
690
- // Source tab: drop the task from every grouping dimension.
691
- removeIdsFromTabData(fromTab, [taskId]);
692
- // Destination tab: add the task to taskIds (deduped) plus the
693
- // appropriate bucket inside each grouping dimension based on the
694
- // updated task.
695
- if (!toTab.taskIds.includes(taskId)) {
696
- toTab.taskIds.push(taskId);
697
- }
698
- const priorityKey = toPriorityCodeType(updatedTask.priority.code);
699
- if (!toTab.taskIdsByPriority[priorityKey].includes(taskId)) {
700
- toTab.taskIdsByPriority[priorityKey].push(taskId);
701
- }
702
- const statusKey = toTaskStatusCodeType(updatedTask.status.code);
703
- if (!toTab.taskIdsByStatus[statusKey].includes(taskId)) {
704
- toTab.taskIdsByStatus[statusKey].push(taskId);
705
- }
706
- const dueDateKey = updatedTask.dueDate != null
707
- ? getDueDateGroupKey(updatedTask.dueDate.toString())
708
- : 'upcoming';
709
- if (!toTab.taskIdsByDueDate[dueDateKey].includes(taskId)) {
710
- toTab.taskIdsByDueDate[dueDateKey].push(taskId);
711
- }
712
- const assigneeKey = getAssigneesGroupKey(updatedTask.assignees);
713
- if (toTab.taskIdsByAssignees[assigneeKey] == null) {
714
- toTab.taskIdsByAssignees[assigneeKey] = [taskId];
715
- }
716
- else if (!toTab.taskIdsByAssignees[assigneeKey].includes(taskId)) {
717
- toTab.taskIdsByAssignees[assigneeKey].push(taskId);
718
- }
719
- const tagIds = [...updatedTask.tagIds].sort();
720
- const tagsKey = tagIds.join(',');
721
- if (toTab.taskIdsByTags[tagsKey] == null) {
722
- toTab.taskIdsByTags[tagsKey] = [taskId];
723
- }
724
- else if (!toTab.taskIdsByTags[tagsKey].includes(taskId)) {
725
- toTab.taskIdsByTags[tagsKey].push(taskId);
726
- }
727
- (updatedTask.taskGroupIds ?? []).forEach((groupId) => {
728
- if (toTab.taskIdsByGroupsIds[groupId] == null) {
729
- toTab.taskIdsByGroupsIds[groupId] = [taskId];
730
- }
731
- else if (!toTab.taskIdsByGroupsIds[groupId].includes(taskId)) {
732
- toTab.taskIdsByGroupsIds[groupId].push(taskId);
733
- }
734
- });
735
- };
736
- /**
737
- * `updateTasks` extraReducer variant — same migration semantics but
738
- * keyed off the raw `TaskPayload` instead of the rich `Task` entity.
739
- */
740
- const migrateTaskPayloadBetweenTabs = (draft, taskId, payload, from, to) => {
741
- const fromTab = draft.byTab[from];
742
- const toTab = draft.byTab[to];
743
- removeIdsFromTabData(fromTab, [taskId]);
744
- if (!toTab.taskIds.includes(taskId)) {
745
- toTab.taskIds.push(taskId);
746
- }
747
- if (payload.priority?.code != null) {
748
- const priorityKey = toPriorityCodeType(payload.priority.code);
749
- if (!toTab.taskIdsByPriority[priorityKey].includes(taskId)) {
750
- toTab.taskIdsByPriority[priorityKey].push(taskId);
751
- }
752
- }
753
- if (payload.status?.code != null) {
754
- const statusKey = toTaskStatusCodeType(payload.status.code);
755
- if (!toTab.taskIdsByStatus[statusKey].includes(taskId)) {
756
- toTab.taskIdsByStatus[statusKey].push(taskId);
757
- }
758
- }
759
- const dueDateKey = getDueDateGroupKey(payload.due_date);
760
- if (!toTab.taskIdsByDueDate[dueDateKey].includes(taskId)) {
761
- toTab.taskIdsByDueDate[dueDateKey].push(taskId);
762
- }
763
- const assigneeKey = getAssigneesGroupKey(payload.assignees ?? []);
764
- if (toTab.taskIdsByAssignees[assigneeKey] == null) {
765
- toTab.taskIdsByAssignees[assigneeKey] = [taskId];
766
- }
767
- else if (!toTab.taskIdsByAssignees[assigneeKey].includes(taskId)) {
768
- toTab.taskIdsByAssignees[assigneeKey].push(taskId);
769
- }
770
- const tagsKey = getTagsGroupKey(payload.tags ?? []);
771
- if (toTab.taskIdsByTags[tagsKey] == null) {
772
- toTab.taskIdsByTags[tagsKey] = [taskId];
773
- }
774
- else if (!toTab.taskIdsByTags[tagsKey].includes(taskId)) {
775
- toTab.taskIdsByTags[tagsKey].push(taskId);
776
- }
777
- (payload.task_group_ids ?? []).forEach((groupId) => {
778
- if (toTab.taskIdsByGroupsIds[groupId] == null) {
779
- toTab.taskIdsByGroupsIds[groupId] = [taskId];
780
- }
781
- else if (!toTab.taskIdsByGroupsIds[groupId].includes(taskId)) {
782
- toTab.taskIdsByGroupsIds[groupId].push(taskId);
783
- }
784
- });
785
- };
786
558
  const removeIdsFromTabData = (tabData, taskIds) => {
787
559
  const removedSet = new Set(taskIds);
788
560
  const isRemoved = (id) => removedSet.has(id);
@@ -824,38 +596,13 @@ const getGroupedTaskIds = (tasks) => {
824
596
  ...initialTaskIdsByDueDate,
825
597
  };
826
598
  const taskIdsByTags = {};
827
- // A task is a "nested subtask" (hidden from the flat top-level list +
828
- // dropped from the tab count) iff a parent in the same payload claims
829
- // it via its `subtasks: [...]` array. Match this criterion exactly to
830
- // WC's `allSubtaskIds` union in TaskManagementPage.tsx — otherwise
831
- // the tab count and the visible top-level row count disagree
832
- // whenever the BE emits `parent_task_id` on a subtask but omits it
833
- // from the parent's `subtasks` array (a real inconsistency this UI
834
- // sees today).
835
- //
836
- // Using `parent_task_id` here would sometimes drop a task from the
837
- // count that WC still renders as a flat top-level row — that's the
838
- // bug we're fixing.
839
- const nestedSubtaskIds = new Set();
840
599
  tasks.forEach((task) => {
841
- (task.subtasks ?? []).forEach((subtaskId) => nestedSubtaskIds.add(subtaskId));
842
- });
843
- tasks.forEach((task) => {
844
- const isNestedSubtask = nestedSubtaskIds.has(task.task_id);
845
- // Subtasks stay in every groupBy array so the UI can render them
846
- // nested under their parent (WC's TaskManagementPage builds
847
- // `subtasksByParentId` from `group.tasks` and then filters them
848
- // out of top-level rows via `filterOutSubtasks`). Only the flat
849
- // top-level `taskIds` array — which drives the Active/Completed/…
850
- // tab counts — must exclude nested subtasks.
600
+ taskIds.push(task.task_id);
851
601
  const priorityKey = toPriorityCodeType(task.priority.code);
852
602
  const statusKey = toTaskStatusCodeType(task.status.code);
853
603
  const assigneeKey = getAssigneesGroupKey(task.assignees);
854
604
  const dueDateKey = getDueDateGroupKey(task.due_date);
855
605
  const tagsKey = getTagsGroupKey(task.tags);
856
- if (!isNestedSubtask) {
857
- taskIds.push(task.task_id);
858
- }
859
606
  taskIdsGroupedByPriority[priorityKey] = [
860
607
  ...taskIdsGroupedByPriority[priorityKey],
861
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) => {
@@ -58,9 +58,7 @@ export const updateTransactionDetailEpic = (actions$, state$, zeniAPI) => action
58
58
  if (response.data.vendors != null) {
59
59
  actions.push(updateVendors(response.data.vendors, 'merge'));
60
60
  }
61
- // 'replace' so deleted lines are removed from Redux; 'merge' would
62
- // keep any line absent from the server response (deletion bug).
63
- actions.push(updateTransaction(transactionDetailKeyForResponse, response.data.transaction, 'replace'));
61
+ actions.push(updateTransaction(transactionDetailKeyForResponse, response.data.transaction));
64
62
  actions.push(initializeTransactionDetailLocalData({
65
63
  transactionId: transactionIdResp,
66
64
  vendorId: response.data.vendors?.[0]?.vendor_id,
@@ -161,7 +159,7 @@ function preparePayload(transactionState, vendorState, transactionDetailState, t
161
159
  };
162
160
  }
163
161
  if (detail != null) {
164
- const transUpdates = toTransactionUpdatePayload(saveUpdates.updates, detail, cotTransactionTracking, isAccountingProjectsEnabled, vendorUpdates, customerUpdates, currentSelectedEntity, saveUpdates.deletedLineIds, saveUpdates.addedLines);
162
+ const transUpdates = toTransactionUpdatePayload(saveUpdates.updates, detail, cotTransactionTracking, isAccountingProjectsEnabled, vendorUpdates, customerUpdates, currentSelectedEntity);
165
163
  const entityUpdates = toEntityUpdatePayload(saveUpdates.entityRelatedUpdates);
166
164
  return {
167
165
  entity_related_updates: entityUpdates,
@@ -171,7 +169,7 @@ function preparePayload(transactionState, vendorState, transactionDetailState, t
171
169
  }
172
170
  return undefined;
173
171
  }
174
- export const toTransactionUpdatePayload = (updates, detail, cotTransactionTracking, isAccountingProjectsEnabled, vendorUpdates, customerUpdates, tranSelectedEntity, deletedLineIds, addedLines) => {
172
+ export const toTransactionUpdatePayload = (updates, detail, cotTransactionTracking, isAccountingProjectsEnabled, vendorUpdates, customerUpdates, tranSelectedEntity) => {
175
173
  const transaction = Object.assign({}, detail);
176
174
  transaction.memo = updates.memo ?? '';
177
175
  // Unlike `memo` (always reset to '' when absent), `documentId` falls back to
@@ -180,11 +178,6 @@ export const toTransactionUpdatePayload = (updates, detail, cotTransactionTracki
180
178
  // either the unchanged current value or undefined, and we must not send an
181
179
  // empty `document_id` that the server would treat as clearing the number.
182
180
  transaction.documentId = updates.documentId ?? transaction.documentId;
183
- // Remove lines deleted by the user in edit mode before patching or serializing.
184
- if (deletedLineIds != null && deletedLineIds.length > 0) {
185
- const deletedSet = new Set(deletedLineIds);
186
- transaction.lines = transaction.lines?.filter((line) => !deletedSet.has(line.id));
187
- }
188
181
  const recommendationUpdates = {};
189
182
  for (const key in updates.updatesByLineId) {
190
183
  const lineUpdates = updates.updatesByLineId[key];
@@ -192,9 +185,6 @@ export const toTransactionUpdatePayload = (updates, detail, cotTransactionTracki
192
185
  if (line.id === key) {
193
186
  const lineTobeUpdated = Object.assign({}, line);
194
187
  lineTobeUpdated.lineDescription = lineUpdates.lineDescription ?? '';
195
- if (lineUpdates.amount != null) {
196
- lineTobeUpdated.amount = lineUpdates.amount;
197
- }
198
188
  if (lineUpdates.class != null) {
199
189
  lineTobeUpdated.class = lineUpdates.class;
200
190
  }
@@ -216,16 +206,6 @@ export const toTransactionUpdatePayload = (updates, detail, cotTransactionTracki
216
206
  else {
217
207
  lineTobeUpdated.vendor = undefined;
218
208
  }
219
- // Persist a debit↔credit flip on an existing JE line. `lineTobeUpdated`
220
- // is typed as the account-and-class shape, but for a JE the runtime
221
- // object is a JournalEntryTransactionLine, so cast to set the side.
222
- if (lineUpdates.postingType != null) {
223
- const journalEntryLine = lineTobeUpdated;
224
- journalEntryLine.postingType = lineUpdates.postingType;
225
- if (lineUpdates.postingTypeName != null) {
226
- journalEntryLine.postingTypeName = lineUpdates.postingTypeName;
227
- }
228
- }
229
209
  if (lineUpdates.recommendation != null) {
230
210
  recommendationUpdates[line.id] = lineUpdates.recommendation;
231
211
  }
@@ -245,12 +225,6 @@ export const toTransactionUpdatePayload = (updates, detail, cotTransactionTracki
245
225
  });
246
226
  transaction.lines = newLineMap;
247
227
  }
248
- // Append lines added by the user in edit mode. Sending id='' tells the
249
- // backend to create a new QBO line rather than updating an existing one.
250
- if (addedLines != null && addedLines.length > 0) {
251
- const newLines = addedLines.map(toNewTransactionLine);
252
- transaction.lines = [...(transaction.lines ?? []), ...newLines];
253
- }
254
228
  transaction.recommendations = recommendationUpdates;
255
229
  // Convert transaction to payload first
256
230
  let basePayload;
@@ -325,52 +299,6 @@ const checkIfVendorOrCustomerMissingForAnyLineItem = (transactionPayload) => {
325
299
  }
326
300
  return isAnyVendorOrCustomerMissing;
327
301
  };
328
- // Converts a locally-created line item (from edit mode) into the wire shape
329
- // expected by the categorize endpoint. Sending id='' signals the backend to
330
- // create a new QBO line rather than update an existing one.
331
- //
332
- // Type-dispatched so each transaction family contributes its own new-line
333
- // shape: journal entries carry debit/credit `postingType`, expenses carry
334
- // `billable`. Phase 3 types extend this by adding a branch rather than forking
335
- // the function.
336
- const EMPTY_ACCOUNT = {
337
- accountDescription: '',
338
- accountId: '',
339
- accountName: '',
340
- labels: [],
341
- };
342
- export const toNewTransactionLine = (item) => {
343
- if (item.type === 'journal_entry_transaction_line') {
344
- const journalEntryLine = {
345
- ...item,
346
- id: '',
347
- type: 'journal_entry_transaction_line',
348
- platformLineDetailType: item.platformLineDetailType ?? 'journal_entry_line_detail',
349
- account: item.account ?? EMPTY_ACCOUNT,
350
- billable: item.billable ?? 'not_billable',
351
- isCategoryMiscategorized: false,
352
- isClassMiscategorized: false,
353
- lineTransactionType: item.lineTransactionType ?? 'journal_entry',
354
- postingType: item.postingType ?? 'debit',
355
- postingTypeName: item.postingTypeName != null && item.postingTypeName !== ''
356
- ? item.postingTypeName
357
- : (item.postingType ?? 'debit').charAt(0).toUpperCase() +
358
- (item.postingType ?? 'debit').slice(1),
359
- };
360
- return journalEntryLine;
361
- }
362
- const accountAndClassLine = {
363
- ...item,
364
- id: '',
365
- type: 'transaction_with_account_and_class_line',
366
- platformLineDetailType: item.platformLineDetailType ?? 'account_based_expense_line_detail',
367
- account: item.account ?? EMPTY_ACCOUNT,
368
- billable: item.billable ?? 'not_billable',
369
- isCategoryMiscategorized: false,
370
- isClassMiscategorized: false,
371
- };
372
- return accountAndClassLine;
373
- };
374
302
  const toEntityUpdatePayload = (entityUpdates) => {
375
303
  const entityUpdatesPayload = [];
376
304
  let entityKey;