@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.
- package/lib/entity/task/taskPayload.d.ts +1 -0
- package/lib/entity/task/taskPayload.js +1 -0
- package/lib/entity/task/taskReducer.d.ts +4 -1
- package/lib/entity/task/taskReducer.js +19 -2
- package/lib/entity/task/taskState.d.ts +3 -0
- package/lib/entity/tenant/epic/fetchAllTenantsEpic.js +38 -45
- package/lib/entity/timeEntry/timeEntryPayload.d.ts +26 -0
- package/lib/entity/timeEntry/timeEntryPayload.js +2 -0
- package/lib/entity/timeEntry/timeEntryReducer.d.ts +8 -0
- package/lib/entity/timeEntry/timeEntryReducer.js +76 -0
- package/lib/entity/timeEntry/timeEntrySelector.d.ts +10 -0
- package/lib/entity/timeEntry/timeEntrySelector.js +32 -0
- package/lib/entity/timeEntry/timeEntryState.d.ts +43 -0
- package/lib/entity/timeEntry/timeEntryState.js +13 -0
- package/lib/epic.d.ts +4 -1
- package/lib/epic.js +9 -0
- package/lib/esm/entity/task/taskPayload.js +1 -0
- package/lib/esm/entity/task/taskReducer.js +18 -1
- package/lib/esm/entity/tenant/epic/fetchAllTenantsEpic.js +39 -46
- package/lib/esm/entity/timeEntry/timeEntryPayload.js +1 -0
- package/lib/esm/entity/timeEntry/timeEntryReducer.js +70 -0
- package/lib/esm/entity/timeEntry/timeEntrySelector.js +23 -0
- package/lib/esm/entity/timeEntry/timeEntryState.js +8 -0
- package/lib/esm/epic.js +9 -0
- package/lib/esm/index.js +6 -3
- package/lib/esm/init.js +3 -14
- package/lib/esm/reducer.js +6 -0
- package/lib/esm/view/taskManager/taskDetailView/taskDetailSelector.js +3 -0
- package/lib/esm/view/taskManager/taskListView/taskList.js +5 -0
- package/lib/esm/view/taskManager/taskListView/taskListFilterHelpers.js +2 -0
- package/lib/esm/view/taskManager/taskListView/taskListViewHelpers.js +37 -0
- package/lib/esm/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.js +24 -0
- package/lib/esm/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.js +73 -0
- package/lib/esm/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.js +77 -0
- package/lib/esm/view/taskManager/timeEntryView/timeEntryViewPayload.js +1 -0
- package/lib/esm/view/taskManager/timeEntryView/timeEntryViewReducer.js +160 -0
- package/lib/esm/view/taskManager/timeEntryView/timeEntryViewSelector.js +41 -0
- package/lib/esm/view/taskManager/timeEntryView/timeEntryViewState.js +1 -0
- package/lib/index.d.ts +7 -4
- package/lib/index.js +66 -53
- package/lib/init.d.ts +0 -9
- package/lib/init.js +4 -17
- package/lib/reducer.d.ts +2 -0
- package/lib/reducer.js +6 -0
- package/lib/rootStateTypes.d.ts +4 -0
- package/lib/view/taskManager/taskDetailView/taskDetailSelector.d.ts +4 -0
- package/lib/view/taskManager/taskDetailView/taskDetailSelector.js +3 -0
- package/lib/view/taskManager/taskListView/taskList.d.ts +1 -1
- package/lib/view/taskManager/taskListView/taskList.js +5 -0
- package/lib/view/taskManager/taskListView/taskListFilterHelpers.js +2 -0
- package/lib/view/taskManager/taskListView/taskListViewHelpers.d.ts +1 -1
- package/lib/view/taskManager/taskListView/taskListViewHelpers.js +37 -0
- package/lib/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.d.ts +18 -0
- package/lib/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.js +28 -0
- package/lib/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.d.ts +23 -0
- package/lib/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.js +78 -0
- package/lib/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.d.ts +23 -0
- package/lib/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.js +83 -0
- package/lib/view/taskManager/timeEntryView/timeEntryViewPayload.d.ts +27 -0
- package/lib/view/taskManager/timeEntryView/timeEntryViewPayload.js +2 -0
- package/lib/view/taskManager/timeEntryView/timeEntryViewReducer.d.ts +48 -0
- package/lib/view/taskManager/timeEntryView/timeEntryViewReducer.js +164 -0
- package/lib/view/taskManager/timeEntryView/timeEntryViewSelector.d.ts +24 -0
- package/lib/view/taskManager/timeEntryView/timeEntryViewSelector.js +46 -0
- package/lib/view/taskManager/timeEntryView/timeEntryViewState.d.ts +21 -0
- package/lib/view/taskManager/timeEntryView/timeEntryViewState.js +5 -0
- package/lib/zeniAPI.d.ts +2 -2
- package/package.json +1 -26
- package/lib/esm/sessionEndpointsLoader.js +0 -7
- package/lib/sessionEndpointsLoader.d.ts +0 -9
- package/lib/sessionEndpointsLoader.js +0 -11
- package/lib/tsconfig.mocks.tsbuildinfo +0 -1
- package/lib/tsconfig.typecheck.tsbuildinfo +0 -1
|
@@ -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
|
+
}));
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { of } from 'rxjs';
|
|
2
|
+
import { catchError, filter, mergeMap } from 'rxjs/operators';
|
|
3
|
+
import { updateTaskTimeSpentMinutes } from '../../../../entity/task/taskReducer';
|
|
4
|
+
import { removeTimeEntry, updateTimeEntries, } from '../../../../entity/timeEntry/timeEntryReducer';
|
|
5
|
+
import { getTimeEntryById } from '../../../../entity/timeEntry/timeEntrySelector';
|
|
6
|
+
import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
|
|
7
|
+
import { deleteTimeEntry, logTime, logTimeFailure, logTimeSuccess, timeEntryWriteFailure, timeEntryWriteSuccess, updateTimeEntryMinutes, } from '../timeEntryViewReducer';
|
|
8
|
+
const unexpected = (what, error) => createZeniAPIStatus('Unexpected Error', `${what} errored out${JSON.stringify(error)}`);
|
|
9
|
+
/**
|
|
10
|
+
* Log time against a task.
|
|
11
|
+
*
|
|
12
|
+
* `mergeMap`, not `switchMap`: two rows on the board can be edited in quick
|
|
13
|
+
* succession and cancelling the first would silently drop that entry.
|
|
14
|
+
*/
|
|
15
|
+
export const logTimeEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(logTime.match), mergeMap((action) => {
|
|
16
|
+
const { taskId, minutes, entryDate, source, identifier } = action.payload;
|
|
17
|
+
return zeniAPI
|
|
18
|
+
.postAndGetJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/tasks/${taskId}/time-entries`, {
|
|
19
|
+
minutes,
|
|
20
|
+
entry_date: entryDate,
|
|
21
|
+
source,
|
|
22
|
+
// Sent only when the caller has one. The server treats it as the
|
|
23
|
+
// entity key, which is what makes a retried timer stop idempotent.
|
|
24
|
+
...(identifier != null ? { identifier } : {}),
|
|
25
|
+
})
|
|
26
|
+
.pipe(mergeMap((response) => {
|
|
27
|
+
if (isSuccessResponse(response) && response.data != null) {
|
|
28
|
+
const { time_entry, task_time_spent_minutes } = response.data;
|
|
29
|
+
return of(updateTimeEntries([time_entry]),
|
|
30
|
+
// The board reads `Task.time_spent` per row, so it has to be
|
|
31
|
+
// told the new total; otherwise the row keeps the old number
|
|
32
|
+
// until something refetches the whole list.
|
|
33
|
+
updateTaskTimeSpentMinutes({
|
|
34
|
+
taskId,
|
|
35
|
+
minutes: task_time_spent_minutes ?? 0,
|
|
36
|
+
}), logTimeSuccess({ taskId, timeEntryId: time_entry.time_entry_id }));
|
|
37
|
+
}
|
|
38
|
+
return of(logTimeFailure({ taskId, status: response.status }));
|
|
39
|
+
}), catchError((error) => of(logTimeFailure({ taskId, status: unexpected('Log time', error) }))));
|
|
40
|
+
}));
|
|
41
|
+
/** Correct an entry's minutes. Owner, or a Zeni user as the admin path. */
|
|
42
|
+
export const updateTimeEntryEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(updateTimeEntryMinutes.match), mergeMap((action) => {
|
|
43
|
+
const { timeEntryId, minutes } = action.payload;
|
|
44
|
+
return zeniAPI
|
|
45
|
+
.putAndGetJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/time-entries/${timeEntryId}`, { minutes })
|
|
46
|
+
.pipe(mergeMap((response) => {
|
|
47
|
+
if (isSuccessResponse(response) && response.data != null) {
|
|
48
|
+
return of(updateTimeEntries([response.data.time_entry]), timeEntryWriteSuccess({ timeEntryId }));
|
|
49
|
+
}
|
|
50
|
+
return of(timeEntryWriteFailure({ timeEntryId, status: response.status }));
|
|
51
|
+
}), catchError((error) => of(timeEntryWriteFailure({
|
|
52
|
+
timeEntryId,
|
|
53
|
+
status: unexpected('Update time entry', error),
|
|
54
|
+
}))));
|
|
55
|
+
}));
|
|
56
|
+
/**
|
|
57
|
+
* Soft delete. The row stays in Datastore for the audit trail but leaves the
|
|
58
|
+
* totals, so the client drops it from the store rather than keeping something
|
|
59
|
+
* every reader would then have to remember to filter.
|
|
60
|
+
*/
|
|
61
|
+
export const deleteTimeEntryEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(deleteTimeEntry.match), mergeMap((action) => {
|
|
62
|
+
const timeEntryId = action.payload;
|
|
63
|
+
// Read the task id BEFORE the delete: the entry is about to leave the
|
|
64
|
+
// store, and the view slice needs to know which task's id list to prune.
|
|
65
|
+
const taskId = getTimeEntryById(state$.value.timeEntryState, timeEntryId)?.taskId;
|
|
66
|
+
return zeniAPI
|
|
67
|
+
.deleteAndGetJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/time-entries/${timeEntryId}`)
|
|
68
|
+
.pipe(mergeMap((response) => {
|
|
69
|
+
if (isSuccessResponse(response)) {
|
|
70
|
+
return of(removeTimeEntry(timeEntryId), timeEntryWriteSuccess({ timeEntryId, removedFromTaskId: taskId }));
|
|
71
|
+
}
|
|
72
|
+
return of(timeEntryWriteFailure({ timeEntryId, status: response.status }));
|
|
73
|
+
}), catchError((error) => of(timeEntryWriteFailure({
|
|
74
|
+
timeEntryId,
|
|
75
|
+
status: unexpected('Delete time entry', error),
|
|
76
|
+
}))));
|
|
77
|
+
}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { createSlice } from '@reduxjs/toolkit';
|
|
2
|
+
import { clearAll } from '../../../rootActions';
|
|
3
|
+
const NOT_STARTED = { fetchState: 'Not-Started', error: undefined };
|
|
4
|
+
export const initialState = {
|
|
5
|
+
timeEntryIdsByTaskId: {},
|
|
6
|
+
fetchStateByTaskId: {},
|
|
7
|
+
timeEntryIdsByRangeKey: {},
|
|
8
|
+
fetchStateByRangeKey: {},
|
|
9
|
+
saveStateByTaskId: {},
|
|
10
|
+
saveStateByTimeEntryId: {},
|
|
11
|
+
taskCategoryFetchState: NOT_STARTED,
|
|
12
|
+
};
|
|
13
|
+
const timeEntryView = createSlice({
|
|
14
|
+
name: 'timeEntryView',
|
|
15
|
+
initialState,
|
|
16
|
+
reducers: {
|
|
17
|
+
fetchTimeEntriesForTask: {
|
|
18
|
+
prepare(taskId, cacheOverride = false) {
|
|
19
|
+
return { payload: { taskId, cacheOverride } };
|
|
20
|
+
},
|
|
21
|
+
reducer(draft, action) {
|
|
22
|
+
draft.fetchStateByTaskId[action.payload.taskId] = {
|
|
23
|
+
fetchState: 'In-Progress',
|
|
24
|
+
error: undefined,
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
fetchTimeEntriesForTaskSuccess(draft, action) {
|
|
29
|
+
const { taskId, timeEntryIds } = action.payload;
|
|
30
|
+
// Replaced, not merged: the server is authoritative about which entries
|
|
31
|
+
// a task has, and a merge would resurrect one that was just deleted.
|
|
32
|
+
draft.timeEntryIdsByTaskId[taskId] = timeEntryIds;
|
|
33
|
+
draft.fetchStateByTaskId[taskId] = {
|
|
34
|
+
fetchState: 'Completed',
|
|
35
|
+
error: undefined,
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
fetchTimeEntriesForTaskFailure(draft, action) {
|
|
39
|
+
draft.fetchStateByTaskId[action.payload.taskId] = {
|
|
40
|
+
fetchState: 'Error',
|
|
41
|
+
error: action.payload.status,
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
fetchTimeEntriesForRange: {
|
|
45
|
+
prepare(userId, startDate, endDate, cacheOverride = false) {
|
|
46
|
+
return { payload: { userId, startDate, endDate, cacheOverride } };
|
|
47
|
+
},
|
|
48
|
+
reducer(draft, action) {
|
|
49
|
+
const { userId, startDate, endDate } = action.payload;
|
|
50
|
+
draft.fetchStateByRangeKey[`${userId}_${startDate}_${endDate}`] = {
|
|
51
|
+
fetchState: 'In-Progress',
|
|
52
|
+
error: undefined,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
fetchTimeEntriesForRangeSuccess(draft, action) {
|
|
57
|
+
const { rangeKey, timeEntryIds } = action.payload;
|
|
58
|
+
draft.timeEntryIdsByRangeKey[rangeKey] = timeEntryIds;
|
|
59
|
+
draft.fetchStateByRangeKey[rangeKey] = {
|
|
60
|
+
fetchState: 'Completed',
|
|
61
|
+
error: undefined,
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
fetchTimeEntriesForRangeFailure(draft, action) {
|
|
65
|
+
draft.fetchStateByRangeKey[action.payload.rangeKey] = {
|
|
66
|
+
fetchState: 'Error',
|
|
67
|
+
error: action.payload.status,
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
logTime: {
|
|
71
|
+
prepare(taskId, minutes, entryDate, source = 'manual', identifier) {
|
|
72
|
+
return { payload: { taskId, minutes, entryDate, source, identifier } };
|
|
73
|
+
},
|
|
74
|
+
reducer(draft, action) {
|
|
75
|
+
draft.saveStateByTaskId[action.payload.taskId] = {
|
|
76
|
+
fetchState: 'In-Progress',
|
|
77
|
+
error: undefined,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
logTimeSuccess(draft, action) {
|
|
82
|
+
const { taskId, timeEntryId } = action.payload;
|
|
83
|
+
draft.saveStateByTaskId[taskId] = {
|
|
84
|
+
fetchState: 'Completed',
|
|
85
|
+
error: undefined,
|
|
86
|
+
};
|
|
87
|
+
const existing = draft.timeEntryIdsByTaskId[taskId] ?? [];
|
|
88
|
+
// Guarded against a duplicate id: a retried write with the same
|
|
89
|
+
// deterministic identifier returns the SAME entry, and pushing it twice
|
|
90
|
+
// would double it in the breakdown even though the server has one row.
|
|
91
|
+
if (!existing.includes(timeEntryId)) {
|
|
92
|
+
draft.timeEntryIdsByTaskId[taskId] = [...existing, timeEntryId];
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
logTimeFailure(draft, action) {
|
|
96
|
+
draft.saveStateByTaskId[action.payload.taskId] = {
|
|
97
|
+
fetchState: 'Error',
|
|
98
|
+
error: action.payload.status,
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
updateTimeEntryMinutes: {
|
|
102
|
+
prepare(timeEntryId, minutes) {
|
|
103
|
+
return { payload: { timeEntryId, minutes } };
|
|
104
|
+
},
|
|
105
|
+
reducer(draft, action) {
|
|
106
|
+
draft.saveStateByTimeEntryId[action.payload.timeEntryId] = {
|
|
107
|
+
fetchState: 'In-Progress',
|
|
108
|
+
error: undefined,
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
deleteTimeEntry(draft, action) {
|
|
113
|
+
draft.saveStateByTimeEntryId[action.payload] = {
|
|
114
|
+
fetchState: 'In-Progress',
|
|
115
|
+
error: undefined,
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
timeEntryWriteSuccess(draft, action) {
|
|
119
|
+
const { timeEntryId, removedFromTaskId } = action.payload;
|
|
120
|
+
draft.saveStateByTimeEntryId[timeEntryId] = {
|
|
121
|
+
fetchState: 'Completed',
|
|
122
|
+
error: undefined,
|
|
123
|
+
};
|
|
124
|
+
if (removedFromTaskId != null) {
|
|
125
|
+
draft.timeEntryIdsByTaskId[removedFromTaskId] = (draft.timeEntryIdsByTaskId[removedFromTaskId] ?? []).filter((candidate) => candidate !== timeEntryId);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
timeEntryWriteFailure(draft, action) {
|
|
129
|
+
draft.saveStateByTimeEntryId[action.payload.timeEntryId] = {
|
|
130
|
+
fetchState: 'Error',
|
|
131
|
+
error: action.payload.status,
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
fetchTaskCategories(draft) {
|
|
135
|
+
draft.taskCategoryFetchState = {
|
|
136
|
+
fetchState: 'In-Progress',
|
|
137
|
+
error: undefined,
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
fetchTaskCategoriesSuccess(draft) {
|
|
141
|
+
draft.taskCategoryFetchState = {
|
|
142
|
+
fetchState: 'Completed',
|
|
143
|
+
error: undefined,
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
fetchTaskCategoriesFailure(draft, action) {
|
|
147
|
+
draft.taskCategoryFetchState = {
|
|
148
|
+
fetchState: 'Error',
|
|
149
|
+
error: action.payload,
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
extraReducers: (builder) => {
|
|
154
|
+
builder.addCase(clearAll, (draft) => {
|
|
155
|
+
Object.assign(draft, initialState);
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
export const { fetchTimeEntriesForTask, fetchTimeEntriesForTaskSuccess, fetchTimeEntriesForTaskFailure, fetchTimeEntriesForRange, fetchTimeEntriesForRangeSuccess, fetchTimeEntriesForRangeFailure, logTime, logTimeSuccess, logTimeFailure, updateTimeEntryMinutes, deleteTimeEntry, timeEntryWriteSuccess, timeEntryWriteFailure, fetchTaskCategories, fetchTaskCategoriesSuccess, fetchTaskCategoriesFailure, } = timeEntryView.actions;
|
|
160
|
+
export default timeEntryView.reducer;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getTimeEntriesByIds } from '../../../entity/timeEntry/timeEntrySelector';
|
|
2
|
+
import { toTimeEntryRangeKey } from './timeEntryViewState';
|
|
3
|
+
const byEntryDateThenCreate = (left, right) => left.entryDate.valueOf() - right.entryDate.valueOf() ||
|
|
4
|
+
left.createTime.valueOf() - right.createTime.valueOf();
|
|
5
|
+
const sumMinutes = (entries) => entries.reduce((total, entry) => total + (entry.minutes || 0), 0);
|
|
6
|
+
/** Every entry on one task, all users — the task detail breakdown. */
|
|
7
|
+
export function getTimeEntriesForTask(state, taskId) {
|
|
8
|
+
const { timeEntryIdsByTaskId, fetchStateByTaskId } = state.timeEntryViewState;
|
|
9
|
+
const timeEntries = getTimeEntriesByIds(state.timeEntryState, timeEntryIdsByTaskId[taskId] ?? []).sort(byEntryDateThenCreate);
|
|
10
|
+
return {
|
|
11
|
+
timeEntries,
|
|
12
|
+
totalMinutes: sumMinutes(timeEntries),
|
|
13
|
+
fetchState: fetchStateByTaskId[taskId]?.fetchState ?? 'Not-Started',
|
|
14
|
+
error: fetchStateByTaskId[taskId]?.error,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** One user's week — the timesheet grid. */
|
|
18
|
+
export function getTimeEntriesForRange(state, userId, startDate, endDate) {
|
|
19
|
+
const rangeKey = toTimeEntryRangeKey(userId, startDate, endDate);
|
|
20
|
+
const { timeEntryIdsByRangeKey, fetchStateByRangeKey } = state.timeEntryViewState;
|
|
21
|
+
const timeEntries = getTimeEntriesByIds(state.timeEntryState, timeEntryIdsByRangeKey[rangeKey] ?? []).sort(byEntryDateThenCreate);
|
|
22
|
+
const minutesByDate = {};
|
|
23
|
+
timeEntries.forEach((entry) => {
|
|
24
|
+
const key = entry.entryDate.format('YYYY-MM-DD');
|
|
25
|
+
minutesByDate[key] = (minutesByDate[key] ?? 0) + (entry.minutes || 0);
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
rangeKey,
|
|
29
|
+
timeEntries,
|
|
30
|
+
minutesByDate,
|
|
31
|
+
totalMinutes: sumMinutes(timeEntries),
|
|
32
|
+
fetchState: fetchStateByRangeKey[rangeKey]?.fetchState ?? 'Not-Started',
|
|
33
|
+
error: fetchStateByRangeKey[rangeKey]?.error,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Save state for the row the user is editing, so two rows saving at once do
|
|
37
|
+
* not share one spinner. */
|
|
38
|
+
export function getTimeEntrySaveState(state, taskId) {
|
|
39
|
+
return (state.timeEntryViewState.saveStateByTaskId[taskId]?.fetchState ??
|
|
40
|
+
'Not-Started');
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const toTimeEntryRangeKey = (userId, startDate, endDate) => `${userId}_${startDate}_${endDate}`;
|
package/lib/index.d.ts
CHANGED
|
@@ -150,6 +150,8 @@ import { DoSignInPayload, deleteConnection, doMagicLinkSignIn, doSignIn, doSignO
|
|
|
150
150
|
import { CurrentTenant, DEFAULT_RECEIPTS_EMAIL, TenantBaseView, TenantCoreDetailsView, TenantView, TenantsBaseOrdered, TenantsOrdered, getCurrentTenant, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, getIsAutoReviewIncomeWithoutCustomerEnabled, getReceiptsEmail, getTenantBaseById, getTenantBaseViewForTenantId, getTenantBaseViewForTenantView, getTenantsBaseByCheckInDateSelector, getTenantsByCheckInDateSelector, getTenantsCoreDetailsByCheckInDateSelector, isOtherProductsEnabledForTenant, isTenantBankingOnly, isTenantBookkeepingEnabled, isTenantCardsOnly, isTenantUsingZeniCOA, isZeniDomainTenant, toTenantCoreDetailsView } from './entity/tenant/tenantSelector';
|
|
151
151
|
import { CapitalizableAccount, CapitalizableAccounts, CapitalizableClassificationType, CapitalizableMatchPattern, Connection, LoggedInUser, RoleResource, Tenant, TenantProductSettings } from './entity/tenant/tenantState';
|
|
152
152
|
import { toExternalSupportedTool } from './entity/tenant/tenantTypes';
|
|
153
|
+
import { getTaskCategories, getTaskCategoryById } from './entity/timeEntry/timeEntrySelector';
|
|
154
|
+
import { BillingType, TaskCategory, TimeEntry, TimeEntrySource } from './entity/timeEntry/timeEntryState';
|
|
153
155
|
import { ToastNotificationPayload } from './entity/toastNotification/toastNotificationPayload';
|
|
154
156
|
import { pushToastNotification } from './entity/toastNotification/toastNotificationReducer';
|
|
155
157
|
import { getLastNotificationTime, getNotifications } from './entity/toastNotification/toastNotificationSelector';
|
|
@@ -175,7 +177,7 @@ import { Filing1099TinType } from './entity/vendor/vendorType';
|
|
|
175
177
|
import { VendorExpenseState, VendorSpend } from './entity/vendorExpense/vendorExpenseState';
|
|
176
178
|
import { VendorExpenseTrend } from './entity/vendorExpense/vendorExpenseTrend';
|
|
177
179
|
import { ExternalAPIEndpoints } from './externalAPI';
|
|
178
|
-
import initialize, { ZeniStore, disconnectPusher, initializePusher, initializeWithNewStore, injectFeatureEpics, injectFeatureModules,
|
|
180
|
+
import initialize, { ZeniStore, disconnectPusher, initializePusher, initializeWithNewStore, injectFeatureEpics, injectFeatureModules, updateZeniAPIClientConfig, zeniAPIClientConfig } from './init';
|
|
179
181
|
import { ZeniAPIStatus, isAccessDeniedError, isInvalidSessionError, isSuccessResponse } from './responsePayload';
|
|
180
182
|
import { clearAll } from './rootActions';
|
|
181
183
|
import { RootState } from './rootStateTypes';
|
|
@@ -635,6 +637,8 @@ import { ALL_TASK_LIST_TABS, DueDateGroupKey, TASK_LIST_FILTER_CATEGORIES, TASK_
|
|
|
635
637
|
import { bulkUpdateTaskList, dragNDropTasks, fetchTaskList, fetchTaskListPage, initiateTaskListLocalData, removeTaskFromList, updateTaskFilters, updateTaskFromListView, updateTaskListLocalData, updateTaskListSearchText, updateTaskListTab, updateTaskListUIState } from './view/taskManager/taskListView/taskListReducer';
|
|
636
638
|
import { TaskGroupWithTasksList, TaskListSelectorView, TaskWithUserDetails, getAllTasks } from './view/taskManager/taskListView/taskListSelector';
|
|
637
639
|
import { SubtaskSortKey, SubtaskSortOrder, getDueDateValueFromDueDateGroupId, getTaskUpdates, sortSubtasks } from './view/taskManager/taskListView/taskListViewHelpers';
|
|
640
|
+
import { deleteTimeEntry, fetchTaskCategories, fetchTimeEntriesForRange, fetchTimeEntriesForTask, logTime, updateTimeEntryMinutes } from './view/taskManager/timeEntryView/timeEntryViewReducer';
|
|
641
|
+
import { TimeEntryListView, TimesheetWeekView, getTimeEntriesForRange, getTimeEntriesForTask, getTimeEntrySaveState } from './view/taskManager/timeEntryView/timeEntryViewSelector';
|
|
638
642
|
import { fetchTasksCard } from './view/tasksCard/tasksCardReducer';
|
|
639
643
|
import { TasksCardReport } from './view/tasksCard/tasksCardSelector';
|
|
640
644
|
import { fetchTopEx, updateTopExDownloadState, updateTopExTimeFrame } from './view/topEx/topExReducer';
|
|
@@ -722,7 +726,7 @@ export type { CreditAgentFocusFilter, CreditAgentSortDir, CreditAgentSortKey, Cr
|
|
|
722
726
|
export { AccountSettingsLocalData };
|
|
723
727
|
export { saveJeAccountSettings, saveJeAccountSettingsLocalData };
|
|
724
728
|
export default initialize;
|
|
725
|
-
export { initializePusher, disconnectPusher, Dayjs, ZeniDate, dateInLocal as zeniDateInLocal, date as zeniDate, dateLocal, dateNow, getBusinessDayOfDate, formatZeniDateFY as formatZeniDate, getFYMonths, getFYQuarterAndYear, getStartOfAndEndOfTimeframeForFY, getActualPeriodOfFYQtr, getActualPeriodOfFY, getLocalTimezone, addPeriod, subtractPeriod, setLocalTimezone, updateZeniDateLocaleID, ZeniUrl, toZeniUrl, toZeniUrlWithoutBaseURL, RESTAPIEndpoints, RootState, updateZeniAPIClientConfig,
|
|
729
|
+
export { initializePusher, disconnectPusher, Dayjs, ZeniDate, dateInLocal as zeniDateInLocal, date as zeniDate, dateLocal, dateNow, getBusinessDayOfDate, formatZeniDateFY as formatZeniDate, getFYMonths, getFYQuarterAndYear, getStartOfAndEndOfTimeframeForFY, getActualPeriodOfFYQtr, getActualPeriodOfFY, getLocalTimezone, addPeriod, subtractPeriod, setLocalTimezone, updateZeniDateLocaleID, ZeniUrl, toZeniUrl, toZeniUrlWithoutBaseURL, RESTAPIEndpoints, RootState, updateZeniAPIClientConfig, zeniAPIClientConfig, injectFeatureEpics, injectFeatureModules, initializeWithNewStore, ZeniStore, ZeniAPIStatus, getTimeframeTickTag, TimeFrameTickTag, isIncompleteMonthPresentInTimeframeTick, getLastMonthOfFYQuarter, getLastMonthOfFYYear, getMonthIndex, dateFromYearMonthDate, getMinDate, ExternalAPIEndpoints, };
|
|
726
730
|
export { ID, FetchState, FetchStateAndError, FetchedState, UpdateType, Mode, AccountViewMode, CompletionStatusType, };
|
|
727
731
|
export { Amount, toAmount, Currency };
|
|
728
732
|
export { View, SelectorView, Report, ReportID, toReportID, ReportIDPlusForecastID, SelectorReport, ReportFormat, toReportFormatStrict, isCashFlowOrBalanceSheetReport, isBalanceSheetReport, isBalanceSheetClassesViewReport, isBalanceSheetProjectViewReport, isCashFlowClassesViewReport, isCashFlowProjectViewReport, isCashFlowReport, isGroupedCashFlowReport, isGroupedBalanceSheetReport, isDashboardReport, isDashboardClassesViewReport, isDashboardProjectViewReport, isPAndLReport, isPAndLClassesViewReport, isPAndLProjectViewReport, isAgingReport, isOpExByVendorReport, isFluxAnalysisOpExReport, PageToken, };
|
|
@@ -972,7 +976,7 @@ export { fetchGlobalMerchantRecommendation, createGlobalMerchant, updateCreateGl
|
|
|
972
976
|
export { approveVendorGlobalReview, rejectVendorGlobalReview, fetchVendorGlobalReviewView, updateSelectedGlobalMerchant, updateVendorGlobalReviewViewUIState, getVendorGlobalReviewView, getTenantMerchantByMerchantId, toVendorGlobalReviewColumnSortKeyType, VendorGlobalReview, VendorGlobalReviewViewUIState, VendorGlobalReviewViewColumnSortKey, VendorGlobalReviewViewSelectorView, VendorGlobalReviewViewState, VendorGlobalReviewViewLocalData, updateVendorGlobalReviewViewLocalData, GlobalReviewPageCurrentSelectionByColumn, GlobalRecommendationFetchSource, };
|
|
973
977
|
export { fetchArAging, updateArAgingUIState, fetchArAgingDetail, updateArAgingDetailUIState, ArAgingReport, ArAgingDetail, NestedAgingBalancesByCustomer, getArAgingReport, getArAgingDetailForCustomer, updateArAgingNodeCollapseState, };
|
|
974
978
|
export { toRecurringBillFrequency, toRecurringBillFrequencyStrict, RecurringBillFrequency, RecurringBillDetail, RecurringBillConfig, RecurringBillStatus, RecurringBillPaymentStatus, };
|
|
975
|
-
export { Task, TaskPayload, TaskListSelectorView, TaskWithUserDetails, TaskListViewSortKey, TaskListUIState, TaskDetailSelectorView, EditTaskLocalData, TaskStatusCodeType, PriorityCodeType, getTaskGroupById, TaskStatus, TaskPriority, TaskCodeType, getAllTasks, fetchTaskList, fetchTaskListPage, getTaskDetail, fetchTaskDetailPage, saveTaskUpdatesToLocalStore, saveTaskDetail, createSubTask, resetSubTaskCreateStatus, fetchSubTasks, CreateSubTaskPayload, archiveTask, CannedResponse, CannedResponsesViewState, fetchCannedResponses, saveCannedResponse, deleteCannedResponse, getCannedResponsesView, updateTaskListSearchText, updateTaskListUIState, allTaskStatus, allTaskPriority, TaskListFilters, updateTaskFilters, TASK_LIST_FILTER_CATEGORIES, TASK_LIST_GROUP_BY_CATEGORIES, TaskListFilterCategoryField, deleteTask, discardTaskUpdatesInLocalStore, TaskFilterCategory, TaskType, TaskGroupWithTasksList, TaskListLocalData, bulkUpdateTaskList, createNewTaskGroup, initiateTaskListLocalData, deleteTaskGroup, updateTaskListLocalData, dragNDropTasks, updateTaskGroupName, TaskGroupState, TaskGroupKey, toTaskListGroupByKeyType, toTaskListGroupByKeyTypeStrict, SubtaskSortKey, SubtaskSortOrder, getTaskUpdates, sortSubtasks, fetchAllTaskGroups, initialTaskDetail, getDueDateValueFromDueDateGroupId, toPriorityCodeType, toTaskStatusCodeType, DueDateGroupKey, toDueDateGroupKeyType, updateTaskFromListView, updateTaskListTab, removeTaskFromList, snoozeTask, unsnoozeTask, convertHHMMStrToMinutes, initialTaskDetailLocalData, TaskListTab, ALL_TASK_LIST_TABS, };
|
|
979
|
+
export { Task, TaskPayload, TaskListSelectorView, TaskWithUserDetails, TaskListViewSortKey, TaskListUIState, TaskDetailSelectorView, EditTaskLocalData, TaskStatusCodeType, PriorityCodeType, getTaskGroupById, TaskStatus, TaskPriority, TaskCodeType, getAllTasks, BillingType, TaskCategory, TimeEntry, TimeEntrySource, TimeEntryListView, TimesheetWeekView, getTaskCategories, getTaskCategoryById, getTimeEntriesForRange, getTimeEntriesForTask, getTimeEntrySaveState, fetchTimeEntriesForTask, fetchTimeEntriesForRange, fetchTaskCategories, logTime, updateTimeEntryMinutes, deleteTimeEntry, fetchTaskList, fetchTaskListPage, getTaskDetail, fetchTaskDetailPage, saveTaskUpdatesToLocalStore, saveTaskDetail, createSubTask, resetSubTaskCreateStatus, fetchSubTasks, CreateSubTaskPayload, archiveTask, CannedResponse, CannedResponsesViewState, fetchCannedResponses, saveCannedResponse, deleteCannedResponse, getCannedResponsesView, updateTaskListSearchText, updateTaskListUIState, allTaskStatus, allTaskPriority, TaskListFilters, updateTaskFilters, TASK_LIST_FILTER_CATEGORIES, TASK_LIST_GROUP_BY_CATEGORIES, TaskListFilterCategoryField, deleteTask, discardTaskUpdatesInLocalStore, TaskFilterCategory, TaskType, TaskGroupWithTasksList, TaskListLocalData, bulkUpdateTaskList, createNewTaskGroup, initiateTaskListLocalData, deleteTaskGroup, updateTaskListLocalData, dragNDropTasks, updateTaskGroupName, TaskGroupState, TaskGroupKey, toTaskListGroupByKeyType, toTaskListGroupByKeyTypeStrict, SubtaskSortKey, SubtaskSortOrder, getTaskUpdates, sortSubtasks, fetchAllTaskGroups, initialTaskDetail, getDueDateValueFromDueDateGroupId, toPriorityCodeType, toTaskStatusCodeType, DueDateGroupKey, toDueDateGroupKeyType, updateTaskFromListView, updateTaskListTab, removeTaskFromList, snoozeTask, unsnoozeTask, convertHHMMStrToMinutes, initialTaskDetailLocalData, TaskListTab, ALL_TASK_LIST_TABS, };
|
|
976
980
|
export { Tag, TagState, TagViewState, getAllTags, TagViewSelectorView, fetchTagList, createTag, deleteTag, };
|
|
977
981
|
export { CardPayment, CardPaymentViewState, SetupIntentData, CardPaymentSelectorView, getAllCardsAndBankPaymentMethods, createCardSetupIntent, confirmCardSetupIntent, addCardPaymentSource, fetchPaymentSources, resetCardPaymentErrorStatuses, clearCardPaymentView, AddCardPaymentSourceType, };
|
|
978
982
|
export { getAuditReportGroupViewSelectorView, getAuditRuleGroupViewSelectorView, getUserFromAllUsers, AuditReportGroupSelectorView, AuditReportGroupViewSelectorView, AuditRuleGroupViewSelectorView, fetchAuditRuleGroupView, fetchAuditReportGroupView, saveReasonForAuditRule, AuditRuleGroup, AuditRuleGroupReport, AuditReportRule, AuditRuleEntityType, AuditRuleEntityIdType, AuditRuleBypassReason, clearAuditReportGroupViewByCompanyId, };
|
|
@@ -1270,7 +1274,6 @@ export { getActualMatchingAmount, getActualMatchingReversalDate, getActualMatchi
|
|
|
1270
1274
|
export type { ExpenseAutomationActualMatchingMatchView } from './view/expenseAutomationView/selectors/actualMatchingViewSelector';
|
|
1271
1275
|
export type { ActualMatchingMatch, ActualMatchingMatchKind, } from './entity/actualMatching/actualMatchingState';
|
|
1272
1276
|
export type { ActualMatchingMatchId } from './entity/actualMatching/actualMatchingTypes';
|
|
1273
|
-
export type { SessionEndpointsLoader } from './sessionEndpointsLoader';
|
|
1274
1277
|
export type { DisplayCurrencyFallback } from './entity/actualMatching/actualMatchingPayload';
|
|
1275
1278
|
export { fetchCustomerSummaryDetail, fetchCustomerSummaryList, resetCustomerSummaryDetailUiState, saveCustomerSummaryDetail, updateCustomerAllTransactionsTabData, updateCustomerDetailSelectedTab, updateCustomerSummaryRevenueSelection, updateCustomerSummaryScrollYOffset, updateCustomerSummarySortUiState, updateCustomerTrendFilter, } from './view/customerSummaryView/customerSummaryViewReducer';
|
|
1276
1279
|
export { getCustomerSummaryAllTransactionsTabData, getCustomerSummaryDetail, getCustomerSummaryList, getCustomerSummarySelectedTabType, getCustomerSummaryTrendFilter, isCustomersTabVisible, } from './view/customerSummaryView/customerSummaryViewSelector';
|