@zeniai/client-epic-state 5.3.7-betaRD0 → 5.3.8-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 (73) 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/tenant/epic/fetchAllTenantsEpic.js +38 -45
  7. package/lib/entity/timeEntry/timeEntryPayload.d.ts +26 -0
  8. package/lib/entity/timeEntry/timeEntryPayload.js +2 -0
  9. package/lib/entity/timeEntry/timeEntryReducer.d.ts +8 -0
  10. package/lib/entity/timeEntry/timeEntryReducer.js +76 -0
  11. package/lib/entity/timeEntry/timeEntrySelector.d.ts +10 -0
  12. package/lib/entity/timeEntry/timeEntrySelector.js +32 -0
  13. package/lib/entity/timeEntry/timeEntryState.d.ts +43 -0
  14. package/lib/entity/timeEntry/timeEntryState.js +13 -0
  15. package/lib/epic.d.ts +4 -1
  16. package/lib/epic.js +9 -0
  17. package/lib/esm/entity/task/taskPayload.js +1 -0
  18. package/lib/esm/entity/task/taskReducer.js +18 -1
  19. package/lib/esm/entity/tenant/epic/fetchAllTenantsEpic.js +39 -46
  20. package/lib/esm/entity/timeEntry/timeEntryPayload.js +1 -0
  21. package/lib/esm/entity/timeEntry/timeEntryReducer.js +70 -0
  22. package/lib/esm/entity/timeEntry/timeEntrySelector.js +23 -0
  23. package/lib/esm/entity/timeEntry/timeEntryState.js +8 -0
  24. package/lib/esm/epic.js +9 -0
  25. package/lib/esm/index.js +6 -3
  26. package/lib/esm/init.js +3 -14
  27. package/lib/esm/reducer.js +6 -0
  28. package/lib/esm/view/taskManager/taskDetailView/taskDetailSelector.js +3 -0
  29. package/lib/esm/view/taskManager/taskListView/taskList.js +5 -0
  30. package/lib/esm/view/taskManager/taskListView/taskListFilterHelpers.js +2 -0
  31. package/lib/esm/view/taskManager/taskListView/taskListViewHelpers.js +37 -0
  32. package/lib/esm/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.js +24 -0
  33. package/lib/esm/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.js +73 -0
  34. package/lib/esm/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.js +77 -0
  35. package/lib/esm/view/taskManager/timeEntryView/timeEntryViewPayload.js +1 -0
  36. package/lib/esm/view/taskManager/timeEntryView/timeEntryViewReducer.js +160 -0
  37. package/lib/esm/view/taskManager/timeEntryView/timeEntryViewSelector.js +41 -0
  38. package/lib/esm/view/taskManager/timeEntryView/timeEntryViewState.js +1 -0
  39. package/lib/index.d.ts +7 -4
  40. package/lib/index.js +66 -53
  41. package/lib/init.d.ts +0 -9
  42. package/lib/init.js +4 -17
  43. package/lib/reducer.d.ts +2 -0
  44. package/lib/reducer.js +6 -0
  45. package/lib/rootStateTypes.d.ts +4 -0
  46. package/lib/view/taskManager/taskDetailView/taskDetailSelector.d.ts +4 -0
  47. package/lib/view/taskManager/taskDetailView/taskDetailSelector.js +3 -0
  48. package/lib/view/taskManager/taskListView/taskList.d.ts +1 -1
  49. package/lib/view/taskManager/taskListView/taskList.js +5 -0
  50. package/lib/view/taskManager/taskListView/taskListFilterHelpers.js +2 -0
  51. package/lib/view/taskManager/taskListView/taskListViewHelpers.d.ts +1 -1
  52. package/lib/view/taskManager/taskListView/taskListViewHelpers.js +37 -0
  53. package/lib/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.d.ts +18 -0
  54. package/lib/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.js +28 -0
  55. package/lib/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.d.ts +23 -0
  56. package/lib/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.js +78 -0
  57. package/lib/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.d.ts +23 -0
  58. package/lib/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.js +83 -0
  59. package/lib/view/taskManager/timeEntryView/timeEntryViewPayload.d.ts +27 -0
  60. package/lib/view/taskManager/timeEntryView/timeEntryViewPayload.js +2 -0
  61. package/lib/view/taskManager/timeEntryView/timeEntryViewReducer.d.ts +48 -0
  62. package/lib/view/taskManager/timeEntryView/timeEntryViewReducer.js +164 -0
  63. package/lib/view/taskManager/timeEntryView/timeEntryViewSelector.d.ts +24 -0
  64. package/lib/view/taskManager/timeEntryView/timeEntryViewSelector.js +46 -0
  65. package/lib/view/taskManager/timeEntryView/timeEntryViewState.d.ts +21 -0
  66. package/lib/view/taskManager/timeEntryView/timeEntryViewState.js +5 -0
  67. package/lib/zeniAPI.d.ts +2 -2
  68. package/package.json +1 -26
  69. package/lib/esm/sessionEndpointsLoader.js +0 -7
  70. package/lib/sessionEndpointsLoader.d.ts +0 -9
  71. package/lib/sessionEndpointsLoader.js +0 -11
  72. package/lib/tsconfig.mocks.tsbuildinfo +0 -1
  73. package/lib/tsconfig.typecheck.tsbuildinfo +0 -1
@@ -1,4 +1,5 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
+ import { convertMinutesToHHMM } from '../../commonStateTypes/fiscalYearHelpers/formatMinutesToFromHHMM';
2
3
  import { clearAll } from '../../rootActions';
3
4
  import { mapTaskPayloadToTask } from './taskPayload';
4
5
  export const initialState = {
@@ -23,6 +24,22 @@ const task = createSlice({
23
24
  removeTask(draft, action) {
24
25
  delete draft.taskByID[action.payload];
25
26
  },
27
+ /**
28
+ * Set one task's rolled-up total from the minutes the time-entry API
29
+ * returns. A whole-task update is not available on that path -- the write
30
+ * response carries the recomputed minutes, not a TaskPayload -- and
31
+ * refetching the task just to learn one number would put a round trip on
32
+ * the keystroke that logged the time.
33
+ *
34
+ * No-op when the task is absent: the ancestors the server recomputed can
35
+ * include a parent that is not on screen and so not in the store.
36
+ */
37
+ updateTaskTimeSpentMinutes(draft, action) {
38
+ const taskInStore = draft.taskByID[action.payload.taskId];
39
+ if (taskInStore != null) {
40
+ taskInStore.timeSpent = convertMinutesToHHMM(action.payload.minutes);
41
+ }
42
+ },
26
43
  appendSubTaskId(draft, action) {
27
44
  const parentTask = draft.taskByID[action.payload.parentTaskId];
28
45
  if (parentTask != null) {
@@ -53,5 +70,5 @@ const task = createSlice({
53
70
  });
54
71
  },
55
72
  });
56
- export const { updateTasks, updateTask, removeTask, appendSubTaskId, setSubTaskIds, } = task.actions;
73
+ export const { updateTasks, updateTask, removeTask, updateTaskTimeSpentMinutes, appendSubTaskId, setSubTaskIds, } = task.actions;
57
74
  export default task.reducer;
@@ -1,8 +1,7 @@
1
- import { defer, from, of } from 'rxjs';
1
+ import { from, of } from 'rxjs';
2
2
  import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
3
  import { toMonth } from '../../../commonStateTypes/timePeriod';
4
4
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
5
- import { getSessionEndpointsLoader } from '../../../sessionEndpointsLoader';
6
5
  import { initializeSelectedPeriodForTenants } from '../../../view/expenseAutomationView/expenseAutomationViewReducer';
7
6
  import { date, dateNow } from '../../../zeniDayJS';
8
7
  import { updateCompanies } from '../../company/companyReducer';
@@ -11,54 +10,48 @@ export const fetchAllTenantsEpic = (actions$, state, zeniAPI) => {
11
10
  const queryParam = {
12
11
  is_include_companies_in_response: true,
13
12
  };
14
- return actions$.pipe(filter(fetchAllTenants.match), switchMap((action) => {
15
- const fetchTenants$ = defer(() => zeniAPI.getJSON(`${zeniAPI.apiEndPoints.tenantMicroServiceBaseUrl}/1.0/users/${action.payload.userId}/tenants?query=${encodeURIComponent(JSON.stringify(queryParam))}`, Object.assign({ 'zeni-user-id': action.payload.userId }, { 'zeni-tenant-id': '' }, action.payload.zeniSessionId != null
16
- ? { 'zeni-session-id': action.payload.zeniSessionId }
17
- : {}))).pipe(mergeMap((response) => {
18
- const currentTenantId = state.value.tenantState.currentTenantId;
19
- if (isSuccessResponse(response) && response.data != null) {
20
- const defaultSelectedPeriodsForTenants = getDefaultSelectedPeriodsForTenants(response.data);
21
- let actions = [
22
- updateTenantsSuccess(response.data),
23
- initializeSelectedPeriodForTenants({
24
- selectedPeriodByTenantId: defaultSelectedPeriodsForTenants,
25
- }),
26
- ];
27
- if (response.data.companies != null) {
28
- let companiesPayload = response.data.companies;
29
- if (currentTenantId != null) {
30
- // filter out this tenant ID related company detail
31
- // Find companyID for this tenant
32
- const currentTenantPayload = response.data.tenants.find((tenant) => tenant.tenant_id === currentTenantId);
33
- if (currentTenantPayload != null) {
34
- companiesPayload = response.data.companies.filter((company) => company.company_id !== currentTenantPayload.company_id);
35
- }
36
- else {
37
- console.warn('Selected tenant record not found in response', currentTenantId);
38
- }
13
+ return actions$.pipe(filter(fetchAllTenants.match), switchMap((action) => zeniAPI
14
+ .getJSON(`${zeniAPI.apiEndPoints.tenantMicroServiceBaseUrl}/1.0/users/${action.payload.userId}/tenants?query=${encodeURIComponent(JSON.stringify(queryParam))}`, Object.assign({ 'zeni-user-id': action.payload.userId }, { 'zeni-tenant-id': '' }, action.payload.zeniSessionId != null
15
+ ? { 'zeni-session-id': action.payload.zeniSessionId }
16
+ : {}))
17
+ .pipe(mergeMap((response) => {
18
+ const currentTenantId = state.value.tenantState.currentTenantId;
19
+ if (isSuccessResponse(response) && response.data != null) {
20
+ const defaultSelectedPeriodsForTenants = getDefaultSelectedPeriodsForTenants(response.data);
21
+ let actions = [
22
+ updateTenantsSuccess(response.data),
23
+ initializeSelectedPeriodForTenants({
24
+ selectedPeriodByTenantId: defaultSelectedPeriodsForTenants,
25
+ }),
26
+ ];
27
+ if (response.data.companies != null) {
28
+ let companiesPayload = response.data.companies;
29
+ if (currentTenantId != null) {
30
+ // filter out this tenant ID related company detail
31
+ // Find companyID for this tenant
32
+ const currentTenantPayload = response.data.tenants.find((tenant) => tenant.tenant_id === currentTenantId);
33
+ if (currentTenantPayload != null) {
34
+ companiesPayload = response.data.companies.filter((company) => company.company_id !== currentTenantPayload.company_id);
35
+ }
36
+ else {
37
+ console.warn('Selected tenant record not found in response', currentTenantId);
39
38
  }
40
- actions = [
41
- updateCompanies({
42
- payload: companiesPayload,
43
- schema: {},
44
- }),
45
- ...actions,
46
- ];
47
39
  }
48
- return from(actions);
49
- }
50
- else {
51
- return of(updateTenantsFailure(response.status));
40
+ actions = [
41
+ updateCompanies({
42
+ payload: companiesPayload,
43
+ schema: {},
44
+ }),
45
+ ...actions,
46
+ ];
52
47
  }
53
- }), catchError((error) => of(updateTenantsFailure(createZeniAPIStatus('Unexpected error', 'Fetch All Tenants REST API call errored out' +
54
- JSON.stringify(error))))));
55
- const sessionEndpointsLoader = getSessionEndpointsLoader();
56
- if (sessionEndpointsLoader == null) {
57
- return fetchTenants$;
48
+ return from(actions);
49
+ }
50
+ else {
51
+ return of(updateTenantsFailure(response.status));
58
52
  }
59
- return defer(() => from(sessionEndpointsLoader(action.payload.userId, action.payload.zeniSessionId))).pipe(switchMap(() => fetchTenants$), catchError((error) => of(updateTenantsFailure(createZeniAPIStatus('Unexpected error', 'Loading session endpoints errored out: ' +
60
- JSON.stringify(error))))));
61
- }));
53
+ }), catchError((error) => of(updateTenantsFailure(createZeniAPIStatus('Unexpected error', 'Fetch All Tenants REST API call errored out' +
54
+ JSON.stringify(error))))))));
62
55
  };
63
56
  export const defaultMonthYearPeriod = {
64
57
  month: toMonth(dateNow().get('month') + 1),
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ import { createSlice } from '@reduxjs/toolkit';
2
+ import { clearAll } from '../../rootActions';
3
+ import { date } from '../../zeniDayJS';
4
+ import { toBillingType, toTimeEntrySource, } from './timeEntryState';
5
+ export const initialState = {
6
+ timeEntriesById: {},
7
+ taskCategoriesById: {},
8
+ };
9
+ const timeEntry = createSlice({
10
+ name: 'timeEntry',
11
+ initialState,
12
+ reducers: {
13
+ updateTimeEntries(draft, action) {
14
+ action.payload.forEach((payload) => {
15
+ const latest = mapTimeEntryPayloadToTimeEntry(payload);
16
+ const inStore = draft.timeEntriesById[latest.timeEntryId];
17
+ // Last-write-wins by updateTime, same as `address`. A weekly fetch and
18
+ // a single-entry edit can land in either order.
19
+ if (inStore == null || latest.updateTime.isAfter(inStore.updateTime)) {
20
+ draft.timeEntriesById[latest.timeEntryId] = latest;
21
+ }
22
+ });
23
+ },
24
+ /** A soft delete comes back as a normal entry; the server has already
25
+ * dropped it from the totals, so the client drops it from the store
26
+ * rather than keeping a row it must then remember to filter. */
27
+ removeTimeEntry(draft, action) {
28
+ delete draft.timeEntriesById[action.payload];
29
+ },
30
+ updateTaskCategories(draft, action) {
31
+ action.payload.forEach((payload) => {
32
+ const category = mapTaskCategoryPayloadToTaskCategory(payload);
33
+ draft.taskCategoriesById[category.taskCategoryId] = category;
34
+ });
35
+ },
36
+ },
37
+ extraReducers: (builder) => {
38
+ builder.addCase(clearAll, (draft) => {
39
+ Object.assign(draft, initialState);
40
+ });
41
+ },
42
+ });
43
+ export const { updateTimeEntries, removeTimeEntry, updateTaskCategories } = timeEntry.actions;
44
+ export default timeEntry.reducer;
45
+ export function mapTimeEntryPayloadToTimeEntry(payload) {
46
+ return {
47
+ timeEntryId: payload.time_entry_id,
48
+ taskId: payload.task_id,
49
+ userId: payload.user_id,
50
+ minutes: payload.minutes,
51
+ entryDate: date(payload.entry_date),
52
+ source: toTimeEntrySource(payload.source),
53
+ createTime: date(payload.create_time),
54
+ updateTime: date(payload.update_time),
55
+ billingType: toBillingType(payload.billing_type),
56
+ createdBy: payload.created_by ?? undefined,
57
+ department: payload.department ?? undefined,
58
+ taskCategoryId: payload.task_category_id ?? undefined,
59
+ };
60
+ }
61
+ export function mapTaskCategoryPayloadToTaskCategory(payload) {
62
+ return {
63
+ taskCategoryId: payload.task_category_id,
64
+ name: payload.name,
65
+ isActive: payload.is_active,
66
+ billingType: toBillingType(payload.billing_type),
67
+ service: payload.service ?? undefined,
68
+ vertical: payload.vertical ?? undefined,
69
+ };
70
+ }
@@ -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
@@ -729,6 +729,9 @@ import { dragNDropTasksEpic, } from './view/taskManager/taskListView/epics/dragN
729
729
  import { fetchTaskListEpic, } from './view/taskManager/taskListView/epics/fetchTaskListEpic';
730
730
  import { fetchTaskListPageEpic, } from './view/taskManager/taskListView/epics/fetchTaskListPageEpic';
731
731
  import { updateTaskFromListViewEpic, } from './view/taskManager/taskListView/epics/updateTaskFromListViewEpic';
732
+ import { fetchTaskCategoriesEpic, } from './view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic';
733
+ import { fetchTimeEntriesForRangeEpic, fetchTimeEntriesForTaskEpic, } from './view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic';
734
+ import { deleteTimeEntryEpic, logTimeEpic, updateTimeEntryEpic, } from './view/taskManager/timeEntryView/epic/writeTimeEntryEpic';
732
735
  import { fetchTasksCardEpic, } from './view/tasksCard/fetchTasksCardEpic';
733
736
  import { fetchTopExEpic, } from './view/topEx/topExEpic';
734
737
  import { fetchTransactionActivityLogEpic, } from './view/transactionActivityLogView/fetchTransactionActivityLogEpic';
@@ -880,6 +883,7 @@ const ALL_EPICS = [
880
883
  deleteTagEpic,
881
884
  deleteTaskEpic,
882
885
  deleteTaskGroupEpic,
886
+ deleteTimeEntryEpic,
883
887
  deleteTransactionAttachmentEpic,
884
888
  deleteUserBankAccountEpic,
885
889
  disableAccountingProjectsEpic,
@@ -1230,6 +1234,7 @@ const ALL_EPICS = [
1230
1234
  fetchSubscriptionViewEpic,
1231
1235
  fetchSubTasksEpic,
1232
1236
  fetchSuggestedQuestionsEpic,
1237
+ fetchTaskCategoriesEpic,
1233
1238
  fetchTaskDetailEpic,
1234
1239
  fetchTaskDetailPageEpic,
1235
1240
  fetchTaskGroupTemplatesEpic,
@@ -1238,6 +1243,8 @@ const ALL_EPICS = [
1238
1243
  fetchTaskListPageEpic,
1239
1244
  fetchTaskManagerMetricsEpic,
1240
1245
  fetchTasksCardEpic,
1246
+ fetchTimeEntriesForRangeEpic,
1247
+ fetchTimeEntriesForTaskEpic,
1241
1248
  fetchTopExEpic,
1242
1249
  fetchTransactionActivityLogEpic,
1243
1250
  fetchTransactionDetailEpic,
@@ -1330,6 +1337,7 @@ const ALL_EPICS = [
1330
1337
  issueCreditNoteEpic,
1331
1338
  lockChargeCardEpic,
1332
1339
  lockChargeCardsEpic,
1340
+ logTimeEpic,
1333
1341
  markAsCompleteScheduleDetailEpic,
1334
1342
  markTransactionAsNotMiscategorizedEpic,
1335
1343
  matchAndReverseAccrualsEpic,
@@ -1554,6 +1562,7 @@ const ALL_EPICS = [
1554
1562
  updateStatementInfoEpic,
1555
1563
  updateTaskFromListViewEpic,
1556
1564
  updateTaskGroupNameEpic,
1565
+ updateTimeEntryEpic,
1557
1566
  updateTransactionDetailEpic,
1558
1567
  updateVendorContactEpic,
1559
1568
  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';
@@ -90,7 +91,7 @@ import { getUserRoleMap, hasAICFOAccess, hasAdminLevelAccess, hasAnyCompanyRole,
90
91
  import { updateVendors } from './entity/vendor/vendorReducer';
91
92
  import { defaultVendorCurrency, } from './entity/vendor/vendorState';
92
93
  import { VendorExpenseTrend } from './entity/vendorExpense/vendorExpenseTrend';
93
- import initialize, { disconnectPusher, initializePusher, initializeWithNewStore, injectFeatureEpics, injectFeatureModules, setSessionEndpointsLoader, updateZeniAPIClientConfig, updateZeniAPIEndpoints, zeniAPIClientConfig, } from './init';
94
+ import initialize, { disconnectPusher, initializePusher, initializeWithNewStore, injectFeatureEpics, injectFeatureModules, updateZeniAPIClientConfig, zeniAPIClientConfig, } from './init';
94
95
  import { isAccessDeniedError, isInvalidSessionError, isSuccessResponse, } from './responsePayload';
95
96
  import { clearAll } from './rootActions';
96
97
  import { fetchAccountList } from './view/accountList/accountListReducer';
@@ -421,6 +422,8 @@ import { ALL_TASK_LIST_TABS, TASK_LIST_FILTER_CATEGORIES, TASK_LIST_GROUP_BY_CAT
421
422
  import { bulkUpdateTaskList, dragNDropTasks, fetchTaskList, fetchTaskListPage, initiateTaskListLocalData, removeTaskFromList, updateTaskFilters, updateTaskFromListView, updateTaskListLocalData, updateTaskListSearchText, updateTaskListTab, updateTaskListUIState, } from './view/taskManager/taskListView/taskListReducer';
422
423
  import { getAllTasks, } from './view/taskManager/taskListView/taskListSelector';
423
424
  import { getDueDateValueFromDueDateGroupId, getTaskUpdates, sortSubtasks, } from './view/taskManager/taskListView/taskListViewHelpers';
425
+ import { deleteTimeEntry, fetchTaskCategories, fetchTimeEntriesForRange, fetchTimeEntriesForTask, logTime, updateTimeEntryMinutes, } from './view/taskManager/timeEntryView/timeEntryViewReducer';
426
+ import { getTimeEntriesForRange, getTimeEntriesForTask, getTimeEntrySaveState, } from './view/taskManager/timeEntryView/timeEntryViewSelector';
424
427
  import { fetchTasksCard } from './view/tasksCard/tasksCardReducer';
425
428
  import { fetchTopEx, updateTopExDownloadState, updateTopExTimeFrame, } from './view/topEx/topExReducer';
426
429
  import { getAllTopExpenses, getTop6Expenses, getTopExpenses, } from './view/topEx/topExSelector';
@@ -485,7 +488,7 @@ export { updateCreditAgentMacro, updateCreditAgentRow, updateCreditAgentRows, }
485
488
  export { getCreditAgentRows, getCreditAgentMacroEntity, } from './entity/creditAgent/creditAgentSelector';
486
489
  export { saveJeAccountSettings, saveJeAccountSettingsLocalData };
487
490
  export default initialize;
488
- export { initializePusher, disconnectPusher, Dayjs, dateInLocal as zeniDateInLocal, date as zeniDate, dateLocal, dateNow, getBusinessDayOfDate, formatZeniDateFY as formatZeniDate, getFYMonths, getFYQuarterAndYear, getStartOfAndEndOfTimeframeForFY, getActualPeriodOfFYQtr, getActualPeriodOfFY, getLocalTimezone, addPeriod, subtractPeriod, setLocalTimezone, updateZeniDateLocaleID, toZeniUrl, toZeniUrlWithoutBaseURL, updateZeniAPIClientConfig, updateZeniAPIEndpoints, setSessionEndpointsLoader, zeniAPIClientConfig, injectFeatureEpics, injectFeatureModules, initializeWithNewStore, getTimeframeTickTag, isIncompleteMonthPresentInTimeframeTick, getLastMonthOfFYQuarter, getLastMonthOfFYYear, getMonthIndex, dateFromYearMonthDate, getMinDate, };
491
+ export { initializePusher, disconnectPusher, Dayjs, dateInLocal as zeniDateInLocal, date as zeniDate, dateLocal, dateNow, getBusinessDayOfDate, formatZeniDateFY as formatZeniDate, getFYMonths, getFYQuarterAndYear, getStartOfAndEndOfTimeframeForFY, getActualPeriodOfFYQtr, getActualPeriodOfFY, getLocalTimezone, addPeriod, subtractPeriod, setLocalTimezone, updateZeniDateLocaleID, toZeniUrl, toZeniUrlWithoutBaseURL, updateZeniAPIClientConfig, zeniAPIClientConfig, injectFeatureEpics, injectFeatureModules, initializeWithNewStore, getTimeframeTickTag, isIncompleteMonthPresentInTimeframeTick, getLastMonthOfFYQuarter, getLastMonthOfFYYear, getMonthIndex, dateFromYearMonthDate, getMinDate, };
489
492
  export { toAmount };
490
493
  export { toReportID, toReportFormatStrict, isCashFlowOrBalanceSheetReport, isBalanceSheetReport, isBalanceSheetClassesViewReport, isBalanceSheetProjectViewReport, isCashFlowClassesViewReport, isCashFlowProjectViewReport, isCashFlowReport, isGroupedCashFlowReport, isGroupedBalanceSheetReport, isDashboardReport, isDashboardClassesViewReport, isDashboardProjectViewReport, isPAndLReport, isPAndLClassesViewReport, isPAndLProjectViewReport, isAgingReport, isOpExByVendorReport, isFluxAnalysisOpExReport, };
491
494
  export { toCOABalanceTimeframe, };
@@ -678,7 +681,7 @@ export { fetchGlobalMerchantRecommendation, createGlobalMerchant, updateCreateGl
678
681
  export { approveVendorGlobalReview, rejectVendorGlobalReview, fetchVendorGlobalReviewView, updateSelectedGlobalMerchant, updateVendorGlobalReviewViewUIState, getVendorGlobalReviewView, getTenantMerchantByMerchantId, toVendorGlobalReviewColumnSortKeyType, updateVendorGlobalReviewViewLocalData, };
679
682
  export { fetchArAging, updateArAgingUIState, fetchArAgingDetail, updateArAgingDetailUIState, getArAgingReport, getArAgingDetailForCustomer, updateArAgingNodeCollapseState, };
680
683
  export { toRecurringBillFrequency, toRecurringBillFrequencyStrict, };
681
- 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, };
684
+ 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, };
682
685
  export { getAllTags, fetchTagList, createTag, deleteTag, };
683
686
  export { getAllCardsAndBankPaymentMethods, createCardSetupIntent, confirmCardSetupIntent, addCardPaymentSource, fetchPaymentSources, resetCardPaymentErrorStatuses, clearCardPaymentView, };
684
687
  export { getAuditReportGroupViewSelectorView, getAuditRuleGroupViewSelectorView, getUserFromAllUsers, fetchAuditRuleGroupView, fetchAuditReportGroupView, saveReasonForAuditRule, clearAuditReportGroupViewByCompanyId, };
package/lib/esm/init.js CHANGED
@@ -2,7 +2,6 @@ import configureNewStore from './configureStore';
2
2
  import { ZeniAPIClient } from './zeniAPI';
3
3
  import { setZeniAPIClient, zeniAPI } from './zeniAPIClientState';
4
4
  export { zeniAPI } from './zeniAPIClientState';
5
- export { setSessionEndpointsLoader } from './sessionEndpointsLoader';
6
5
  export { disconnectPusher, initializePusher, pusher } from './pusherClient';
7
6
  export let store;
8
7
  let initialized = false;
@@ -14,10 +13,11 @@ let initialized = false;
14
13
  */
15
14
  export default function initialize(restEndPoints, userId, sessionId, tenantId, isSandboxEnv, onSessionInvalidated) {
16
15
  if (initialized) {
17
- console.warn('client-epic-state is already initialized');
16
+ console.warn(`Already initialized with url ${JSON.stringify(zeniAPI.apiEndPoints)}`);
18
17
  return store;
19
18
  }
20
19
  initialized = true;
20
+ console.info(`initializing with ${JSON.stringify(restEndPoints)}`);
21
21
  initialize3rdPartyExtensions();
22
22
  setZeniAPIClient(new ZeniAPIClient(restEndPoints, userId, sessionId, tenantId, onSessionInvalidated));
23
23
  store = configureNewStore(zeniAPI, isSandboxEnv);
@@ -35,18 +35,6 @@ export { injectFeatureModules, injectFeatureEpics } from './configureStore';
35
35
  export function updateZeniAPIClientConfig(userId, sessionId, tenantId) {
36
36
  zeniAPI.update(userId, sessionId, tenantId);
37
37
  }
38
- /**
39
- * Replace the REST endpoints the API client calls. Epics read the endpoints
40
- * at request time, so every request made after this uses the new ones.
41
- * Note: This version SHOULD BE used only in the client side.
42
- * @param restEndPoints
43
- */
44
- export function updateZeniAPIEndpoints(restEndPoints) {
45
- if (zeniAPI == null) {
46
- throw new Error('Call initialize() before updateZeniAPIEndpoints()');
47
- }
48
- zeniAPI.apiEndPoints = restEndPoints;
49
- }
50
38
  /**
51
39
  * Returns current Zeni REST API client config.
52
40
  * @returns
@@ -73,6 +61,7 @@ export function zeniAPIClientConfig() {
73
61
  * @param tenantId tenant id
74
62
  */
75
63
  export async function initializeWithNewStore(restEndPoints, userId, sessionId, tenantId) {
64
+ console.info(`initializeWithNewStore with ${JSON.stringify(restEndPoints)}`);
76
65
  initialize3rdPartyExtensions();
77
66
  const zeniHttpClient = new ZeniAPIClient(restEndPoints, userId, sessionId, tenantId);
78
67
  const store = configureNewStore(zeniHttpClient, false);
@@ -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';
@@ -255,6 +256,7 @@ import taskDetailView, { initialState as initialTaskDetailState, } from './view/
255
256
  import taskGroupTemplateView, { initialState as initialTaskGroupTemplateViewState, } from './view/taskManager/taskGroupTemplateView/taskGroupTemplateViewReducer';
256
257
  import taskGroupView, { initialState as initialTaskGroupViewState, } from './view/taskManager/taskGroupView/taskGroupViewReducer';
257
258
  import taskList, { initialState as initialTaskListState, } from './view/taskManager/taskListView/taskListReducer';
259
+ import timeEntryView, { initialState as initialTimeEntryViewState, } from './view/taskManager/timeEntryView/timeEntryViewReducer';
258
260
  import tasksCard, { initialState as initialTasksCardState, } from './view/tasksCard/tasksCardReducer';
259
261
  import topEx, { initialState as initialTopExState, } from './view/topEx/topExReducer';
260
262
  import transactionActivityLogView, { initialState as initialTransactionActivityLogViewState, } from './view/transactionActivityLogView/transactionActivityLogViewReducer';
@@ -343,6 +345,7 @@ const initialEntitiesState = {
343
345
  subscriptionState: initialSubscriptionState,
344
346
  subscriptionSummaryState: initialSubscriptionSummaryState,
345
347
  tagState: initialTagState,
348
+ timeEntryState: initialTimeEntryState,
346
349
  taskGroupState: initialTaskGroupState,
347
350
  taskGroupTemplateState: initialTaskGroupTemplateState,
348
351
  taskState: initialTaskState,
@@ -524,6 +527,7 @@ const initialViewsState = {
524
527
  taskGroupTemplateViewState: initialTaskGroupTemplateViewState,
525
528
  taskGroupViewState: initialTaskGroupViewState,
526
529
  taskListState: initialTaskListState,
530
+ timeEntryViewState: initialTimeEntryViewState,
527
531
  tasksCardState: initialTasksCardState,
528
532
  topExState: initialTopExState,
529
533
  transactionActivityLogViewState: initialTransactionActivityLogViewState,
@@ -632,6 +636,7 @@ const entityReducers = {
632
636
  subscriptionState: subscription,
633
637
  subscriptionSummaryState: subscriptionSummary,
634
638
  tagState: tag,
639
+ timeEntryState: timeEntry,
635
640
  taskGroupState: taskGroup,
636
641
  taskGroupTemplateState: taskGroupTemplate,
637
642
  taskState: task,
@@ -812,6 +817,7 @@ const viewReducers = {
812
817
  taskGroupTemplateViewState: taskGroupTemplateView,
813
818
  taskGroupViewState: taskGroupView,
814
819
  taskListState: taskList,
820
+ timeEntryViewState: timeEntryView,
815
821
  tagViewState: tagView,
816
822
  tasksCardState: tasksCard,
817
823
  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,6 +47,11 @@ 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 },
51
56
  ];
52
57
  export const TASK_LIST_GROUP_BY_CATEGORIES = [
@@ -12,6 +12,8 @@ export const getCategoryValueForTaskList = (key, task) => {
12
12
  return task.createTime;
13
13
  case 'assignee':
14
14
  return task.assignees;
15
+ case 'createdBy':
16
+ return task.createdBy;
15
17
  default:
16
18
  return undefined;
17
19
  }
@@ -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))))))));