@zeniai/client-epic-state 5.2.35-beta1MM → 5.2.35-beta2MM
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/esm/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +73 -32
- package/lib/view/expenseAutomationView/selectorTypes/actualMatchingViewSelectorTypes.d.ts +10 -8
- package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.d.ts +2 -1
- package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +74 -32
- package/package.json +1 -1
|
@@ -171,18 +171,82 @@ function sortActualMatchingRows(rows, uiState) {
|
|
|
171
171
|
const lodashOrder = uiState.sortOrder === 'ascending' ? 'asc' : 'desc';
|
|
172
172
|
return orderBy(rows, [(row) => getActualMatchingSortValue(row, uiState.sortKey)], [lodashOrder]);
|
|
173
173
|
}
|
|
174
|
+
const ESTIMATED_MINUTES_PER_PENDING_MATCH = 4;
|
|
174
175
|
function emptyKpiSummary() {
|
|
175
176
|
return {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
manual: { count: 0 },
|
|
177
|
+
actualsReceived: { count: 0, expectedCount: 0, vendorsPending: 0 },
|
|
178
|
+
autoReversed: { count: 0, percent: 0, clearedAmount: 0 },
|
|
179
|
+
pendingReview: { count: 0, amount: 0, estimatedMinutesToClear: 0 },
|
|
180
|
+
variance: { percent: 0, averagePercentPerMatch: 0, netAmount: 0 },
|
|
181
181
|
};
|
|
182
182
|
}
|
|
183
183
|
function sumRowAmounts(rows) {
|
|
184
184
|
return rows.reduce((sum, row) => sum + getActualMatchingAmount(row), 0);
|
|
185
185
|
}
|
|
186
|
+
function uniqueVendorCount(rows) {
|
|
187
|
+
const ids = new Set();
|
|
188
|
+
rows.forEach((row) => {
|
|
189
|
+
if (row.vendorId != null && row.vendorId !== '') {
|
|
190
|
+
ids.add(row.vendorId);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (row.vendorName !== '') {
|
|
194
|
+
ids.add(row.vendorName);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
return ids.size;
|
|
198
|
+
}
|
|
199
|
+
function rowVariancePercent(row) {
|
|
200
|
+
const accrued = row.accruedAmount?.amount ?? row.estimatedAmount.amount;
|
|
201
|
+
const actual = row.actualAmount?.amount;
|
|
202
|
+
if (actual == null || accrued === 0) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
return ((actual - accrued) / accrued) * 100;
|
|
206
|
+
}
|
|
207
|
+
export function buildActualMatchingKpiSummary(rows) {
|
|
208
|
+
if (rows.length === 0) {
|
|
209
|
+
return emptyKpiSummary();
|
|
210
|
+
}
|
|
211
|
+
const autoReversed = rows.filter((row) => row.kind === 'auto_reversed');
|
|
212
|
+
const pendingReview = rows.filter((row) => row.kind === 'pending_review');
|
|
213
|
+
const receivedCount = rows.length;
|
|
214
|
+
const rowsWithActual = rows.filter((row) => row.actualAmount?.amount != null);
|
|
215
|
+
const accruedTotal = rowsWithActual.reduce((sum, row) => sum + (row.accruedAmount?.amount ?? row.estimatedAmount.amount), 0);
|
|
216
|
+
const actualTotal = rowsWithActual.reduce((sum, row) => sum + (row.actualAmount?.amount ?? 0), 0);
|
|
217
|
+
const variancePercents = rows
|
|
218
|
+
.map(rowVariancePercent)
|
|
219
|
+
.filter((percent) => percent != null);
|
|
220
|
+
return {
|
|
221
|
+
actualsReceived: {
|
|
222
|
+
count: receivedCount,
|
|
223
|
+
// List API does not return still-expected accruals; hide "of N expected"
|
|
224
|
+
// in the strip until expectedCount is greater than received.
|
|
225
|
+
expectedCount: receivedCount,
|
|
226
|
+
vendorsPending: uniqueVendorCount(pendingReview),
|
|
227
|
+
},
|
|
228
|
+
autoReversed: {
|
|
229
|
+
count: autoReversed.length,
|
|
230
|
+
percent: Math.round((autoReversed.length / receivedCount) * 100),
|
|
231
|
+
clearedAmount: sumRowAmounts(autoReversed),
|
|
232
|
+
},
|
|
233
|
+
pendingReview: {
|
|
234
|
+
count: pendingReview.length,
|
|
235
|
+
amount: sumRowAmounts(pendingReview),
|
|
236
|
+
estimatedMinutesToClear: pendingReview.length * ESTIMATED_MINUTES_PER_PENDING_MATCH,
|
|
237
|
+
},
|
|
238
|
+
variance: {
|
|
239
|
+
percent: accruedTotal === 0
|
|
240
|
+
? 0
|
|
241
|
+
: ((actualTotal - accruedTotal) / accruedTotal) * 100,
|
|
242
|
+
averagePercentPerMatch: variancePercents.length === 0
|
|
243
|
+
? 0
|
|
244
|
+
: variancePercents.reduce((sum, percent) => sum + percent, 0) /
|
|
245
|
+
variancePercents.length,
|
|
246
|
+
netAmount: actualTotal - accruedTotal,
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
186
250
|
export function getExpenseAutomationActualMatchingMatchView(state, matchId) {
|
|
187
251
|
const view = state.expenseAutomationActualMatchingViewState;
|
|
188
252
|
return {
|
|
@@ -198,38 +262,15 @@ export function getExpenseAutomationActualMatchingView(state) {
|
|
|
198
262
|
const selectedPeriod = getSelectedMonthYearForCurrentTenant(state);
|
|
199
263
|
const monthYearPeriodId = selectedPeriod != null ? toMonthYearPeriodId(selectedPeriod) : undefined;
|
|
200
264
|
const matchIds = monthYearPeriodId != null ? matchIdsByPeriod[monthYearPeriodId] : undefined;
|
|
201
|
-
const
|
|
265
|
+
const allRows = (matchIds ?? [])
|
|
202
266
|
.map((matchId) => getActualMatchingMatchById(state.actualMatchingState, matchId))
|
|
203
267
|
.filter((match) => match != null)
|
|
204
268
|
.filter((match) => match.members.length > 0)
|
|
205
|
-
.map(toActualMatchingRow)
|
|
206
|
-
|
|
269
|
+
.map(toActualMatchingRow);
|
|
270
|
+
const rows = allRows.filter((row) => matchesSearch(row, uiState.searchString));
|
|
207
271
|
const reversed = sortActualMatchingRows(rows.filter((row) => isReversedMatchKind(row.kind)), uiState);
|
|
208
272
|
const pendingReview = sortActualMatchingRows(rows.filter((row) => row.kind === 'pending_review'), uiState);
|
|
209
|
-
const
|
|
210
|
-
const listedRows = [...reversed, ...pendingReview];
|
|
211
|
-
const kpiSummary = listedRows.length === 0
|
|
212
|
-
? emptyKpiSummary()
|
|
213
|
-
: {
|
|
214
|
-
autoReversed: {
|
|
215
|
-
count: autoReversedForKpi.length,
|
|
216
|
-
amount: sumRowAmounts(autoReversedForKpi),
|
|
217
|
-
},
|
|
218
|
-
pendingReview: {
|
|
219
|
-
count: pendingReview.length,
|
|
220
|
-
amount: sumRowAmounts(pendingReview),
|
|
221
|
-
},
|
|
222
|
-
totalThisClose: {
|
|
223
|
-
count: listedRows.length,
|
|
224
|
-
amount: sumRowAmounts(listedRows),
|
|
225
|
-
},
|
|
226
|
-
ai: {
|
|
227
|
-
count: listedRows.filter((row) => row.origin === 'agent').length,
|
|
228
|
-
},
|
|
229
|
-
manual: {
|
|
230
|
-
count: listedRows.filter((row) => row.origin === 'human').length,
|
|
231
|
-
},
|
|
232
|
-
};
|
|
273
|
+
const kpiSummary = buildActualMatchingKpiSummary(allRows);
|
|
233
274
|
const searchedAccruals = matchModal.accrualSearch.results
|
|
234
275
|
.map((key) => getJEOneTimeAccrualByKey(key, state.jeSchedulesState))
|
|
235
276
|
.filter((accrual) => accrual != null);
|
|
@@ -26,23 +26,25 @@ export interface ActualMatchingRow extends JEOneTimeAccrual {
|
|
|
26
26
|
transactionType?: string;
|
|
27
27
|
}
|
|
28
28
|
export interface ActualMatchingKpiSummary {
|
|
29
|
-
|
|
29
|
+
actualsReceived: {
|
|
30
30
|
count: number;
|
|
31
|
+
expectedCount: number;
|
|
32
|
+
vendorsPending: number;
|
|
31
33
|
};
|
|
32
34
|
autoReversed: {
|
|
33
|
-
|
|
34
|
-
count: number;
|
|
35
|
-
};
|
|
36
|
-
manual: {
|
|
35
|
+
clearedAmount: number;
|
|
37
36
|
count: number;
|
|
37
|
+
percent: number;
|
|
38
38
|
};
|
|
39
39
|
pendingReview: {
|
|
40
40
|
amount: number;
|
|
41
41
|
count: number;
|
|
42
|
+
estimatedMinutesToClear: number;
|
|
42
43
|
};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
variance: {
|
|
45
|
+
averagePercentPerMatch: number;
|
|
46
|
+
netAmount: number;
|
|
47
|
+
percent: number;
|
|
46
48
|
};
|
|
47
49
|
}
|
|
48
50
|
export interface ActualMatchingMatchModalView {
|
|
@@ -6,7 +6,7 @@ import { JEOneTimeAccrual } from '../../../entity/jeSchedules/jeSchedulesSelecto
|
|
|
6
6
|
import { JEOneTimeAccrualMatchSuggestion } from '../../../entity/jeSchedules/jeSchedulesState';
|
|
7
7
|
import { RootState } from '../../../rootStateTypes';
|
|
8
8
|
import { ZeniDate } from '../../../zeniDayJS';
|
|
9
|
-
import { ActualMatchingRow, ActualMatchingRowKind, ExpenseAutomationActualMatchingViewSelector } from '../selectorTypes/actualMatchingViewSelectorTypes';
|
|
9
|
+
import { ActualMatchingKpiSummary, ActualMatchingRow, ActualMatchingRowKind, ExpenseAutomationActualMatchingViewSelector } from '../selectorTypes/actualMatchingViewSelectorTypes';
|
|
10
10
|
export declare function getSelectedMonthYearForCurrentTenant(state: RootState): MonthYearPeriod | undefined;
|
|
11
11
|
export declare function parseActualMatchingMatchId(matchId: string): {
|
|
12
12
|
transactionIntegrationId: string;
|
|
@@ -21,6 +21,7 @@ export declare function isReversedMatchKind(kind: ActualMatchingRowKind): boolea
|
|
|
21
21
|
export declare function getClearedMatchedAmount(accrual: JEOneTimeAccrual): number | undefined;
|
|
22
22
|
export declare function getActualMatchingAmount(row: ActualMatchingRow): number;
|
|
23
23
|
export declare function getActualMatchingReversalDate(accrual: JEOneTimeAccrual): ZeniDate | undefined;
|
|
24
|
+
export declare function buildActualMatchingKpiSummary(rows: ActualMatchingRow[]): ActualMatchingKpiSummary;
|
|
24
25
|
export interface ExpenseAutomationActualMatchingMatchView {
|
|
25
26
|
detailFetchState: FetchStateAndError;
|
|
26
27
|
refreshStatus: FetchStateAndError;
|
|
@@ -12,6 +12,7 @@ exports.isReversedMatchKind = isReversedMatchKind;
|
|
|
12
12
|
exports.getClearedMatchedAmount = getClearedMatchedAmount;
|
|
13
13
|
exports.getActualMatchingAmount = getActualMatchingAmount;
|
|
14
14
|
exports.getActualMatchingReversalDate = getActualMatchingReversalDate;
|
|
15
|
+
exports.buildActualMatchingKpiSummary = buildActualMatchingKpiSummary;
|
|
15
16
|
exports.getExpenseAutomationActualMatchingMatchView = getExpenseAutomationActualMatchingMatchView;
|
|
16
17
|
exports.getExpenseAutomationActualMatchingView = getExpenseAutomationActualMatchingView;
|
|
17
18
|
const orderBy_1 = __importDefault(require("lodash/orderBy"));
|
|
@@ -187,18 +188,82 @@ function sortActualMatchingRows(rows, uiState) {
|
|
|
187
188
|
const lodashOrder = uiState.sortOrder === 'ascending' ? 'asc' : 'desc';
|
|
188
189
|
return (0, orderBy_1.default)(rows, [(row) => getActualMatchingSortValue(row, uiState.sortKey)], [lodashOrder]);
|
|
189
190
|
}
|
|
191
|
+
const ESTIMATED_MINUTES_PER_PENDING_MATCH = 4;
|
|
190
192
|
function emptyKpiSummary() {
|
|
191
193
|
return {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
manual: { count: 0 },
|
|
194
|
+
actualsReceived: { count: 0, expectedCount: 0, vendorsPending: 0 },
|
|
195
|
+
autoReversed: { count: 0, percent: 0, clearedAmount: 0 },
|
|
196
|
+
pendingReview: { count: 0, amount: 0, estimatedMinutesToClear: 0 },
|
|
197
|
+
variance: { percent: 0, averagePercentPerMatch: 0, netAmount: 0 },
|
|
197
198
|
};
|
|
198
199
|
}
|
|
199
200
|
function sumRowAmounts(rows) {
|
|
200
201
|
return rows.reduce((sum, row) => sum + getActualMatchingAmount(row), 0);
|
|
201
202
|
}
|
|
203
|
+
function uniqueVendorCount(rows) {
|
|
204
|
+
const ids = new Set();
|
|
205
|
+
rows.forEach((row) => {
|
|
206
|
+
if (row.vendorId != null && row.vendorId !== '') {
|
|
207
|
+
ids.add(row.vendorId);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (row.vendorName !== '') {
|
|
211
|
+
ids.add(row.vendorName);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
return ids.size;
|
|
215
|
+
}
|
|
216
|
+
function rowVariancePercent(row) {
|
|
217
|
+
const accrued = row.accruedAmount?.amount ?? row.estimatedAmount.amount;
|
|
218
|
+
const actual = row.actualAmount?.amount;
|
|
219
|
+
if (actual == null || accrued === 0) {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
return ((actual - accrued) / accrued) * 100;
|
|
223
|
+
}
|
|
224
|
+
function buildActualMatchingKpiSummary(rows) {
|
|
225
|
+
if (rows.length === 0) {
|
|
226
|
+
return emptyKpiSummary();
|
|
227
|
+
}
|
|
228
|
+
const autoReversed = rows.filter((row) => row.kind === 'auto_reversed');
|
|
229
|
+
const pendingReview = rows.filter((row) => row.kind === 'pending_review');
|
|
230
|
+
const receivedCount = rows.length;
|
|
231
|
+
const rowsWithActual = rows.filter((row) => row.actualAmount?.amount != null);
|
|
232
|
+
const accruedTotal = rowsWithActual.reduce((sum, row) => sum + (row.accruedAmount?.amount ?? row.estimatedAmount.amount), 0);
|
|
233
|
+
const actualTotal = rowsWithActual.reduce((sum, row) => sum + (row.actualAmount?.amount ?? 0), 0);
|
|
234
|
+
const variancePercents = rows
|
|
235
|
+
.map(rowVariancePercent)
|
|
236
|
+
.filter((percent) => percent != null);
|
|
237
|
+
return {
|
|
238
|
+
actualsReceived: {
|
|
239
|
+
count: receivedCount,
|
|
240
|
+
// List API does not return still-expected accruals; hide "of N expected"
|
|
241
|
+
// in the strip until expectedCount is greater than received.
|
|
242
|
+
expectedCount: receivedCount,
|
|
243
|
+
vendorsPending: uniqueVendorCount(pendingReview),
|
|
244
|
+
},
|
|
245
|
+
autoReversed: {
|
|
246
|
+
count: autoReversed.length,
|
|
247
|
+
percent: Math.round((autoReversed.length / receivedCount) * 100),
|
|
248
|
+
clearedAmount: sumRowAmounts(autoReversed),
|
|
249
|
+
},
|
|
250
|
+
pendingReview: {
|
|
251
|
+
count: pendingReview.length,
|
|
252
|
+
amount: sumRowAmounts(pendingReview),
|
|
253
|
+
estimatedMinutesToClear: pendingReview.length * ESTIMATED_MINUTES_PER_PENDING_MATCH,
|
|
254
|
+
},
|
|
255
|
+
variance: {
|
|
256
|
+
percent: accruedTotal === 0
|
|
257
|
+
? 0
|
|
258
|
+
: ((actualTotal - accruedTotal) / accruedTotal) * 100,
|
|
259
|
+
averagePercentPerMatch: variancePercents.length === 0
|
|
260
|
+
? 0
|
|
261
|
+
: variancePercents.reduce((sum, percent) => sum + percent, 0) /
|
|
262
|
+
variancePercents.length,
|
|
263
|
+
netAmount: actualTotal - accruedTotal,
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
202
267
|
function getExpenseAutomationActualMatchingMatchView(state, matchId) {
|
|
203
268
|
const view = state.expenseAutomationActualMatchingViewState;
|
|
204
269
|
return {
|
|
@@ -214,38 +279,15 @@ function getExpenseAutomationActualMatchingView(state) {
|
|
|
214
279
|
const selectedPeriod = getSelectedMonthYearForCurrentTenant(state);
|
|
215
280
|
const monthYearPeriodId = selectedPeriod != null ? (0, timePeriod_1.toMonthYearPeriodId)(selectedPeriod) : undefined;
|
|
216
281
|
const matchIds = monthYearPeriodId != null ? matchIdsByPeriod[monthYearPeriodId] : undefined;
|
|
217
|
-
const
|
|
282
|
+
const allRows = (matchIds ?? [])
|
|
218
283
|
.map((matchId) => (0, actualMatchingSelector_1.getActualMatchingMatchById)(state.actualMatchingState, matchId))
|
|
219
284
|
.filter((match) => match != null)
|
|
220
285
|
.filter((match) => match.members.length > 0)
|
|
221
|
-
.map(toActualMatchingRow)
|
|
222
|
-
|
|
286
|
+
.map(toActualMatchingRow);
|
|
287
|
+
const rows = allRows.filter((row) => matchesSearch(row, uiState.searchString));
|
|
223
288
|
const reversed = sortActualMatchingRows(rows.filter((row) => isReversedMatchKind(row.kind)), uiState);
|
|
224
289
|
const pendingReview = sortActualMatchingRows(rows.filter((row) => row.kind === 'pending_review'), uiState);
|
|
225
|
-
const
|
|
226
|
-
const listedRows = [...reversed, ...pendingReview];
|
|
227
|
-
const kpiSummary = listedRows.length === 0
|
|
228
|
-
? emptyKpiSummary()
|
|
229
|
-
: {
|
|
230
|
-
autoReversed: {
|
|
231
|
-
count: autoReversedForKpi.length,
|
|
232
|
-
amount: sumRowAmounts(autoReversedForKpi),
|
|
233
|
-
},
|
|
234
|
-
pendingReview: {
|
|
235
|
-
count: pendingReview.length,
|
|
236
|
-
amount: sumRowAmounts(pendingReview),
|
|
237
|
-
},
|
|
238
|
-
totalThisClose: {
|
|
239
|
-
count: listedRows.length,
|
|
240
|
-
amount: sumRowAmounts(listedRows),
|
|
241
|
-
},
|
|
242
|
-
ai: {
|
|
243
|
-
count: listedRows.filter((row) => row.origin === 'agent').length,
|
|
244
|
-
},
|
|
245
|
-
manual: {
|
|
246
|
-
count: listedRows.filter((row) => row.origin === 'human').length,
|
|
247
|
-
},
|
|
248
|
-
};
|
|
290
|
+
const kpiSummary = buildActualMatchingKpiSummary(allRows);
|
|
249
291
|
const searchedAccruals = matchModal.accrualSearch.results
|
|
250
292
|
.map((key) => (0, jeSchedulesSelector_1.getJEOneTimeAccrualByKey)(key, state.jeSchedulesState))
|
|
251
293
|
.filter((accrual) => accrual != null);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeniai/client-epic-state",
|
|
3
|
-
"version": "5.2.35-
|
|
3
|
+
"version": "5.2.35-beta2MM",
|
|
4
4
|
"description": "Shared module between Web & Mobile containing required abstractions for state management, async network communication. ",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "lib/esm/index.js",
|