@zeniai/client-epic-state 5.3.7 → 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/invoicing/invoice/invoicePayload.d.ts +1 -0
- package/lib/entity/invoicing/invoice/invoicePayload.js +3 -0
- package/lib/entity/invoicing/invoice/invoiceState.d.ts +2 -0
- 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/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/invoicing/invoice/invoicePayload.js +3 -0
- package/lib/esm/entity/task/taskPayload.js +1 -0
- package/lib/esm/entity/task/taskReducer.js +18 -1
- 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 +4 -1
- package/lib/esm/reducer.js +6 -0
- package/lib/esm/view/expenseAutomationView/epics/accountRecon/fetchReconciliationViewEpic.js +2 -1
- 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 +5 -1
- package/lib/index.js +31 -16
- package/lib/reducer.d.ts +2 -0
- package/lib/reducer.js +6 -0
- package/lib/rootStateTypes.d.ts +4 -0
- package/lib/view/expenseAutomationView/epics/accountRecon/fetchReconciliationViewEpic.js +2 -1
- 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/package.json +1 -1
|
@@ -32,6 +32,7 @@ const getTaskDetail = (state, taskId) => {
|
|
|
32
32
|
let isDeleted = undefined;
|
|
33
33
|
let depth = undefined;
|
|
34
34
|
let recurringSourceTaskId = undefined;
|
|
35
|
+
let taskCategoryId = undefined;
|
|
35
36
|
let snoozedUntil = undefined;
|
|
36
37
|
let taskGroupId = undefined;
|
|
37
38
|
let subtasks = [];
|
|
@@ -45,6 +46,7 @@ const getTaskDetail = (state, taskId) => {
|
|
|
45
46
|
if (taskEntity != null) {
|
|
46
47
|
createdByUser = (0, userAndRole_1.getUserAndUserRole)(userState, userRoleState, addressState, taskEntity.createdBy);
|
|
47
48
|
depth = taskEntity.depth;
|
|
49
|
+
taskCategoryId = taskEntity.taskCategoryId;
|
|
48
50
|
isArchived = taskEntity.isArchived;
|
|
49
51
|
isDeleted = taskEntity.isDeleted;
|
|
50
52
|
recurringSourceTaskId = taskEntity.recurringSourceTaskId;
|
|
@@ -109,6 +111,7 @@ const getTaskDetail = (state, taskId) => {
|
|
|
109
111
|
version: 0,
|
|
110
112
|
createdByUser,
|
|
111
113
|
depth,
|
|
114
|
+
taskCategoryId,
|
|
112
115
|
isArchived,
|
|
113
116
|
isDeleted,
|
|
114
117
|
recurringSourceTaskId,
|
|
@@ -48,7 +48,7 @@ export interface TaskFilterCategory {
|
|
|
48
48
|
values: (string | ZeniDate)[];
|
|
49
49
|
valuesCombinationOperator: 'ANY';
|
|
50
50
|
}
|
|
51
|
-
export type TaskListFilterCategoryField = 'status' | 'dueDate' | 'assignee' | 'creationDate' | 'priority' | 'tag';
|
|
51
|
+
export type TaskListFilterCategoryField = 'status' | 'dueDate' | 'assignee' | 'createdBy' | 'creationDate' | 'priority' | 'tag';
|
|
52
52
|
export declare const TASK_LIST_GROUP_BY_KEYS: readonly ["groupName", "status", "dueDate", "assignee", "priority", "tag"];
|
|
53
53
|
export declare const toTaskListGroupByKeyType: (v: string) => "status" | "dueDate" | "priority" | "assignee" | "tag" | "groupName";
|
|
54
54
|
export declare const toTaskListGroupByKeyTypeStrict: (v: string) => "status" | "dueDate" | "priority" | "assignee" | "tag" | "groupName" | undefined;
|
|
@@ -53,6 +53,11 @@ exports.TASK_LIST_FILTER_CATEGORIES = [
|
|
|
53
53
|
type: 'user',
|
|
54
54
|
multiple: true,
|
|
55
55
|
},
|
|
56
|
+
{
|
|
57
|
+
value: 'createdBy',
|
|
58
|
+
type: 'user',
|
|
59
|
+
multiple: true,
|
|
60
|
+
},
|
|
56
61
|
{ value: 'creationDate', type: 'dateRange', multiple: true },
|
|
57
62
|
];
|
|
58
63
|
exports.TASK_LIST_GROUP_BY_CATEGORIES = [
|
|
@@ -3,7 +3,7 @@ import { Task } from '../../../entity/task/taskState';
|
|
|
3
3
|
import { User } from '../../../entity/user/userState';
|
|
4
4
|
import { ZeniDate } from '../../../zeniDayJS';
|
|
5
5
|
import { TaskGroupKey } from './taskList';
|
|
6
|
-
export type SubtaskSortKey = 'name' | 'status' | 'assignee' | 'tag';
|
|
6
|
+
export type SubtaskSortKey = 'name' | 'status' | 'assignee' | 'tag' | 'priority' | 'dueDate' | 'timeSpent';
|
|
7
7
|
export type SubtaskSortOrder = 'asc' | 'desc';
|
|
8
8
|
export declare const getDueDateValueFromDueDateGroupId: (groupId: string) => ZeniDate;
|
|
9
9
|
export declare const getTaskUpdates: (groupByKey: TaskGroupKey, groupId: string) => Partial<Task>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.sortSubtasks = exports.getTaskUpdates = exports.getDueDateValueFromDueDateGroupId = void 0;
|
|
4
|
+
const formatMinutesToFromHHMM_1 = require("../../../commonStateTypes/fiscalYearHelpers/formatMinutesToFromHHMM");
|
|
4
5
|
const zeniDayJS_1 = require("../../../zeniDayJS");
|
|
5
6
|
const taskDetailSelector_1 = require("../taskDetailView/taskDetailSelector");
|
|
6
7
|
const taskList_1 = require("./taskList");
|
|
@@ -68,6 +69,11 @@ const sortSubtasks = (subtasks, sortKey, sortOrder, userList, tagList) => {
|
|
|
68
69
|
return acc;
|
|
69
70
|
}, {});
|
|
70
71
|
const unknownStatusRank = taskDetailSelector_1.allTaskStatus.length;
|
|
72
|
+
const priorityOrder = taskDetailSelector_1.allTaskPriority.reduce((acc, priority, index) => {
|
|
73
|
+
acc[priority.code] = index;
|
|
74
|
+
return acc;
|
|
75
|
+
}, {});
|
|
76
|
+
const unknownPriorityRank = taskDetailSelector_1.allTaskPriority.length;
|
|
71
77
|
const cmpStr = (a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' });
|
|
72
78
|
const keyFn = (task) => {
|
|
73
79
|
if (sortKey === 'name') {
|
|
@@ -94,6 +100,37 @@ const sortSubtasks = (subtasks, sortKey, sortOrder, userList, tagList) => {
|
|
|
94
100
|
const tag = tagList.find((candidate) => candidate.tagId === firstTagId);
|
|
95
101
|
return tag?.name ?? '';
|
|
96
102
|
}
|
|
103
|
+
if (sortKey === 'priority') {
|
|
104
|
+
// Rank by the canonical order, not the code string: alphabetical would
|
|
105
|
+
// sort "high" before "urgent" and read as wrong to anyone scanning it.
|
|
106
|
+
return priorityOrder[task.priority?.code] ?? unknownPriorityRank;
|
|
107
|
+
}
|
|
108
|
+
if (sortKey === 'dueDate') {
|
|
109
|
+
// A task with no date is not "earliest", so undated rows belong at the
|
|
110
|
+
// bottom in BOTH directions. The comparator negates for 'desc', so one
|
|
111
|
+
// fixed sentinel would flip them to the top — the sentinel has to flip
|
|
112
|
+
// with the order to survive that negation.
|
|
113
|
+
// `dueDate` is already a ZeniDate (dayjs), so valueOf() is the epoch.
|
|
114
|
+
// An unparseable date yields NaN, and a NaN comparator leaves the array
|
|
115
|
+
// untouched -- the same silent failure the timeSpent branch guards.
|
|
116
|
+
// Treat it as undated rather than as a date.
|
|
117
|
+
const due = task.dueDate;
|
|
118
|
+
const epoch = due?.valueOf();
|
|
119
|
+
if (epoch == null || Number.isNaN(epoch)) {
|
|
120
|
+
return sortOrder === 'asc'
|
|
121
|
+
? Number.MAX_SAFE_INTEGER
|
|
122
|
+
: Number.MIN_SAFE_INTEGER;
|
|
123
|
+
}
|
|
124
|
+
return epoch;
|
|
125
|
+
}
|
|
126
|
+
if (sortKey === 'timeSpent') {
|
|
127
|
+
// `timeSpent` is an "HHMM" STRING (no colon). Comparing it as text puts
|
|
128
|
+
// "1000" before "0900" only by luck of padding, and any malformed value
|
|
129
|
+
// makes the converter return NaN -- which a comparator turns into "no
|
|
130
|
+
// sort at all", silently. Coerce that to 0.
|
|
131
|
+
const minutes = (0, formatMinutesToFromHHMM_1.convertHHMMStrToMinutes)(task.timeSpent ?? '');
|
|
132
|
+
return Number.isFinite(minutes) ? minutes : 0;
|
|
133
|
+
}
|
|
97
134
|
return '';
|
|
98
135
|
};
|
|
99
136
|
const copy = [...subtasks];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ActionsObservable, StateObservable } from 'redux-observable';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { updateTaskCategories } from '../../../../entity/timeEntry/timeEntryReducer';
|
|
4
|
+
import { RootState } from '../../../../rootStateTypes';
|
|
5
|
+
import { ZeniAPI } from '../../../../zeniAPI';
|
|
6
|
+
import { fetchTaskCategories, fetchTaskCategoriesFailure, fetchTaskCategoriesSuccess } from '../timeEntryViewReducer';
|
|
7
|
+
export type ActionType = ReturnType<typeof fetchTaskCategories> | ReturnType<typeof fetchTaskCategoriesSuccess> | ReturnType<typeof fetchTaskCategoriesFailure> | ReturnType<typeof updateTaskCategories>;
|
|
8
|
+
/**
|
|
9
|
+
* The 156-category catalog that fills the Category dropdown.
|
|
10
|
+
*
|
|
11
|
+
* `switchMap` here, unlike the entry epics: there is one catalog, so a second
|
|
12
|
+
* request supersedes the first rather than racing it.
|
|
13
|
+
*
|
|
14
|
+
* Zeni-only on the server (the billing marker is commercially sensitive), so
|
|
15
|
+
* a customer user gets a 401 — surfaced as Error rather than an empty list,
|
|
16
|
+
* because an empty catalog looks like "no categories exist".
|
|
17
|
+
*/
|
|
18
|
+
export declare const fetchTaskCategoriesEpic: (actions$: ActionsObservable<ActionType>, _state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchTaskCategoriesEpic = void 0;
|
|
4
|
+
const rxjs_1 = require("rxjs");
|
|
5
|
+
const operators_1 = require("rxjs/operators");
|
|
6
|
+
const operators_2 = require("rxjs/operators");
|
|
7
|
+
const timeEntryReducer_1 = require("../../../../entity/timeEntry/timeEntryReducer");
|
|
8
|
+
const responsePayload_1 = require("../../../../responsePayload");
|
|
9
|
+
const timeEntryViewReducer_1 = require("../timeEntryViewReducer");
|
|
10
|
+
/**
|
|
11
|
+
* The 156-category catalog that fills the Category dropdown.
|
|
12
|
+
*
|
|
13
|
+
* `switchMap` here, unlike the entry epics: there is one catalog, so a second
|
|
14
|
+
* request supersedes the first rather than racing it.
|
|
15
|
+
*
|
|
16
|
+
* Zeni-only on the server (the billing marker is commercially sensitive), so
|
|
17
|
+
* a customer user gets a 401 — surfaced as Error rather than an empty list,
|
|
18
|
+
* because an empty catalog looks like "no categories exist".
|
|
19
|
+
*/
|
|
20
|
+
const fetchTaskCategoriesEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(timeEntryViewReducer_1.fetchTaskCategories.match), (0, operators_1.switchMap)(() => zeniAPI
|
|
21
|
+
.getJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/task-categories`)
|
|
22
|
+
.pipe((0, operators_2.mergeMap)((response) => {
|
|
23
|
+
if ((0, responsePayload_1.isSuccessResponse)(response) && response.data != null) {
|
|
24
|
+
return (0, rxjs_1.of)((0, timeEntryReducer_1.updateTaskCategories)(response.data.task_categories ?? []), (0, timeEntryViewReducer_1.fetchTaskCategoriesSuccess)());
|
|
25
|
+
}
|
|
26
|
+
return (0, rxjs_1.of)((0, timeEntryViewReducer_1.fetchTaskCategoriesFailure)(response.status));
|
|
27
|
+
}), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, timeEntryViewReducer_1.fetchTaskCategoriesFailure)((0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'Fetch task categories errored out' + JSON.stringify(error))))))));
|
|
28
|
+
exports.fetchTaskCategoriesEpic = fetchTaskCategoriesEpic;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ActionsObservable, StateObservable } from 'redux-observable';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { updateTimeEntries } from '../../../../entity/timeEntry/timeEntryReducer';
|
|
4
|
+
import { RootState } from '../../../../rootStateTypes';
|
|
5
|
+
import { ZeniAPI } from '../../../../zeniAPI';
|
|
6
|
+
import { fetchTimeEntriesForRange, fetchTimeEntriesForRangeFailure, fetchTimeEntriesForRangeSuccess, fetchTimeEntriesForTask, fetchTimeEntriesForTaskFailure, fetchTimeEntriesForTaskSuccess } from '../timeEntryViewReducer';
|
|
7
|
+
export type ActionType = ReturnType<typeof fetchTimeEntriesForTask> | ReturnType<typeof fetchTimeEntriesForTaskSuccess> | ReturnType<typeof fetchTimeEntriesForTaskFailure> | ReturnType<typeof fetchTimeEntriesForRange> | ReturnType<typeof fetchTimeEntriesForRangeSuccess> | ReturnType<typeof fetchTimeEntriesForRangeFailure> | ReturnType<typeof updateTimeEntries>;
|
|
8
|
+
/**
|
|
9
|
+
* Every entry on one task, all users — the task detail breakdown.
|
|
10
|
+
*
|
|
11
|
+
* `mergeMap`, not `switchMap`: a detail page and an expanded board row can
|
|
12
|
+
* ask for two different tasks at once, and cancelling the first would leave
|
|
13
|
+
* its breakdown stuck In-Progress forever.
|
|
14
|
+
*/
|
|
15
|
+
export declare const fetchTimeEntriesForTaskEpic: (actions$: ActionsObservable<ActionType>, state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
|
|
16
|
+
/**
|
|
17
|
+
* One user's entries in an inclusive date range — the weekly timesheet.
|
|
18
|
+
*
|
|
19
|
+
* Needs the `is_deleted + user_id + entry_date` composite index (api #593).
|
|
20
|
+
* Without it the server returns 500, not an empty list, so the failure is
|
|
21
|
+
* surfaced rather than rendered as a blank week.
|
|
22
|
+
*/
|
|
23
|
+
export declare const fetchTimeEntriesForRangeEpic: (actions$: ActionsObservable<ActionType>, _state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchTimeEntriesForRangeEpic = exports.fetchTimeEntriesForTaskEpic = void 0;
|
|
4
|
+
const rxjs_1 = require("rxjs");
|
|
5
|
+
const operators_1 = require("rxjs/operators");
|
|
6
|
+
const timeEntryReducer_1 = require("../../../../entity/timeEntry/timeEntryReducer");
|
|
7
|
+
const responsePayload_1 = require("../../../../responsePayload");
|
|
8
|
+
const timeEntryViewReducer_1 = require("../timeEntryViewReducer");
|
|
9
|
+
const timeEntryViewState_1 = require("../timeEntryViewState");
|
|
10
|
+
/**
|
|
11
|
+
* Every entry on one task, all users — the task detail breakdown.
|
|
12
|
+
*
|
|
13
|
+
* `mergeMap`, not `switchMap`: a detail page and an expanded board row can
|
|
14
|
+
* ask for two different tasks at once, and cancelling the first would leave
|
|
15
|
+
* its breakdown stuck In-Progress forever.
|
|
16
|
+
*/
|
|
17
|
+
const fetchTimeEntriesForTaskEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(timeEntryViewReducer_1.fetchTimeEntriesForTask.match), (0, operators_1.mergeMap)((action) => {
|
|
18
|
+
const { taskId, cacheOverride } = action.payload;
|
|
19
|
+
const cached = state$.value.timeEntryViewState.timeEntryIdsByTaskId[taskId];
|
|
20
|
+
const status = state$.value.timeEntryViewState.fetchStateByTaskId[taskId];
|
|
21
|
+
// The reducer has already flipped this request to In-Progress, so the
|
|
22
|
+
// guard reads the COMPLETED cache, never the fetch state -- a fetch
|
|
23
|
+
// state is not an in-flight signal.
|
|
24
|
+
if (cacheOverride !== true &&
|
|
25
|
+
cached != null &&
|
|
26
|
+
status?.fetchState === 'In-Progress') {
|
|
27
|
+
return (0, rxjs_1.of)((0, timeEntryViewReducer_1.fetchTimeEntriesForTaskSuccess)({ taskId, timeEntryIds: cached }));
|
|
28
|
+
}
|
|
29
|
+
return zeniAPI
|
|
30
|
+
.getJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/tasks/${taskId}/time-entries`)
|
|
31
|
+
.pipe((0, operators_1.mergeMap)((response) => {
|
|
32
|
+
if ((0, responsePayload_1.isSuccessResponse)(response) && response.data != null) {
|
|
33
|
+
const entries = response.data.time_entries ?? [];
|
|
34
|
+
return (0, rxjs_1.of)((0, timeEntryReducer_1.updateTimeEntries)(entries), (0, timeEntryViewReducer_1.fetchTimeEntriesForTaskSuccess)({
|
|
35
|
+
taskId,
|
|
36
|
+
timeEntryIds: entries.map((entry) => entry.time_entry_id),
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
return (0, rxjs_1.of)((0, timeEntryViewReducer_1.fetchTimeEntriesForTaskFailure)({ taskId, status: response.status }));
|
|
40
|
+
}), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, timeEntryViewReducer_1.fetchTimeEntriesForTaskFailure)({
|
|
41
|
+
taskId,
|
|
42
|
+
status: (0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'Fetch time entries for task errored out' +
|
|
43
|
+
JSON.stringify(error)),
|
|
44
|
+
}))));
|
|
45
|
+
}));
|
|
46
|
+
exports.fetchTimeEntriesForTaskEpic = fetchTimeEntriesForTaskEpic;
|
|
47
|
+
/**
|
|
48
|
+
* One user's entries in an inclusive date range — the weekly timesheet.
|
|
49
|
+
*
|
|
50
|
+
* Needs the `is_deleted + user_id + entry_date` composite index (api #593).
|
|
51
|
+
* Without it the server returns 500, not an empty list, so the failure is
|
|
52
|
+
* surfaced rather than rendered as a blank week.
|
|
53
|
+
*/
|
|
54
|
+
const fetchTimeEntriesForRangeEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(timeEntryViewReducer_1.fetchTimeEntriesForRange.match), (0, operators_1.mergeMap)((action) => {
|
|
55
|
+
const { userId, startDate, endDate } = action.payload;
|
|
56
|
+
const rangeKey = (0, timeEntryViewState_1.toTimeEntryRangeKey)(userId, startDate, endDate);
|
|
57
|
+
const query = `user_id=${encodeURIComponent(userId)}&start_date=${encodeURIComponent(startDate)}&end_date=${encodeURIComponent(endDate)}`;
|
|
58
|
+
return zeniAPI
|
|
59
|
+
.getJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/time-entries?${query}`)
|
|
60
|
+
.pipe((0, operators_1.mergeMap)((response) => {
|
|
61
|
+
if ((0, responsePayload_1.isSuccessResponse)(response) && response.data != null) {
|
|
62
|
+
const entries = response.data.time_entries ?? [];
|
|
63
|
+
return (0, rxjs_1.of)((0, timeEntryReducer_1.updateTimeEntries)(entries), (0, timeEntryViewReducer_1.fetchTimeEntriesForRangeSuccess)({
|
|
64
|
+
rangeKey,
|
|
65
|
+
timeEntryIds: entries.map((entry) => entry.time_entry_id),
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
return (0, rxjs_1.of)((0, timeEntryViewReducer_1.fetchTimeEntriesForRangeFailure)({
|
|
69
|
+
rangeKey,
|
|
70
|
+
status: response.status,
|
|
71
|
+
}));
|
|
72
|
+
}), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, timeEntryViewReducer_1.fetchTimeEntriesForRangeFailure)({
|
|
73
|
+
rangeKey,
|
|
74
|
+
status: (0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'Fetch time entries for range errored out' +
|
|
75
|
+
JSON.stringify(error)),
|
|
76
|
+
}))));
|
|
77
|
+
}));
|
|
78
|
+
exports.fetchTimeEntriesForRangeEpic = fetchTimeEntriesForRangeEpic;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ActionsObservable, StateObservable } from 'redux-observable';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { updateTaskTimeSpentMinutes } from '../../../../entity/task/taskReducer';
|
|
4
|
+
import { removeTimeEntry, updateTimeEntries } from '../../../../entity/timeEntry/timeEntryReducer';
|
|
5
|
+
import { RootState } from '../../../../rootStateTypes';
|
|
6
|
+
import { ZeniAPI } from '../../../../zeniAPI';
|
|
7
|
+
import { deleteTimeEntry, logTime, logTimeFailure, logTimeSuccess, timeEntryWriteFailure, timeEntryWriteSuccess, updateTimeEntryMinutes } from '../timeEntryViewReducer';
|
|
8
|
+
export type ActionType = ReturnType<typeof logTime> | ReturnType<typeof logTimeSuccess> | ReturnType<typeof logTimeFailure> | ReturnType<typeof updateTimeEntryMinutes> | ReturnType<typeof deleteTimeEntry> | ReturnType<typeof timeEntryWriteSuccess> | ReturnType<typeof timeEntryWriteFailure> | ReturnType<typeof updateTimeEntries> | ReturnType<typeof removeTimeEntry> | ReturnType<typeof updateTaskTimeSpentMinutes>;
|
|
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 declare const logTimeEpic: (actions$: ActionsObservable<ActionType>, _state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
|
|
16
|
+
/** Correct an entry's minutes. Owner, or a Zeni user as the admin path. */
|
|
17
|
+
export declare const updateTimeEntryEpic: (actions$: ActionsObservable<ActionType>, _state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
|
|
18
|
+
/**
|
|
19
|
+
* Soft delete. The row stays in Datastore for the audit trail but leaves the
|
|
20
|
+
* totals, so the client drops it from the store rather than keeping something
|
|
21
|
+
* every reader would then have to remember to filter.
|
|
22
|
+
*/
|
|
23
|
+
export declare const deleteTimeEntryEpic: (actions$: ActionsObservable<ActionType>, state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deleteTimeEntryEpic = exports.updateTimeEntryEpic = exports.logTimeEpic = void 0;
|
|
4
|
+
const rxjs_1 = require("rxjs");
|
|
5
|
+
const operators_1 = require("rxjs/operators");
|
|
6
|
+
const taskReducer_1 = require("../../../../entity/task/taskReducer");
|
|
7
|
+
const timeEntryReducer_1 = require("../../../../entity/timeEntry/timeEntryReducer");
|
|
8
|
+
const timeEntrySelector_1 = require("../../../../entity/timeEntry/timeEntrySelector");
|
|
9
|
+
const responsePayload_1 = require("../../../../responsePayload");
|
|
10
|
+
const timeEntryViewReducer_1 = require("../timeEntryViewReducer");
|
|
11
|
+
const unexpected = (what, error) => (0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', `${what} errored out${JSON.stringify(error)}`);
|
|
12
|
+
/**
|
|
13
|
+
* Log time against a task.
|
|
14
|
+
*
|
|
15
|
+
* `mergeMap`, not `switchMap`: two rows on the board can be edited in quick
|
|
16
|
+
* succession and cancelling the first would silently drop that entry.
|
|
17
|
+
*/
|
|
18
|
+
const logTimeEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(timeEntryViewReducer_1.logTime.match), (0, operators_1.mergeMap)((action) => {
|
|
19
|
+
const { taskId, minutes, entryDate, source, identifier } = action.payload;
|
|
20
|
+
return zeniAPI
|
|
21
|
+
.postAndGetJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/tasks/${taskId}/time-entries`, {
|
|
22
|
+
minutes,
|
|
23
|
+
entry_date: entryDate,
|
|
24
|
+
source,
|
|
25
|
+
// Sent only when the caller has one. The server treats it as the
|
|
26
|
+
// entity key, which is what makes a retried timer stop idempotent.
|
|
27
|
+
...(identifier != null ? { identifier } : {}),
|
|
28
|
+
})
|
|
29
|
+
.pipe((0, operators_1.mergeMap)((response) => {
|
|
30
|
+
if ((0, responsePayload_1.isSuccessResponse)(response) && response.data != null) {
|
|
31
|
+
const { time_entry, task_time_spent_minutes } = response.data;
|
|
32
|
+
return (0, rxjs_1.of)((0, timeEntryReducer_1.updateTimeEntries)([time_entry]),
|
|
33
|
+
// The board reads `Task.time_spent` per row, so it has to be
|
|
34
|
+
// told the new total; otherwise the row keeps the old number
|
|
35
|
+
// until something refetches the whole list.
|
|
36
|
+
(0, taskReducer_1.updateTaskTimeSpentMinutes)({
|
|
37
|
+
taskId,
|
|
38
|
+
minutes: task_time_spent_minutes ?? 0,
|
|
39
|
+
}), (0, timeEntryViewReducer_1.logTimeSuccess)({ taskId, timeEntryId: time_entry.time_entry_id }));
|
|
40
|
+
}
|
|
41
|
+
return (0, rxjs_1.of)((0, timeEntryViewReducer_1.logTimeFailure)({ taskId, status: response.status }));
|
|
42
|
+
}), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, timeEntryViewReducer_1.logTimeFailure)({ taskId, status: unexpected('Log time', error) }))));
|
|
43
|
+
}));
|
|
44
|
+
exports.logTimeEpic = logTimeEpic;
|
|
45
|
+
/** Correct an entry's minutes. Owner, or a Zeni user as the admin path. */
|
|
46
|
+
const updateTimeEntryEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(timeEntryViewReducer_1.updateTimeEntryMinutes.match), (0, operators_1.mergeMap)((action) => {
|
|
47
|
+
const { timeEntryId, minutes } = action.payload;
|
|
48
|
+
return zeniAPI
|
|
49
|
+
.putAndGetJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/time-entries/${timeEntryId}`, { minutes })
|
|
50
|
+
.pipe((0, operators_1.mergeMap)((response) => {
|
|
51
|
+
if ((0, responsePayload_1.isSuccessResponse)(response) && response.data != null) {
|
|
52
|
+
return (0, rxjs_1.of)((0, timeEntryReducer_1.updateTimeEntries)([response.data.time_entry]), (0, timeEntryViewReducer_1.timeEntryWriteSuccess)({ timeEntryId }));
|
|
53
|
+
}
|
|
54
|
+
return (0, rxjs_1.of)((0, timeEntryViewReducer_1.timeEntryWriteFailure)({ timeEntryId, status: response.status }));
|
|
55
|
+
}), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, timeEntryViewReducer_1.timeEntryWriteFailure)({
|
|
56
|
+
timeEntryId,
|
|
57
|
+
status: unexpected('Update time entry', error),
|
|
58
|
+
}))));
|
|
59
|
+
}));
|
|
60
|
+
exports.updateTimeEntryEpic = updateTimeEntryEpic;
|
|
61
|
+
/**
|
|
62
|
+
* Soft delete. The row stays in Datastore for the audit trail but leaves the
|
|
63
|
+
* totals, so the client drops it from the store rather than keeping something
|
|
64
|
+
* every reader would then have to remember to filter.
|
|
65
|
+
*/
|
|
66
|
+
const deleteTimeEntryEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(timeEntryViewReducer_1.deleteTimeEntry.match), (0, operators_1.mergeMap)((action) => {
|
|
67
|
+
const timeEntryId = action.payload;
|
|
68
|
+
// Read the task id BEFORE the delete: the entry is about to leave the
|
|
69
|
+
// store, and the view slice needs to know which task's id list to prune.
|
|
70
|
+
const taskId = (0, timeEntrySelector_1.getTimeEntryById)(state$.value.timeEntryState, timeEntryId)?.taskId;
|
|
71
|
+
return zeniAPI
|
|
72
|
+
.deleteAndGetJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/time-entries/${timeEntryId}`)
|
|
73
|
+
.pipe((0, operators_1.mergeMap)((response) => {
|
|
74
|
+
if ((0, responsePayload_1.isSuccessResponse)(response)) {
|
|
75
|
+
return (0, rxjs_1.of)((0, timeEntryReducer_1.removeTimeEntry)(timeEntryId), (0, timeEntryViewReducer_1.timeEntryWriteSuccess)({ timeEntryId, removedFromTaskId: taskId }));
|
|
76
|
+
}
|
|
77
|
+
return (0, rxjs_1.of)((0, timeEntryViewReducer_1.timeEntryWriteFailure)({ timeEntryId, status: response.status }));
|
|
78
|
+
}), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, timeEntryViewReducer_1.timeEntryWriteFailure)({
|
|
79
|
+
timeEntryId,
|
|
80
|
+
status: unexpected('Delete time entry', error),
|
|
81
|
+
}))));
|
|
82
|
+
}));
|
|
83
|
+
exports.deleteTimeEntryEpic = deleteTimeEntryEpic;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ID } from '../../../commonStateTypes/common';
|
|
2
|
+
import { TaskCategoryPayload, TimeEntryPayload } from '../../../entity/timeEntry/timeEntryPayload';
|
|
3
|
+
import { ZeniAPIResponse } from '../../../responsePayload';
|
|
4
|
+
export interface TimeEntriesPayload {
|
|
5
|
+
time_entries: TimeEntryPayload[];
|
|
6
|
+
total_minutes: number;
|
|
7
|
+
}
|
|
8
|
+
export interface TimeEntryRangePayload extends TimeEntriesPayload {
|
|
9
|
+
end_date: string;
|
|
10
|
+
start_date: string;
|
|
11
|
+
user_id: ID;
|
|
12
|
+
}
|
|
13
|
+
export interface TimeEntryWritePayload {
|
|
14
|
+
/** The ids whose `time_spent` the server recomputed. The client refreshes
|
|
15
|
+
* exactly these board rows instead of the whole list. */
|
|
16
|
+
recomputed_task_ids: ID[];
|
|
17
|
+
time_entry: TimeEntryPayload;
|
|
18
|
+
task_time_spent_minutes?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface TaskCategoriesPayload {
|
|
21
|
+
count: number;
|
|
22
|
+
task_categories: TaskCategoryPayload[];
|
|
23
|
+
}
|
|
24
|
+
export type TimeEntriesResponse = ZeniAPIResponse<TimeEntriesPayload>;
|
|
25
|
+
export type TimeEntryRangeResponse = ZeniAPIResponse<TimeEntryRangePayload>;
|
|
26
|
+
export type TimeEntryWriteResponse = ZeniAPIResponse<TimeEntryWritePayload>;
|
|
27
|
+
export type TaskCategoriesResponse = ZeniAPIResponse<TaskCategoriesPayload>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ID } from '../../../commonStateTypes/common';
|
|
2
|
+
import { ZeniAPIStatus } from '../../../responsePayload';
|
|
3
|
+
import { TimeEntryRangeKey, TimeEntryViewState } from './timeEntryViewState';
|
|
4
|
+
export declare const initialState: TimeEntryViewState;
|
|
5
|
+
export declare const fetchTimeEntriesForTask: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[taskId: string, cacheOverride?: any], {
|
|
6
|
+
taskId: string;
|
|
7
|
+
cacheOverride: any;
|
|
8
|
+
}, "timeEntryView/fetchTimeEntriesForTask", never, never>, fetchTimeEntriesForTaskSuccess: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
|
|
9
|
+
taskId: ID;
|
|
10
|
+
timeEntryIds: ID[];
|
|
11
|
+
}, "timeEntryView/fetchTimeEntriesForTaskSuccess">, fetchTimeEntriesForTaskFailure: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
|
|
12
|
+
status: ZeniAPIStatus;
|
|
13
|
+
taskId: ID;
|
|
14
|
+
}, "timeEntryView/fetchTimeEntriesForTaskFailure">, fetchTimeEntriesForRange: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[userId: string, startDate: string, endDate: string, cacheOverride?: any], {
|
|
15
|
+
userId: string;
|
|
16
|
+
startDate: string;
|
|
17
|
+
endDate: string;
|
|
18
|
+
cacheOverride: any;
|
|
19
|
+
}, "timeEntryView/fetchTimeEntriesForRange", never, never>, fetchTimeEntriesForRangeSuccess: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
|
|
20
|
+
rangeKey: TimeEntryRangeKey;
|
|
21
|
+
timeEntryIds: ID[];
|
|
22
|
+
}, "timeEntryView/fetchTimeEntriesForRangeSuccess">, fetchTimeEntriesForRangeFailure: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
|
|
23
|
+
rangeKey: TimeEntryRangeKey;
|
|
24
|
+
status: ZeniAPIStatus;
|
|
25
|
+
}, "timeEntryView/fetchTimeEntriesForRangeFailure">, logTime: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[taskId: string, minutes: number, entryDate: string, source?: "manual" | "timer" | undefined, identifier?: string | undefined], {
|
|
26
|
+
taskId: string;
|
|
27
|
+
minutes: number;
|
|
28
|
+
entryDate: string;
|
|
29
|
+
source: "manual" | "timer";
|
|
30
|
+
identifier: string | undefined;
|
|
31
|
+
}, "timeEntryView/logTime", never, never>, logTimeSuccess: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
|
|
32
|
+
taskId: ID;
|
|
33
|
+
timeEntryId: ID;
|
|
34
|
+
}, "timeEntryView/logTimeSuccess">, logTimeFailure: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
|
|
35
|
+
status: ZeniAPIStatus;
|
|
36
|
+
taskId: ID;
|
|
37
|
+
}, "timeEntryView/logTimeFailure">, updateTimeEntryMinutes: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[timeEntryId: string, minutes: number], {
|
|
38
|
+
timeEntryId: string;
|
|
39
|
+
minutes: number;
|
|
40
|
+
}, "timeEntryView/updateTimeEntryMinutes", never, never>, deleteTimeEntry: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "timeEntryView/deleteTimeEntry">, timeEntryWriteSuccess: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
|
|
41
|
+
timeEntryId: ID;
|
|
42
|
+
removedFromTaskId?: ID;
|
|
43
|
+
}, "timeEntryView/timeEntryWriteSuccess">, timeEntryWriteFailure: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
|
|
44
|
+
status: ZeniAPIStatus;
|
|
45
|
+
timeEntryId: ID;
|
|
46
|
+
}, "timeEntryView/timeEntryWriteFailure">, fetchTaskCategories: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"timeEntryView/fetchTaskCategories">, fetchTaskCategoriesSuccess: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"timeEntryView/fetchTaskCategoriesSuccess">, fetchTaskCategoriesFailure: import("@reduxjs/toolkit").ActionCreatorWithPayload<ZeniAPIStatus<Record<string, unknown>>, "timeEntryView/fetchTaskCategoriesFailure">;
|
|
47
|
+
declare const _default: import("redux").Reducer<TimeEntryViewState>;
|
|
48
|
+
export default _default;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var _a;
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.fetchTaskCategoriesFailure = exports.fetchTaskCategoriesSuccess = exports.fetchTaskCategories = exports.timeEntryWriteFailure = exports.timeEntryWriteSuccess = exports.deleteTimeEntry = exports.updateTimeEntryMinutes = exports.logTimeFailure = exports.logTimeSuccess = exports.logTime = exports.fetchTimeEntriesForRangeFailure = exports.fetchTimeEntriesForRangeSuccess = exports.fetchTimeEntriesForRange = exports.fetchTimeEntriesForTaskFailure = exports.fetchTimeEntriesForTaskSuccess = exports.fetchTimeEntriesForTask = exports.initialState = void 0;
|
|
5
|
+
const toolkit_1 = require("@reduxjs/toolkit");
|
|
6
|
+
const rootActions_1 = require("../../../rootActions");
|
|
7
|
+
const NOT_STARTED = { fetchState: 'Not-Started', error: undefined };
|
|
8
|
+
exports.initialState = {
|
|
9
|
+
timeEntryIdsByTaskId: {},
|
|
10
|
+
fetchStateByTaskId: {},
|
|
11
|
+
timeEntryIdsByRangeKey: {},
|
|
12
|
+
fetchStateByRangeKey: {},
|
|
13
|
+
saveStateByTaskId: {},
|
|
14
|
+
saveStateByTimeEntryId: {},
|
|
15
|
+
taskCategoryFetchState: NOT_STARTED,
|
|
16
|
+
};
|
|
17
|
+
const timeEntryView = (0, toolkit_1.createSlice)({
|
|
18
|
+
name: 'timeEntryView',
|
|
19
|
+
initialState: exports.initialState,
|
|
20
|
+
reducers: {
|
|
21
|
+
fetchTimeEntriesForTask: {
|
|
22
|
+
prepare(taskId, cacheOverride = false) {
|
|
23
|
+
return { payload: { taskId, cacheOverride } };
|
|
24
|
+
},
|
|
25
|
+
reducer(draft, action) {
|
|
26
|
+
draft.fetchStateByTaskId[action.payload.taskId] = {
|
|
27
|
+
fetchState: 'In-Progress',
|
|
28
|
+
error: undefined,
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
fetchTimeEntriesForTaskSuccess(draft, action) {
|
|
33
|
+
const { taskId, timeEntryIds } = action.payload;
|
|
34
|
+
// Replaced, not merged: the server is authoritative about which entries
|
|
35
|
+
// a task has, and a merge would resurrect one that was just deleted.
|
|
36
|
+
draft.timeEntryIdsByTaskId[taskId] = timeEntryIds;
|
|
37
|
+
draft.fetchStateByTaskId[taskId] = {
|
|
38
|
+
fetchState: 'Completed',
|
|
39
|
+
error: undefined,
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
fetchTimeEntriesForTaskFailure(draft, action) {
|
|
43
|
+
draft.fetchStateByTaskId[action.payload.taskId] = {
|
|
44
|
+
fetchState: 'Error',
|
|
45
|
+
error: action.payload.status,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
fetchTimeEntriesForRange: {
|
|
49
|
+
prepare(userId, startDate, endDate, cacheOverride = false) {
|
|
50
|
+
return { payload: { userId, startDate, endDate, cacheOverride } };
|
|
51
|
+
},
|
|
52
|
+
reducer(draft, action) {
|
|
53
|
+
const { userId, startDate, endDate } = action.payload;
|
|
54
|
+
draft.fetchStateByRangeKey[`${userId}_${startDate}_${endDate}`] = {
|
|
55
|
+
fetchState: 'In-Progress',
|
|
56
|
+
error: undefined,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
fetchTimeEntriesForRangeSuccess(draft, action) {
|
|
61
|
+
const { rangeKey, timeEntryIds } = action.payload;
|
|
62
|
+
draft.timeEntryIdsByRangeKey[rangeKey] = timeEntryIds;
|
|
63
|
+
draft.fetchStateByRangeKey[rangeKey] = {
|
|
64
|
+
fetchState: 'Completed',
|
|
65
|
+
error: undefined,
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
fetchTimeEntriesForRangeFailure(draft, action) {
|
|
69
|
+
draft.fetchStateByRangeKey[action.payload.rangeKey] = {
|
|
70
|
+
fetchState: 'Error',
|
|
71
|
+
error: action.payload.status,
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
logTime: {
|
|
75
|
+
prepare(taskId, minutes, entryDate, source = 'manual', identifier) {
|
|
76
|
+
return { payload: { taskId, minutes, entryDate, source, identifier } };
|
|
77
|
+
},
|
|
78
|
+
reducer(draft, action) {
|
|
79
|
+
draft.saveStateByTaskId[action.payload.taskId] = {
|
|
80
|
+
fetchState: 'In-Progress',
|
|
81
|
+
error: undefined,
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
logTimeSuccess(draft, action) {
|
|
86
|
+
const { taskId, timeEntryId } = action.payload;
|
|
87
|
+
draft.saveStateByTaskId[taskId] = {
|
|
88
|
+
fetchState: 'Completed',
|
|
89
|
+
error: undefined,
|
|
90
|
+
};
|
|
91
|
+
const existing = draft.timeEntryIdsByTaskId[taskId] ?? [];
|
|
92
|
+
// Guarded against a duplicate id: a retried write with the same
|
|
93
|
+
// deterministic identifier returns the SAME entry, and pushing it twice
|
|
94
|
+
// would double it in the breakdown even though the server has one row.
|
|
95
|
+
if (!existing.includes(timeEntryId)) {
|
|
96
|
+
draft.timeEntryIdsByTaskId[taskId] = [...existing, timeEntryId];
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
logTimeFailure(draft, action) {
|
|
100
|
+
draft.saveStateByTaskId[action.payload.taskId] = {
|
|
101
|
+
fetchState: 'Error',
|
|
102
|
+
error: action.payload.status,
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
updateTimeEntryMinutes: {
|
|
106
|
+
prepare(timeEntryId, minutes) {
|
|
107
|
+
return { payload: { timeEntryId, minutes } };
|
|
108
|
+
},
|
|
109
|
+
reducer(draft, action) {
|
|
110
|
+
draft.saveStateByTimeEntryId[action.payload.timeEntryId] = {
|
|
111
|
+
fetchState: 'In-Progress',
|
|
112
|
+
error: undefined,
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
deleteTimeEntry(draft, action) {
|
|
117
|
+
draft.saveStateByTimeEntryId[action.payload] = {
|
|
118
|
+
fetchState: 'In-Progress',
|
|
119
|
+
error: undefined,
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
timeEntryWriteSuccess(draft, action) {
|
|
123
|
+
const { timeEntryId, removedFromTaskId } = action.payload;
|
|
124
|
+
draft.saveStateByTimeEntryId[timeEntryId] = {
|
|
125
|
+
fetchState: 'Completed',
|
|
126
|
+
error: undefined,
|
|
127
|
+
};
|
|
128
|
+
if (removedFromTaskId != null) {
|
|
129
|
+
draft.timeEntryIdsByTaskId[removedFromTaskId] = (draft.timeEntryIdsByTaskId[removedFromTaskId] ?? []).filter((candidate) => candidate !== timeEntryId);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
timeEntryWriteFailure(draft, action) {
|
|
133
|
+
draft.saveStateByTimeEntryId[action.payload.timeEntryId] = {
|
|
134
|
+
fetchState: 'Error',
|
|
135
|
+
error: action.payload.status,
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
fetchTaskCategories(draft) {
|
|
139
|
+
draft.taskCategoryFetchState = {
|
|
140
|
+
fetchState: 'In-Progress',
|
|
141
|
+
error: undefined,
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
fetchTaskCategoriesSuccess(draft) {
|
|
145
|
+
draft.taskCategoryFetchState = {
|
|
146
|
+
fetchState: 'Completed',
|
|
147
|
+
error: undefined,
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
fetchTaskCategoriesFailure(draft, action) {
|
|
151
|
+
draft.taskCategoryFetchState = {
|
|
152
|
+
fetchState: 'Error',
|
|
153
|
+
error: action.payload,
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
extraReducers: (builder) => {
|
|
158
|
+
builder.addCase(rootActions_1.clearAll, (draft) => {
|
|
159
|
+
Object.assign(draft, exports.initialState);
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
_a = timeEntryView.actions, exports.fetchTimeEntriesForTask = _a.fetchTimeEntriesForTask, exports.fetchTimeEntriesForTaskSuccess = _a.fetchTimeEntriesForTaskSuccess, exports.fetchTimeEntriesForTaskFailure = _a.fetchTimeEntriesForTaskFailure, exports.fetchTimeEntriesForRange = _a.fetchTimeEntriesForRange, exports.fetchTimeEntriesForRangeSuccess = _a.fetchTimeEntriesForRangeSuccess, exports.fetchTimeEntriesForRangeFailure = _a.fetchTimeEntriesForRangeFailure, exports.logTime = _a.logTime, exports.logTimeSuccess = _a.logTimeSuccess, exports.logTimeFailure = _a.logTimeFailure, exports.updateTimeEntryMinutes = _a.updateTimeEntryMinutes, exports.deleteTimeEntry = _a.deleteTimeEntry, exports.timeEntryWriteSuccess = _a.timeEntryWriteSuccess, exports.timeEntryWriteFailure = _a.timeEntryWriteFailure, exports.fetchTaskCategories = _a.fetchTaskCategories, exports.fetchTaskCategoriesSuccess = _a.fetchTaskCategoriesSuccess, exports.fetchTaskCategoriesFailure = _a.fetchTaskCategoriesFailure;
|
|
164
|
+
exports.default = timeEntryView.reducer;
|