@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.
@@ -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
- autoReversed: { count: 0, amount: 0 },
177
- pendingReview: { count: 0, amount: 0 },
178
- totalThisClose: { count: 0, amount: 0 },
179
- ai: { count: 0 },
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 rows = (matchIds ?? [])
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
- .filter((row) => matchesSearch(row, uiState.searchString));
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 autoReversedForKpi = reversed.filter((row) => row.kind === 'auto_reversed');
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
- ai: {
29
+ actualsReceived: {
30
30
  count: number;
31
+ expectedCount: number;
32
+ vendorsPending: number;
31
33
  };
32
34
  autoReversed: {
33
- amount: number;
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
- totalThisClose: {
44
- amount: number;
45
- count: number;
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
- autoReversed: { count: 0, amount: 0 },
193
- pendingReview: { count: 0, amount: 0 },
194
- totalThisClose: { count: 0, amount: 0 },
195
- ai: { count: 0 },
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 rows = (matchIds ?? [])
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
- .filter((row) => matchesSearch(row, uiState.searchString));
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 autoReversedForKpi = reversed.filter((row) => row.kind === 'auto_reversed');
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-beta1MM",
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",