@zeniai/client-epic-state 5.3.10 → 5.3.12-beta0DI

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 (66) hide show
  1. package/lib/entity/task/taskPayload.d.ts +1 -0
  2. package/lib/entity/task/taskPayload.js +1 -0
  3. package/lib/entity/task/taskReducer.d.ts +4 -1
  4. package/lib/entity/task/taskReducer.js +19 -2
  5. package/lib/entity/task/taskState.d.ts +3 -0
  6. package/lib/entity/timeEntry/timeEntryPayload.d.ts +26 -0
  7. package/lib/entity/timeEntry/timeEntryPayload.js +2 -0
  8. package/lib/entity/timeEntry/timeEntryReducer.d.ts +8 -0
  9. package/lib/entity/timeEntry/timeEntryReducer.js +76 -0
  10. package/lib/entity/timeEntry/timeEntrySelector.d.ts +10 -0
  11. package/lib/entity/timeEntry/timeEntrySelector.js +32 -0
  12. package/lib/entity/timeEntry/timeEntryState.d.ts +43 -0
  13. package/lib/entity/timeEntry/timeEntryState.js +13 -0
  14. package/lib/epic.d.ts +4 -1
  15. package/lib/epic.js +9 -0
  16. package/lib/esm/entity/task/taskPayload.js +1 -0
  17. package/lib/esm/entity/task/taskReducer.js +18 -1
  18. package/lib/esm/entity/timeEntry/timeEntryPayload.js +1 -0
  19. package/lib/esm/entity/timeEntry/timeEntryReducer.js +70 -0
  20. package/lib/esm/entity/timeEntry/timeEntrySelector.js +23 -0
  21. package/lib/esm/entity/timeEntry/timeEntryState.js +8 -0
  22. package/lib/esm/epic.js +9 -0
  23. package/lib/esm/index.js +4 -1
  24. package/lib/esm/reducer.js +6 -0
  25. package/lib/esm/view/taskManager/taskDetailView/taskDetailSelector.js +3 -0
  26. package/lib/esm/view/taskManager/taskListView/taskList.js +6 -0
  27. package/lib/esm/view/taskManager/taskListView/taskListFilterHelpers.js +7 -0
  28. package/lib/esm/view/taskManager/taskListView/taskListSelector.js +27 -11
  29. package/lib/esm/view/taskManager/taskListView/taskListViewHelpers.js +37 -0
  30. package/lib/esm/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.js +24 -0
  31. package/lib/esm/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.js +73 -0
  32. package/lib/esm/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.js +77 -0
  33. package/lib/esm/view/taskManager/timeEntryView/timeEntryViewPayload.js +1 -0
  34. package/lib/esm/view/taskManager/timeEntryView/timeEntryViewReducer.js +160 -0
  35. package/lib/esm/view/taskManager/timeEntryView/timeEntryViewSelector.js +41 -0
  36. package/lib/esm/view/taskManager/timeEntryView/timeEntryViewState.js +1 -0
  37. package/lib/index.d.ts +5 -1
  38. package/lib/index.js +31 -16
  39. package/lib/reducer.d.ts +2 -0
  40. package/lib/reducer.js +6 -0
  41. package/lib/rootStateTypes.d.ts +4 -0
  42. package/lib/view/taskManager/taskDetailView/taskDetailSelector.d.ts +4 -0
  43. package/lib/view/taskManager/taskDetailView/taskDetailSelector.js +3 -0
  44. package/lib/view/taskManager/taskListView/taskList.d.ts +1 -1
  45. package/lib/view/taskManager/taskListView/taskList.js +6 -0
  46. package/lib/view/taskManager/taskListView/taskListFilterHelpers.d.ts +4 -0
  47. package/lib/view/taskManager/taskListView/taskListFilterHelpers.js +9 -1
  48. package/lib/view/taskManager/taskListView/taskListSelector.d.ts +6 -3
  49. package/lib/view/taskManager/taskListView/taskListSelector.js +26 -10
  50. package/lib/view/taskManager/taskListView/taskListViewHelpers.d.ts +1 -1
  51. package/lib/view/taskManager/taskListView/taskListViewHelpers.js +37 -0
  52. package/lib/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.d.ts +18 -0
  53. package/lib/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.js +28 -0
  54. package/lib/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.d.ts +23 -0
  55. package/lib/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.js +78 -0
  56. package/lib/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.d.ts +23 -0
  57. package/lib/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.js +83 -0
  58. package/lib/view/taskManager/timeEntryView/timeEntryViewPayload.d.ts +27 -0
  59. package/lib/view/taskManager/timeEntryView/timeEntryViewPayload.js +2 -0
  60. package/lib/view/taskManager/timeEntryView/timeEntryViewReducer.d.ts +48 -0
  61. package/lib/view/taskManager/timeEntryView/timeEntryViewReducer.js +164 -0
  62. package/lib/view/taskManager/timeEntryView/timeEntryViewSelector.d.ts +24 -0
  63. package/lib/view/taskManager/timeEntryView/timeEntryViewSelector.js +46 -0
  64. package/lib/view/taskManager/timeEntryView/timeEntryViewState.d.ts +21 -0
  65. package/lib/view/taskManager/timeEntryView/timeEntryViewState.js +5 -0
  66. package/package.json +1 -1
@@ -0,0 +1,23 @@
1
+ import recordGet from 'lodash/get';
2
+ export function getTimeEntryById(timeEntryState, timeEntryId) {
3
+ return recordGet(timeEntryState.timeEntriesById, timeEntryId, undefined);
4
+ }
5
+ export function getTimeEntriesByIds(timeEntryState, timeEntryIds) {
6
+ return timeEntryIds
7
+ .map((timeEntryId) => getTimeEntryById(timeEntryState, timeEntryId))
8
+ .filter((entry) => entry != null);
9
+ }
10
+ export function getTaskCategoryById(timeEntryState, taskCategoryId) {
11
+ return recordGet(timeEntryState.taskCategoriesById, taskCategoryId, undefined);
12
+ }
13
+ /**
14
+ * The catalog, ordered service → vertical → name, matching the order the
15
+ * server sends so the dropdown reads the same on both sides.
16
+ */
17
+ export function getTaskCategories(timeEntryState, includeInactive = false) {
18
+ return Object.values(timeEntryState.taskCategoriesById)
19
+ .filter((category) => includeInactive || category.isActive)
20
+ .sort((left, right) => (left.service ?? '').localeCompare(right.service ?? '') ||
21
+ (left.vertical ?? '').localeCompare(right.vertical ?? '') ||
22
+ left.name.localeCompare(right.name));
23
+ }
@@ -0,0 +1,8 @@
1
+ import { stringToUnionStrict } from '../../commonStateTypes/stringToUnion';
2
+ export const ALL_TIME_ENTRY_SOURCES = ['timer', 'manual', 'migration'];
3
+ export const ALL_BILLING_TYPES = ['billable', 'non_billable'];
4
+ /** Unknown values become `undefined` rather than being cast through.
5
+ * A server that gains a source we do not know about must not land an
6
+ * unmatched string in a union the UI switches on. */
7
+ export const toTimeEntrySource = (raw) => raw == null ? undefined : stringToUnionStrict(raw, ALL_TIME_ENTRY_SOURCES);
8
+ export const toBillingType = (raw) => raw == null ? undefined : stringToUnionStrict(raw, ALL_BILLING_TYPES);
package/lib/esm/epic.js CHANGED
@@ -731,6 +731,9 @@ import { dragNDropTasksEpic, } from './view/taskManager/taskListView/epics/dragN
731
731
  import { fetchTaskListEpic, } from './view/taskManager/taskListView/epics/fetchTaskListEpic';
732
732
  import { fetchTaskListPageEpic, } from './view/taskManager/taskListView/epics/fetchTaskListPageEpic';
733
733
  import { updateTaskFromListViewEpic, } from './view/taskManager/taskListView/epics/updateTaskFromListViewEpic';
734
+ import { fetchTaskCategoriesEpic, } from './view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic';
735
+ import { fetchTimeEntriesForRangeEpic, fetchTimeEntriesForTaskEpic, } from './view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic';
736
+ import { deleteTimeEntryEpic, logTimeEpic, updateTimeEntryEpic, } from './view/taskManager/timeEntryView/epic/writeTimeEntryEpic';
734
737
  import { fetchTasksCardEpic, } from './view/tasksCard/fetchTasksCardEpic';
735
738
  import { fetchTopExEpic, } from './view/topEx/topExEpic';
736
739
  import { fetchTransactionActivityLogEpic, } from './view/transactionActivityLogView/fetchTransactionActivityLogEpic';
@@ -882,6 +885,7 @@ const ALL_EPICS = [
882
885
  deleteTagEpic,
883
886
  deleteTaskEpic,
884
887
  deleteTaskGroupEpic,
888
+ deleteTimeEntryEpic,
885
889
  deleteTransactionAttachmentEpic,
886
890
  deleteUserBankAccountEpic,
887
891
  disableAccountingProjectsEpic,
@@ -1233,6 +1237,7 @@ const ALL_EPICS = [
1233
1237
  fetchSubscriptionViewEpic,
1234
1238
  fetchSubTasksEpic,
1235
1239
  fetchSuggestedQuestionsEpic,
1240
+ fetchTaskCategoriesEpic,
1236
1241
  fetchTaskDetailEpic,
1237
1242
  fetchTaskDetailPageEpic,
1238
1243
  fetchTaskGroupTemplatesEpic,
@@ -1241,6 +1246,8 @@ const ALL_EPICS = [
1241
1246
  fetchTaskListPageEpic,
1242
1247
  fetchTaskManagerMetricsEpic,
1243
1248
  fetchTasksCardEpic,
1249
+ fetchTimeEntriesForRangeEpic,
1250
+ fetchTimeEntriesForTaskEpic,
1244
1251
  fetchTopExEpic,
1245
1252
  fetchTransactionActivityLogEpic,
1246
1253
  fetchTransactionDetailEpic,
@@ -1333,6 +1340,7 @@ const ALL_EPICS = [
1333
1340
  issueCreditNoteEpic,
1334
1341
  lockChargeCardEpic,
1335
1342
  lockChargeCardsEpic,
1343
+ logTimeEpic,
1336
1344
  markAsCompleteScheduleDetailEpic,
1337
1345
  markCoachmarkDismissedEpic,
1338
1346
  markTransactionAsNotMiscategorizedEpic,
@@ -1558,6 +1566,7 @@ const ALL_EPICS = [
1558
1566
  updateStatementInfoEpic,
1559
1567
  updateTaskFromListViewEpic,
1560
1568
  updateTaskGroupNameEpic,
1569
+ updateTimeEntryEpic,
1561
1570
  updateTransactionDetailEpic,
1562
1571
  updateVendorContactEpic,
1563
1572
  uploadAccountStatementEpic,
package/lib/esm/index.js CHANGED
@@ -75,6 +75,7 @@ import { getTaskGroupById } from './entity/taskGroup/taskGroupSelector';
75
75
  import { deleteConnection, doMagicLinkSignIn, doSignIn, doSignOut, fetchActiveTenant, fetchAllTenants, fetchExcludedResources as fetchExcludedResourcesForTenant, fetchExternalConnections as fetchExternalConnectionsForTenant, fetchSubscriptionSummaryForTenant, initEmailConnectOAuth, resendVerifyDeviceOTP, resetSignInState, saveAPIKeyConnection, saveConnectorCredentials, saveExternalConnection as saveExternalConnectionForTenant, saveOAuthConnection, sendEmailMagicLinkToUser, sendSessionHeartbeat, toExternalIntegrationType, updateCurrentTenant, updateSignInState, verifyDeviceWithTwoFA, } from './entity/tenant/tenantReducer';
76
76
  import { DEFAULT_RECEIPTS_EMAIL, getCurrentTenant, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, getIsAutoReviewIncomeWithoutCustomerEnabled, getReceiptsEmail, getTenantBaseById, getTenantBaseViewForTenantId, getTenantBaseViewForTenantView, getTenantsBaseByCheckInDateSelector, getTenantsByCheckInDateSelector, getTenantsCoreDetailsByCheckInDateSelector, isOtherProductsEnabledForTenant, isTenantBankingOnly, isTenantBookkeepingEnabled, isTenantCardsOnly, isTenantUsingZeniCOA, isZeniDomainTenant, toTenantCoreDetailsView, } from './entity/tenant/tenantSelector';
77
77
  import { toExternalSupportedTool } from './entity/tenant/tenantTypes';
78
+ import { getTaskCategories, getTaskCategoryById, } from './entity/timeEntry/timeEntrySelector';
78
79
  import { pushToastNotification } from './entity/toastNotification/toastNotificationReducer';
79
80
  import { getLastNotificationTime, getNotifications, } from './entity/toastNotification/toastNotificationSelector';
80
81
  import { mapFileEntityToAttachment, toContentType, toContentTypeStrict, } from './entity/transaction/stateTypes/attachment';
@@ -423,6 +424,8 @@ import { ALL_TASK_LIST_TABS, TASK_LIST_FILTER_CATEGORIES, TASK_LIST_GROUP_BY_CAT
423
424
  import { bulkUpdateTaskList, dragNDropTasks, fetchTaskList, fetchTaskListPage, initiateTaskListLocalData, removeTaskFromList, updateTaskFilters, updateTaskFromListView, updateTaskListLocalData, updateTaskListSearchText, updateTaskListTab, updateTaskListUIState, } from './view/taskManager/taskListView/taskListReducer';
424
425
  import { getAllTasks, } from './view/taskManager/taskListView/taskListSelector';
425
426
  import { getDueDateValueFromDueDateGroupId, getTaskUpdates, sortSubtasks, } from './view/taskManager/taskListView/taskListViewHelpers';
427
+ import { deleteTimeEntry, fetchTaskCategories, fetchTimeEntriesForRange, fetchTimeEntriesForTask, logTime, updateTimeEntryMinutes, } from './view/taskManager/timeEntryView/timeEntryViewReducer';
428
+ import { getTimeEntriesForRange, getTimeEntriesForTask, getTimeEntrySaveState, } from './view/taskManager/timeEntryView/timeEntryViewSelector';
426
429
  import { fetchTasksCard } from './view/tasksCard/tasksCardReducer';
427
430
  import { fetchTopEx, updateTopExDownloadState, updateTopExTimeFrame, } from './view/topEx/topExReducer';
428
431
  import { getAllTopExpenses, getTop6Expenses, getTopExpenses, } from './view/topEx/topExSelector';
@@ -681,7 +684,7 @@ export { fetchGlobalMerchantRecommendation, createGlobalMerchant, updateCreateGl
681
684
  export { approveVendorGlobalReview, rejectVendorGlobalReview, fetchVendorGlobalReviewView, updateSelectedGlobalMerchant, updateVendorGlobalReviewViewUIState, getVendorGlobalReviewView, getTenantMerchantByMerchantId, toVendorGlobalReviewColumnSortKeyType, updateVendorGlobalReviewViewLocalData, };
682
685
  export { fetchArAging, updateArAgingUIState, fetchArAgingDetail, updateArAgingDetailUIState, getArAgingReport, getArAgingDetailForCustomer, updateArAgingNodeCollapseState, };
683
686
  export { toRecurringBillFrequency, toRecurringBillFrequencyStrict, };
684
- export { getTaskGroupById, getAllTasks, fetchTaskList, fetchTaskListPage, getTaskDetail, fetchTaskDetailPage, saveTaskUpdatesToLocalStore, saveTaskDetail, createSubTask, resetSubTaskCreateStatus, fetchSubTasks, archiveTask, fetchCannedResponses, saveCannedResponse, deleteCannedResponse, getCannedResponsesView, updateTaskListSearchText, updateTaskListUIState, allTaskStatus, allTaskPriority, updateTaskFilters, TASK_LIST_FILTER_CATEGORIES, TASK_LIST_GROUP_BY_CATEGORIES, deleteTask, discardTaskUpdatesInLocalStore, bulkUpdateTaskList, createNewTaskGroup, initiateTaskListLocalData, deleteTaskGroup, updateTaskListLocalData, dragNDropTasks, updateTaskGroupName, toTaskListGroupByKeyType, toTaskListGroupByKeyTypeStrict, getTaskUpdates, sortSubtasks, fetchAllTaskGroups, initialTaskDetail, getDueDateValueFromDueDateGroupId, toPriorityCodeType, toTaskStatusCodeType, toDueDateGroupKeyType, updateTaskFromListView, updateTaskListTab, removeTaskFromList, snoozeTask, unsnoozeTask, convertHHMMStrToMinutes, initialTaskDetailLocalData, ALL_TASK_LIST_TABS, };
687
+ export { getTaskGroupById, getAllTasks, getTaskCategories, getTaskCategoryById, getTimeEntriesForRange, getTimeEntriesForTask, getTimeEntrySaveState, fetchTimeEntriesForTask, fetchTimeEntriesForRange, fetchTaskCategories, logTime, updateTimeEntryMinutes, deleteTimeEntry, fetchTaskList, fetchTaskListPage, getTaskDetail, fetchTaskDetailPage, saveTaskUpdatesToLocalStore, saveTaskDetail, createSubTask, resetSubTaskCreateStatus, fetchSubTasks, archiveTask, fetchCannedResponses, saveCannedResponse, deleteCannedResponse, getCannedResponsesView, updateTaskListSearchText, updateTaskListUIState, allTaskStatus, allTaskPriority, updateTaskFilters, TASK_LIST_FILTER_CATEGORIES, TASK_LIST_GROUP_BY_CATEGORIES, deleteTask, discardTaskUpdatesInLocalStore, bulkUpdateTaskList, createNewTaskGroup, initiateTaskListLocalData, deleteTaskGroup, updateTaskListLocalData, dragNDropTasks, updateTaskGroupName, toTaskListGroupByKeyType, toTaskListGroupByKeyTypeStrict, getTaskUpdates, sortSubtasks, fetchAllTaskGroups, initialTaskDetail, getDueDateValueFromDueDateGroupId, toPriorityCodeType, toTaskStatusCodeType, toDueDateGroupKeyType, updateTaskFromListView, updateTaskListTab, removeTaskFromList, snoozeTask, unsnoozeTask, convertHHMMStrToMinutes, initialTaskDetailLocalData, ALL_TASK_LIST_TABS, };
685
688
  export { getAllTags, fetchTagList, createTag, deleteTag, };
686
689
  export { getAllCardsAndBankPaymentMethods, createCardSetupIntent, confirmCardSetupIntent, addCardPaymentSource, fetchPaymentSources, resetCardPaymentErrorStatuses, clearCardPaymentView, };
687
690
  export { getAuditReportGroupViewSelectorView, getAuditRuleGroupViewSelectorView, getUserFromAllUsers, fetchAuditRuleGroupView, fetchAuditReportGroupView, saveReasonForAuditRule, clearAuditReportGroupViewByCompanyId, };
@@ -70,6 +70,7 @@ import taskGroup, { initialState as initialTaskGroupState, } from './entity/task
70
70
  import taskGroupTemplate, { initialState as initialTaskGroupTemplateState, } from './entity/taskGroupTemplate/taskGroupTemplateReducer';
71
71
  import taskSummary, { initialState as initailTaskSummaryState, } from './entity/taskSummary/taskSummaryReducer';
72
72
  import tenant, { initialState as initialTenantState, } from './entity/tenant/tenantReducer';
73
+ import timeEntry, { initialState as initialTimeEntryState, } from './entity/timeEntry/timeEntryReducer';
73
74
  import toastNotification, { initialState as initialToastNotificationState, } from './entity/toastNotification/toastNotificationReducer';
74
75
  import transaction, { initialState as initialTransactionState, } from './entity/transaction/transactionReducer';
75
76
  import transactionActivityLog, { initialState as initialTransactionActivityLogState, } from './entity/transactionActivityLog/transactionActivityLogReducer';
@@ -256,6 +257,7 @@ import taskDetailView, { initialState as initialTaskDetailState, } from './view/
256
257
  import taskGroupTemplateView, { initialState as initialTaskGroupTemplateViewState, } from './view/taskManager/taskGroupTemplateView/taskGroupTemplateViewReducer';
257
258
  import taskGroupView, { initialState as initialTaskGroupViewState, } from './view/taskManager/taskGroupView/taskGroupViewReducer';
258
259
  import taskList, { initialState as initialTaskListState, } from './view/taskManager/taskListView/taskListReducer';
260
+ import timeEntryView, { initialState as initialTimeEntryViewState, } from './view/taskManager/timeEntryView/timeEntryViewReducer';
259
261
  import tasksCard, { initialState as initialTasksCardState, } from './view/tasksCard/tasksCardReducer';
260
262
  import topEx, { initialState as initialTopExState, } from './view/topEx/topExReducer';
261
263
  import transactionActivityLogView, { initialState as initialTransactionActivityLogViewState, } from './view/transactionActivityLogView/transactionActivityLogViewReducer';
@@ -344,6 +346,7 @@ const initialEntitiesState = {
344
346
  subscriptionState: initialSubscriptionState,
345
347
  subscriptionSummaryState: initialSubscriptionSummaryState,
346
348
  tagState: initialTagState,
349
+ timeEntryState: initialTimeEntryState,
347
350
  taskGroupState: initialTaskGroupState,
348
351
  taskGroupTemplateState: initialTaskGroupTemplateState,
349
352
  taskState: initialTaskState,
@@ -526,6 +529,7 @@ const initialViewsState = {
526
529
  taskGroupTemplateViewState: initialTaskGroupTemplateViewState,
527
530
  taskGroupViewState: initialTaskGroupViewState,
528
531
  taskListState: initialTaskListState,
532
+ timeEntryViewState: initialTimeEntryViewState,
529
533
  tasksCardState: initialTasksCardState,
530
534
  topExState: initialTopExState,
531
535
  transactionActivityLogViewState: initialTransactionActivityLogViewState,
@@ -634,6 +638,7 @@ const entityReducers = {
634
638
  subscriptionState: subscription,
635
639
  subscriptionSummaryState: subscriptionSummary,
636
640
  tagState: tag,
641
+ timeEntryState: timeEntry,
637
642
  taskGroupState: taskGroup,
638
643
  taskGroupTemplateState: taskGroupTemplate,
639
644
  taskState: task,
@@ -815,6 +820,7 @@ const viewReducers = {
815
820
  taskGroupTemplateViewState: taskGroupTemplateView,
816
821
  taskGroupViewState: taskGroupView,
817
822
  taskListState: taskList,
823
+ timeEntryViewState: timeEntryView,
818
824
  tagViewState: tagView,
819
825
  tasksCardState: tasksCard,
820
826
  topExState: topEx,
@@ -26,6 +26,7 @@ export const getTaskDetail = (state, taskId) => {
26
26
  let isDeleted = undefined;
27
27
  let depth = undefined;
28
28
  let recurringSourceTaskId = undefined;
29
+ let taskCategoryId = undefined;
29
30
  let snoozedUntil = undefined;
30
31
  let taskGroupId = undefined;
31
32
  let subtasks = [];
@@ -39,6 +40,7 @@ export const getTaskDetail = (state, taskId) => {
39
40
  if (taskEntity != null) {
40
41
  createdByUser = getUserAndUserRole(userState, userRoleState, addressState, taskEntity.createdBy);
41
42
  depth = taskEntity.depth;
43
+ taskCategoryId = taskEntity.taskCategoryId;
42
44
  isArchived = taskEntity.isArchived;
43
45
  isDeleted = taskEntity.isDeleted;
44
46
  recurringSourceTaskId = taskEntity.recurringSourceTaskId;
@@ -103,6 +105,7 @@ export const getTaskDetail = (state, taskId) => {
103
105
  version: 0,
104
106
  createdByUser,
105
107
  depth,
108
+ taskCategoryId,
106
109
  isArchived,
107
110
  isDeleted,
108
111
  recurringSourceTaskId,
@@ -47,7 +47,13 @@ export const TASK_LIST_FILTER_CATEGORIES = [
47
47
  type: 'user',
48
48
  multiple: true,
49
49
  },
50
+ {
51
+ value: 'createdBy',
52
+ type: 'user',
53
+ multiple: true,
54
+ },
50
55
  { value: 'creationDate', type: 'dateRange', multiple: true },
56
+ { value: 'billing', type: 'dropdown', multiple: true },
51
57
  ];
52
58
  export const TASK_LIST_GROUP_BY_CATEGORIES = [
53
59
  { value: 'status' },
@@ -1,3 +1,8 @@
1
+ /** Billing lives on the task's category, not the task, so it needs the catalog. */
2
+ export const buildCategoryValueGetter = (taskCategoriesById) => (key, task) => key === 'billing'
3
+ ? (taskCategoriesById[task.taskCategoryId ?? '']?.billingType ??
4
+ 'non_billable')
5
+ : getCategoryValueForTaskList(key, task);
1
6
  export const getCategoryValueForTaskList = (key, task) => {
2
7
  switch (key) {
3
8
  case 'dueDate':
@@ -12,6 +17,8 @@ export const getCategoryValueForTaskList = (key, task) => {
12
17
  return task.createTime;
13
18
  case 'assignee':
14
19
  return task.assignees;
20
+ case 'createdBy':
21
+ return task.createdBy;
15
22
  default:
16
23
  return undefined;
17
24
  }
@@ -15,7 +15,7 @@ import { getUserGroupList } from '../../userGroupListView/userGroupListViewSelec
15
15
  import { getUserList, } from '../../userListView/userListViewSelector';
16
16
  import { allTaskPriority, allTaskStatus, } from '../taskDetailView/taskDetailSelector';
17
17
  import { DUE_DATE_GROUP_KEYS, } from './taskList';
18
- import { getCategoryValueForTaskList } from './taskListFilterHelpers';
18
+ import { buildCategoryValueGetter } from './taskListFilterHelpers';
19
19
  const sortTasksList = (tagState, tasksList, sortKey, sortOrder) => {
20
20
  const tasksInOrder = orderBy(tasksList, (task) => {
21
21
  switch (sortKey) {
@@ -47,7 +47,7 @@ const sortTasksList = (tagState, tasksList, sortKey, sortOrder) => {
47
47
  }, sortOrder);
48
48
  return tasksInOrder;
49
49
  };
50
- const getTasksWithUserDetails = ({ taskListState, taskState, userState, userRoleState, addressState, taskIds, tagState, userGroupsState, }) => {
50
+ const getTasksWithUserDetails = ({ taskListState, taskState, userState, userRoleState, addressState, taskIds, tagState, userGroupsState, taskCategoriesById, }) => {
51
51
  const { sortKey, searchText } = taskListState.uiState;
52
52
  const sortOrder = getSortOrder(taskListState.uiState.sortOrder);
53
53
  const tasks = taskIds
@@ -70,11 +70,11 @@ const getTasksWithUserDetails = ({ taskListState, taskState, userState, userRole
70
70
  return undefined;
71
71
  })
72
72
  .filter((value) => value?.name.toLowerCase().includes(searchText.toLowerCase()) === true);
73
- const filteredTaskList = applyAdvancedFiltersOnList(getCategoryValueForTaskList, tasks, taskListState.filters);
73
+ const filteredTaskList = applyAdvancedFiltersOnList(buildCategoryValueGetter(taskCategoriesById), tasks, taskListState.filters);
74
74
  const sortedTaskList = sortTasksList(tagState, filteredTaskList, sortKey, sortOrder);
75
75
  return sortedTaskList;
76
76
  };
77
- const getTaskListGroupedByGroupName = ({ taskListState, taskState, taskGroupState, taskGroupsIds, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
77
+ const getTaskListGroupedByGroupName = ({ taskListState, taskState, taskGroupState, taskGroupsIds, userState, userRoleState, addressState, tagState, userGroupsState, taskCategoriesById, }) => {
78
78
  const taskGroupsWithTasksList = taskGroupsIds
79
79
  .map((taskGroupId) => {
80
80
  const taskGroup = getTaskGroupById(taskGroupState, taskGroupId);
@@ -88,6 +88,7 @@ const getTaskListGroupedByGroupName = ({ taskListState, taskState, taskGroupStat
88
88
  taskIds,
89
89
  tagState,
90
90
  userGroupsState,
91
+ taskCategoriesById,
91
92
  });
92
93
  if (taskGroup != null) {
93
94
  return {
@@ -119,7 +120,7 @@ const getTaskListGroupedByGroupName = ({ taskListState, taskState, taskGroupStat
119
120
  // Also hides deleted groups, which are empty as a side effect.
120
121
  return sortedTaskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
121
122
  };
122
- export const getTaskListGroupedByAssignees = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
123
+ export const getTaskListGroupedByAssignees = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, taskCategoriesById, }) => {
123
124
  const assigneeGroups = Object.keys(taskListState.taskIdsByAssignees);
124
125
  const taskGroupsWithTasksList = assigneeGroups.map((groupKey) => {
125
126
  const taskIds = taskListState.taskIdsByAssignees[groupKey];
@@ -132,6 +133,7 @@ export const getTaskListGroupedByAssignees = ({ taskListState, taskState, userSt
132
133
  taskIds,
133
134
  tagState,
134
135
  userGroupsState,
136
+ taskCategoriesById,
135
137
  });
136
138
  const assigneeIds = groupKey.split(',');
137
139
  const groupName = getUsersByUserIds(userState, assigneeIds)
@@ -149,7 +151,7 @@ export const getTaskListGroupedByAssignees = ({ taskListState, taskState, userSt
149
151
  });
150
152
  return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
151
153
  };
152
- export const getTaskListGroupedByPriority = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
154
+ export const getTaskListGroupedByPriority = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, taskCategoriesById, }) => {
153
155
  const taskGroupsWithTasksList = ALL_PRIORITY_CODE.map((groupKey) => {
154
156
  const taskIds = taskListState.taskIdsByPriority[groupKey] ?? [];
155
157
  const tasksWithUserDetails = getTasksWithUserDetails({
@@ -161,6 +163,7 @@ export const getTaskListGroupedByPriority = ({ taskListState, taskState, userSta
161
163
  taskIds,
162
164
  tagState,
163
165
  userGroupsState,
166
+ taskCategoriesById,
164
167
  });
165
168
  return {
166
169
  groupName: allTaskPriority.find((priority) => priority.code === groupKey)?.name ??
@@ -172,7 +175,7 @@ export const getTaskListGroupedByPriority = ({ taskListState, taskState, userSta
172
175
  });
173
176
  return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
174
177
  };
175
- const getTaskListGroupedByStatus = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
178
+ const getTaskListGroupedByStatus = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, taskCategoriesById, }) => {
176
179
  const taskGroupsWithTasksList = ALL_TASK_STATUS_CODE.map((groupKey) => {
177
180
  const taskIds = taskListState.taskIdsByStatus[groupKey] ?? [];
178
181
  const tasksWithUserDetails = getTasksWithUserDetails({
@@ -184,6 +187,7 @@ const getTaskListGroupedByStatus = ({ taskListState, taskState, userState, userR
184
187
  taskIds,
185
188
  tagState,
186
189
  userGroupsState,
190
+ taskCategoriesById,
187
191
  });
188
192
  return {
189
193
  groupName: allTaskStatus.find((taskStatus) => taskStatus.code === groupKey)
@@ -195,7 +199,7 @@ const getTaskListGroupedByStatus = ({ taskListState, taskState, userState, userR
195
199
  });
196
200
  return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
197
201
  };
198
- const getTaskListGroupedByTags = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
202
+ const getTaskListGroupedByTags = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, taskCategoriesById, }) => {
199
203
  const tagGroups = Object.keys(taskListState.taskIdsByTags);
200
204
  const taskGroupsWithTasksList = tagGroups.map((groupKey) => {
201
205
  const taskIds = taskListState.taskIdsByTags[groupKey];
@@ -208,6 +212,7 @@ const getTaskListGroupedByTags = ({ taskListState, taskState, userState, userRol
208
212
  taskIds,
209
213
  tagState,
210
214
  userGroupsState,
215
+ taskCategoriesById,
211
216
  });
212
217
  const tagIds = groupKey.split(',');
213
218
  const groupName = getTagsByIds(tagState, tagIds)
@@ -224,7 +229,7 @@ const getTaskListGroupedByTags = ({ taskListState, taskState, userState, userRol
224
229
  });
225
230
  return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
226
231
  };
227
- const getTaskListGroupedByDueDate = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
232
+ const getTaskListGroupedByDueDate = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, taskCategoriesById, }) => {
228
233
  const taskGroupsWithTasksList = DUE_DATE_GROUP_KEYS.map((groupKey) => {
229
234
  const taskIds = taskListState.taskIdsByDueDate[groupKey];
230
235
  const tasksWithUserDetails = getTasksWithUserDetails({
@@ -236,6 +241,7 @@ const getTaskListGroupedByDueDate = ({ taskListState, taskState, userState, user
236
241
  taskIds,
237
242
  tagState,
238
243
  userGroupsState,
244
+ taskCategoriesById,
239
245
  });
240
246
  return {
241
247
  groupName: groupKey,
@@ -246,7 +252,7 @@ const getTaskListGroupedByDueDate = ({ taskListState, taskState, userState, user
246
252
  });
247
253
  return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
248
254
  };
249
- const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRoleState, addressState, taskGroupState, taskGroupsIds, tagState, userGroupsState, }) => {
255
+ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRoleState, addressState, taskGroupState, taskGroupsIds, tagState, userGroupsState, taskCategoriesById, }) => {
250
256
  const { groupByKey } = taskListState.uiState;
251
257
  if (groupByKey === 'groupName') {
252
258
  return getTaskListGroupedByGroupName({
@@ -259,6 +265,7 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
259
265
  taskGroupsIds,
260
266
  tagState,
261
267
  userGroupsState,
268
+ taskCategoriesById,
262
269
  });
263
270
  }
264
271
  if (groupByKey === 'assignee') {
@@ -270,6 +277,7 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
270
277
  addressState,
271
278
  tagState,
272
279
  userGroupsState,
280
+ taskCategoriesById,
273
281
  });
274
282
  }
275
283
  if (groupByKey === 'priority') {
@@ -281,6 +289,7 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
281
289
  addressState,
282
290
  tagState,
283
291
  userGroupsState,
292
+ taskCategoriesById,
284
293
  });
285
294
  }
286
295
  if (groupByKey === 'status') {
@@ -292,6 +301,7 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
292
301
  addressState,
293
302
  tagState,
294
303
  userGroupsState,
304
+ taskCategoriesById,
295
305
  });
296
306
  }
297
307
  if (groupByKey === 'tag') {
@@ -303,6 +313,7 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
303
313
  addressState,
304
314
  tagState,
305
315
  userGroupsState,
316
+ taskCategoriesById,
306
317
  });
307
318
  }
308
319
  return getTaskListGroupedByDueDate({
@@ -313,6 +324,7 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
313
324
  addressState,
314
325
  tagState,
315
326
  userGroupsState,
327
+ taskCategoriesById,
316
328
  });
317
329
  };
318
330
  /**
@@ -337,7 +349,10 @@ const getTaskGroupsWithTaskListMemoized = createSelector((state) => state.taskLi
337
349
  // invalidate this memo and rebuild a board the user is already using.
338
350
  (state) => tagsAreStructural(state.taskListState.uiState)
339
351
  ? state.tagState
340
- : EMPTY_TAG_STATE, (state) => state.userGroupsState, (uiState, filters, taskIdsByGroupsIds, taskIdsByAssignees, taskIdsByPriority, taskIdsByStatus, taskIdsByTags, taskIdsByDueDate, taskState, userState, userRoleState, addressState, taskGroupState, taskGroupsIds, tagState, userGroupsState) => getTaskGroupsWithTaskList({
352
+ : EMPTY_TAG_STATE, (state) => state.userGroupsState,
353
+ // Only the catalog, not the whole timeEntry slice: billing is read off
354
+ // the task's category, and a timer tick must not rebuild the board.
355
+ (state) => state.timeEntryState.taskCategoriesById, (uiState, filters, taskIdsByGroupsIds, taskIdsByAssignees, taskIdsByPriority, taskIdsByStatus, taskIdsByTags, taskIdsByDueDate, taskState, userState, userRoleState, addressState, taskGroupState, taskGroupsIds, tagState, userGroupsState, taskCategoriesById) => getTaskGroupsWithTaskList({
341
356
  taskListState: {
342
357
  uiState,
343
358
  filters,
@@ -356,6 +371,7 @@ const getTaskGroupsWithTaskListMemoized = createSelector((state) => state.taskLi
356
371
  taskGroupsIds,
357
372
  tagState,
358
373
  userGroupsState,
374
+ taskCategoriesById,
359
375
  }));
360
376
  export const getAllTasks = createSelector(
361
377
  // `taskState`, `taskGroupState`, `addressState` and `tagState` are gone from
@@ -1,3 +1,4 @@
1
+ import { convertHHMMStrToMinutes } from '../../../commonStateTypes/fiscalYearHelpers/formatMinutesToFromHHMM';
1
2
  import { dateNow } from '../../../zeniDayJS';
2
3
  import { allTaskPriority, allTaskStatus, } from '../taskDetailView/taskDetailSelector';
3
4
  import { toDueDateGroupKeyType } from './taskList';
@@ -63,6 +64,11 @@ export const sortSubtasks = (subtasks, sortKey, sortOrder, userList, tagList) =>
63
64
  return acc;
64
65
  }, {});
65
66
  const unknownStatusRank = allTaskStatus.length;
67
+ const priorityOrder = allTaskPriority.reduce((acc, priority, index) => {
68
+ acc[priority.code] = index;
69
+ return acc;
70
+ }, {});
71
+ const unknownPriorityRank = allTaskPriority.length;
66
72
  const cmpStr = (a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' });
67
73
  const keyFn = (task) => {
68
74
  if (sortKey === 'name') {
@@ -89,6 +95,37 @@ export const sortSubtasks = (subtasks, sortKey, sortOrder, userList, tagList) =>
89
95
  const tag = tagList.find((candidate) => candidate.tagId === firstTagId);
90
96
  return tag?.name ?? '';
91
97
  }
98
+ if (sortKey === 'priority') {
99
+ // Rank by the canonical order, not the code string: alphabetical would
100
+ // sort "high" before "urgent" and read as wrong to anyone scanning it.
101
+ return priorityOrder[task.priority?.code] ?? unknownPriorityRank;
102
+ }
103
+ if (sortKey === 'dueDate') {
104
+ // A task with no date is not "earliest", so undated rows belong at the
105
+ // bottom in BOTH directions. The comparator negates for 'desc', so one
106
+ // fixed sentinel would flip them to the top — the sentinel has to flip
107
+ // with the order to survive that negation.
108
+ // `dueDate` is already a ZeniDate (dayjs), so valueOf() is the epoch.
109
+ // An unparseable date yields NaN, and a NaN comparator leaves the array
110
+ // untouched -- the same silent failure the timeSpent branch guards.
111
+ // Treat it as undated rather than as a date.
112
+ const due = task.dueDate;
113
+ const epoch = due?.valueOf();
114
+ if (epoch == null || Number.isNaN(epoch)) {
115
+ return sortOrder === 'asc'
116
+ ? Number.MAX_SAFE_INTEGER
117
+ : Number.MIN_SAFE_INTEGER;
118
+ }
119
+ return epoch;
120
+ }
121
+ if (sortKey === 'timeSpent') {
122
+ // `timeSpent` is an "HHMM" STRING (no colon). Comparing it as text puts
123
+ // "1000" before "0900" only by luck of padding, and any malformed value
124
+ // makes the converter return NaN -- which a comparator turns into "no
125
+ // sort at all", silently. Coerce that to 0.
126
+ const minutes = convertHHMMStrToMinutes(task.timeSpent ?? '');
127
+ return Number.isFinite(minutes) ? minutes : 0;
128
+ }
92
129
  return '';
93
130
  };
94
131
  const copy = [...subtasks];
@@ -0,0 +1,24 @@
1
+ import { of } from 'rxjs';
2
+ import { catchError, filter, switchMap } from 'rxjs/operators';
3
+ import { mergeMap } from 'rxjs/operators';
4
+ import { updateTaskCategories } from '../../../../entity/timeEntry/timeEntryReducer';
5
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
6
+ import { fetchTaskCategories, fetchTaskCategoriesFailure, fetchTaskCategoriesSuccess, } from '../timeEntryViewReducer';
7
+ /**
8
+ * The 156-category catalog that fills the Category dropdown.
9
+ *
10
+ * `switchMap` here, unlike the entry epics: there is one catalog, so a second
11
+ * request supersedes the first rather than racing it.
12
+ *
13
+ * Zeni-only on the server (the billing marker is commercially sensitive), so
14
+ * a customer user gets a 401 — surfaced as Error rather than an empty list,
15
+ * because an empty catalog looks like "no categories exist".
16
+ */
17
+ export const fetchTaskCategoriesEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchTaskCategories.match), switchMap(() => zeniAPI
18
+ .getJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/task-categories`)
19
+ .pipe(mergeMap((response) => {
20
+ if (isSuccessResponse(response) && response.data != null) {
21
+ return of(updateTaskCategories(response.data.task_categories ?? []), fetchTaskCategoriesSuccess());
22
+ }
23
+ return of(fetchTaskCategoriesFailure(response.status));
24
+ }), catchError((error) => of(fetchTaskCategoriesFailure(createZeniAPIStatus('Unexpected Error', 'Fetch task categories errored out' + JSON.stringify(error))))))));
@@ -0,0 +1,73 @@
1
+ import { of } from 'rxjs';
2
+ import { catchError, filter, mergeMap } from 'rxjs/operators';
3
+ import { updateTimeEntries } from '../../../../entity/timeEntry/timeEntryReducer';
4
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
5
+ import { fetchTimeEntriesForRange, fetchTimeEntriesForRangeFailure, fetchTimeEntriesForRangeSuccess, fetchTimeEntriesForTask, fetchTimeEntriesForTaskFailure, fetchTimeEntriesForTaskSuccess, } from '../timeEntryViewReducer';
6
+ import { toTimeEntryRangeKey } from '../timeEntryViewState';
7
+ /**
8
+ * Every entry on one task, all users — the task detail breakdown.
9
+ *
10
+ * `mergeMap`, not `switchMap`: a detail page and an expanded board row can
11
+ * ask for two different tasks at once, and cancelling the first would leave
12
+ * its breakdown stuck In-Progress forever.
13
+ */
14
+ export const fetchTimeEntriesForTaskEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchTimeEntriesForTask.match), mergeMap((action) => {
15
+ const { taskId, cacheOverride } = action.payload;
16
+ const cached = state$.value.timeEntryViewState.timeEntryIdsByTaskId[taskId];
17
+ const status = state$.value.timeEntryViewState.fetchStateByTaskId[taskId];
18
+ // The reducer has already flipped this request to In-Progress, so the
19
+ // guard reads the COMPLETED cache, never the fetch state -- a fetch
20
+ // state is not an in-flight signal.
21
+ if (cacheOverride !== true &&
22
+ cached != null &&
23
+ status?.fetchState === 'In-Progress') {
24
+ return of(fetchTimeEntriesForTaskSuccess({ taskId, timeEntryIds: cached }));
25
+ }
26
+ return zeniAPI
27
+ .getJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/tasks/${taskId}/time-entries`)
28
+ .pipe(mergeMap((response) => {
29
+ if (isSuccessResponse(response) && response.data != null) {
30
+ const entries = response.data.time_entries ?? [];
31
+ return of(updateTimeEntries(entries), fetchTimeEntriesForTaskSuccess({
32
+ taskId,
33
+ timeEntryIds: entries.map((entry) => entry.time_entry_id),
34
+ }));
35
+ }
36
+ return of(fetchTimeEntriesForTaskFailure({ taskId, status: response.status }));
37
+ }), catchError((error) => of(fetchTimeEntriesForTaskFailure({
38
+ taskId,
39
+ status: createZeniAPIStatus('Unexpected Error', 'Fetch time entries for task errored out' +
40
+ JSON.stringify(error)),
41
+ }))));
42
+ }));
43
+ /**
44
+ * One user's entries in an inclusive date range — the weekly timesheet.
45
+ *
46
+ * Needs the `is_deleted + user_id + entry_date` composite index (api #593).
47
+ * Without it the server returns 500, not an empty list, so the failure is
48
+ * surfaced rather than rendered as a blank week.
49
+ */
50
+ export const fetchTimeEntriesForRangeEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchTimeEntriesForRange.match), mergeMap((action) => {
51
+ const { userId, startDate, endDate } = action.payload;
52
+ const rangeKey = toTimeEntryRangeKey(userId, startDate, endDate);
53
+ const query = `user_id=${encodeURIComponent(userId)}&start_date=${encodeURIComponent(startDate)}&end_date=${encodeURIComponent(endDate)}`;
54
+ return zeniAPI
55
+ .getJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/time-entries?${query}`)
56
+ .pipe(mergeMap((response) => {
57
+ if (isSuccessResponse(response) && response.data != null) {
58
+ const entries = response.data.time_entries ?? [];
59
+ return of(updateTimeEntries(entries), fetchTimeEntriesForRangeSuccess({
60
+ rangeKey,
61
+ timeEntryIds: entries.map((entry) => entry.time_entry_id),
62
+ }));
63
+ }
64
+ return of(fetchTimeEntriesForRangeFailure({
65
+ rangeKey,
66
+ status: response.status,
67
+ }));
68
+ }), catchError((error) => of(fetchTimeEntriesForRangeFailure({
69
+ rangeKey,
70
+ status: createZeniAPIStatus('Unexpected Error', 'Fetch time entries for range errored out' +
71
+ JSON.stringify(error)),
72
+ }))));
73
+ }));