@zeniai/client-epic-state 5.2.35-beta8MM → 5.2.35-beta9MM

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/lib/entity/actualMatching/actualMatchingPayload.js +8 -2
  2. package/lib/entity/aiCfo/aiCfoReducer.js +78 -4
  3. package/lib/entity/aiCfo/aiCfoState.d.ts +16 -0
  4. package/lib/esm/entity/actualMatching/actualMatchingPayload.js +8 -2
  5. package/lib/esm/entity/aiCfo/aiCfoReducer.js +79 -5
  6. package/lib/esm/view/expenseAutomationView/epics/actualMatching/matchAndReverseAccrualsEpic.js +3 -3
  7. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/prefetchOtherJeSchedulesTabEpic.js +4 -5
  8. package/lib/esm/view/expenseAutomationView/expenseAutomationViewSelector.js +4 -2
  9. package/lib/esm/view/expenseAutomationView/payload/actualMatchingPayload.js +1 -1
  10. package/lib/esm/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +16 -1
  11. package/lib/esm/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +29 -9
  12. package/lib/esm/view/financeStatement/financeStatementReducer.js +17 -27
  13. package/lib/esm/view/spendManagement/billPay/billList/billListSelector.js +3 -3
  14. package/lib/view/expenseAutomationView/epics/actualMatching/matchAndReverseAccrualsEpic.js +3 -3
  15. package/lib/view/expenseAutomationView/epics/jeSchedule/prefetchOtherJeSchedulesTabEpic.js +4 -5
  16. package/lib/view/expenseAutomationView/expenseAutomationViewSelector.js +4 -2
  17. package/lib/view/expenseAutomationView/payload/actualMatchingPayload.js +1 -1
  18. package/lib/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +16 -1
  19. package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.d.ts +1 -1
  20. package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +29 -9
  21. package/lib/view/expenseAutomationView/types/actualMatchingViewState.d.ts +1 -0
  22. package/lib/view/financeStatement/financeStatementReducer.js +17 -27
  23. package/lib/view/spendManagement/billPay/billList/billListSelector.js +2 -2
  24. package/package.json +1 -1
@@ -12,6 +12,12 @@ const MATCH_KINDS = [
12
12
  function toMatchKind(raw) {
13
13
  return (0, stringToUnion_1.stringToUnion)(raw, MATCH_KINDS);
14
14
  }
15
+ function toMatchMemberAccrual(member, matchCurrencyCode) {
16
+ const currency = member.currency != null && member.currency !== ''
17
+ ? member.currency
18
+ : matchCurrencyCode;
19
+ return (0, jeSchedulesPayload_1.toJEOneTimeAccrual)({ ...member, currency });
20
+ }
15
21
  function toActualMatchingMatch(payload, fallbackCurrency) {
16
22
  const currencyCode = payload.currency_code != null && payload.currency_code !== ''
17
23
  ? payload.currency_code
@@ -34,7 +40,7 @@ function toActualMatchingMatch(payload, fallbackCurrency) {
34
40
  matchShape: payload.match_shape ?? undefined,
35
41
  accruedAmount: (0, amount_1.toAmount)(payload.accrued_amount, currencyCode, currencySymbol),
36
42
  actualAmount: (0, amount_1.toAmount)(payload.actual_amount, currencyCode, currencySymbol),
37
- members: payload.members.map(jeSchedulesPayload_1.toJEOneTimeAccrual),
38
- leftovers: (payload.leftovers ?? []).map(jeSchedulesPayload_1.toJEOneTimeAccrual),
43
+ members: payload.members.map((member) => toMatchMemberAccrual(member, currencyCode)),
44
+ leftovers: (payload.leftovers ?? []).map((member) => toMatchMemberAccrual(member, currencyCode)),
39
45
  };
40
46
  }
@@ -7,6 +7,7 @@ const rootActions_1 = require("../../rootActions");
7
7
  const zeniDayJS_1 = require("../../zeniDayJS");
8
8
  const aiCfoState_1 = require("./aiCfoState");
9
9
  exports.initialAiCfoState = {
10
+ deletedChatSessionIds: [],
10
11
  aiCfoByChatSessionId: {},
11
12
  partialQuestionAnswers: {},
12
13
  syntheticAnswersByChatSessionId: {},
@@ -474,6 +475,11 @@ const toResponseBlockType = (answer, userId) => {
474
475
  createdAt: (0, zeniDayJS_1.date)(timestamp),
475
476
  };
476
477
  };
478
+ // How many deleted session ids to remember. The tombstone only has to outlive
479
+ // an in-flight history request, which is seconds, so this is generous. Capped
480
+ // because `clearSession` would otherwise grow the array for the life of the
481
+ // store and every `setChatHistory` scans it.
482
+ const DELETED_SESSION_MEMORY = 100;
477
483
  const aiCfo = (0, toolkit_1.createSlice)({
478
484
  name: 'aiCfo',
479
485
  initialState: exports.initialAiCfoState,
@@ -495,8 +501,25 @@ const aiCfo = (0, toolkit_1.createSlice)({
495
501
  setSessions(draft, action) {
496
502
  action.payload.forEach((chatSessionPayload) => {
497
503
  const { chat_session_id } = chatSessionPayload;
498
- // This is to prevent overwriting the session if it is already in state
499
- if (draft.aiCfoByChatSessionId[chat_session_id] == null) {
504
+ if (draft.aiCfoByChatSessionId[chat_session_id] != null) {
505
+ // The list is authoritative for a session's metadata, and a session
506
+ // seeded by `setChatHistory` has guessed metadata: no summary, and a
507
+ // `createdAt` taken from one page of a newest-first paginated
508
+ // history, which is later than the session really started.
509
+ // `questionAnswers` is left alone — the list must never wipe a
510
+ // thread that is on screen.
511
+ const entry = draft.aiCfoByChatSessionId[chat_session_id];
512
+ const incoming = toChatSession(chatSessionPayload);
513
+ entry.chatSession = {
514
+ ...incoming,
515
+ // Never downgrade a title already in state: a page of the list can
516
+ // carry a nullish summary, and assigning it wholesale would blank
517
+ // a header that was correct.
518
+ chatSessionSummary: incoming.chatSessionSummary ??
519
+ entry.chatSession.chatSessionSummary,
520
+ };
521
+ }
522
+ else {
500
523
  draft.aiCfoByChatSessionId[chat_session_id] = {
501
524
  chatSession: toChatSession(chatSessionPayload),
502
525
  questionAnswers: [],
@@ -509,8 +532,52 @@ const aiCfo = (0, toolkit_1.createSlice)({
509
532
  setChatHistory(draft, action) {
510
533
  const { chatSessionId, history, isPaginationComplete = false, } = action.payload;
511
534
  if (draft.aiCfoByChatSessionId[chatSessionId] == null) {
512
- console.warn(`session with id ${chatSessionId} not found in setChatHistory`);
513
- return;
535
+ // Sessions created server-side (a routine run) are opened by deep
536
+ // link, so their history can arrive before the sessions list has
537
+ // registered them. Dropping it here loses the messages for good: the
538
+ // response's null page token sets `hasMore: false`, which is the
539
+ // condition the refetch is guarded on. The response is itself proof
540
+ // the session exists, so seed the entry from the messages.
541
+ if (history.length === 0) {
542
+ return;
543
+ }
544
+ // A deleted session. Its history request is never cancelled
545
+ // (`mergeMap`), so seeding would resurrect the conversation into the
546
+ // rail, where clicking it 404s. Only this path is guarded:
547
+ // `clearSession` is optimistic, so if the server still lists the
548
+ // session, `setSessions` re-adding it is correct.
549
+ if (draft.deletedChatSessionIds?.includes(chatSessionId) === true) {
550
+ return;
551
+ }
552
+ // The human's id, taken off a message they actually sent. The API
553
+ // returns newest first, so `history[0]` is usually the agent's reply,
554
+ // and `chatSession.userId` is read as "whose conversation is this".
555
+ const owner = history.find((message) => message.sender === 'user') ?? history[0];
556
+ // Oldest message in the page, not `history[0]`: a session cannot
557
+ // start after its own messages, and the API returns newest first.
558
+ //
559
+ // Compared as instants, not strings. The wire format is an RFC 1123
560
+ // HTTP-date ("Thu, 03 Sep 2026 12:34:56 GMT") because `set_200`
561
+ // bypasses chat's isoformat encoder, and lexically "Mon…" precedes
562
+ // "Sat…" while being the later day. Unparseable values are filtered
563
+ // out, not compared: every comparison against NaN is false, so one
564
+ // used as the reduce's seed would carry through to an Invalid Date.
565
+ const oldest = history
566
+ .map((message) => ({ at: Date.parse(message.created_at), message }))
567
+ .filter(({ at }) => !Number.isNaN(at))
568
+ .reduce((min, entry) => (min == null || entry.at < min.at ? entry : min), undefined)?.message;
569
+ draft.aiCfoByChatSessionId[chatSessionId] = {
570
+ chatSession: {
571
+ chatSessionId,
572
+ userId: owner.user_id,
573
+ chatSessionSummary: undefined,
574
+ // Nothing in the page carried a usable timestamp. "Now" is a
575
+ // guess, but it is a sane one and it keeps the row out of the
576
+ // Invalid-Date behaviour above; the sessions list corrects it.
577
+ createdAt: oldest != null ? (0, zeniDayJS_1.date)(oldest.created_at) : (0, zeniDayJS_1.dateNow)(),
578
+ },
579
+ questionAnswers: [],
580
+ };
514
581
  }
515
582
  const session = draft.aiCfoByChatSessionId[chatSessionId];
516
583
  // Check if we have an existing partial Q&A pair for this session
@@ -790,6 +857,13 @@ const aiCfo = (0, toolkit_1.createSlice)({
790
857
  },
791
858
  clearSession(draft, action) {
792
859
  const sessionId = action.payload;
860
+ draft.deletedChatSessionIds ?? (draft.deletedChatSessionIds = []);
861
+ if (!draft.deletedChatSessionIds.includes(sessionId)) {
862
+ draft.deletedChatSessionIds.push(sessionId);
863
+ if (draft.deletedChatSessionIds.length > DELETED_SESSION_MEMORY) {
864
+ draft.deletedChatSessionIds.splice(0, draft.deletedChatSessionIds.length - DELETED_SESSION_MEMORY);
865
+ }
866
+ }
793
867
  delete draft.aiCfoByChatSessionId[sessionId];
794
868
  delete draft.syntheticAnswersByChatSessionId[sessionId];
795
869
  if (draft.partialQuestionAnswers?.[sessionId] != null) {
@@ -241,4 +241,20 @@ export interface AiCfoState {
241
241
  aiCfoByChatSessionId: Record<ID, ChatSessionWithMessages>;
242
242
  partialQuestionAnswers: Record<ID, AiCfoQuestionWithAnswer | undefined>;
243
243
  syntheticAnswersByChatSessionId: Record<ID, SyntheticAiCfoAnswer[]>;
244
+ /**
245
+ * Sessions this client has deleted. `setChatHistory` seeds an entry for a
246
+ * session it has not seen, so without a tombstone an in-flight history
247
+ * response arriving after the delete would recreate the whole conversation
248
+ * and put the deleted chat back in the rail. `fetchChatHistoryEpic` uses
249
+ * `mergeMap`, so that request is never cancelled. Same race, and the same
250
+ * reasoning, as `deletedScheduleIds` on the view slice. Bounded — it only
251
+ * needs to outlive an in-flight request.
252
+ *
253
+ * Optional because `AiCfoState` is public API of a package four apps
254
+ * consume: making it required turns a bug fix into a compile break for
255
+ * every literal of this type, in this repo and in theirs. The reducer's
256
+ * initial state always sets it, so it is only ever absent on a
257
+ * hand-built literal — which is exactly the case that must keep working.
258
+ */
259
+ deletedChatSessionIds?: ID[];
244
260
  }
@@ -9,6 +9,12 @@ const MATCH_KINDS = [
9
9
  function toMatchKind(raw) {
10
10
  return stringToUnion(raw, MATCH_KINDS);
11
11
  }
12
+ function toMatchMemberAccrual(member, matchCurrencyCode) {
13
+ const currency = member.currency != null && member.currency !== ''
14
+ ? member.currency
15
+ : matchCurrencyCode;
16
+ return toJEOneTimeAccrual({ ...member, currency });
17
+ }
12
18
  export function toActualMatchingMatch(payload, fallbackCurrency) {
13
19
  const currencyCode = payload.currency_code != null && payload.currency_code !== ''
14
20
  ? payload.currency_code
@@ -31,7 +37,7 @@ export function toActualMatchingMatch(payload, fallbackCurrency) {
31
37
  matchShape: payload.match_shape ?? undefined,
32
38
  accruedAmount: toAmount(payload.accrued_amount, currencyCode, currencySymbol),
33
39
  actualAmount: toAmount(payload.actual_amount, currencyCode, currencySymbol),
34
- members: payload.members.map(toJEOneTimeAccrual),
35
- leftovers: (payload.leftovers ?? []).map(toJEOneTimeAccrual),
40
+ members: payload.members.map((member) => toMatchMemberAccrual(member, currencyCode)),
41
+ leftovers: (payload.leftovers ?? []).map((member) => toMatchMemberAccrual(member, currencyCode)),
36
42
  };
37
43
  }
@@ -1,8 +1,9 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
2
  import { clearAll } from '../../rootActions';
3
- import { date as zeniDate } from '../../zeniDayJS';
3
+ import { dateNow, date as zeniDate } from '../../zeniDayJS';
4
4
  import { ALL_AI_CFO_ANSWER_RESPONSE_TYPES, toAiCfoAnswerResponseType, toAiCfoAnswerResponseTypeStrict, toAiCfoAnswerStateType, toAiCfoVisualizationTypeStrict, toInteractiveFormTypeStrict, toMessageSender, toMessageType, toYFormatScaleStrict, toYFormatTypeStrict, toYFormatUnitStrict, } from './aiCfoState';
5
5
  export const initialAiCfoState = {
6
+ deletedChatSessionIds: [],
6
7
  aiCfoByChatSessionId: {},
7
8
  partialQuestionAnswers: {},
8
9
  syntheticAnswersByChatSessionId: {},
@@ -468,6 +469,11 @@ const toResponseBlockType = (answer, userId) => {
468
469
  createdAt: zeniDate(timestamp),
469
470
  };
470
471
  };
472
+ // How many deleted session ids to remember. The tombstone only has to outlive
473
+ // an in-flight history request, which is seconds, so this is generous. Capped
474
+ // because `clearSession` would otherwise grow the array for the life of the
475
+ // store and every `setChatHistory` scans it.
476
+ const DELETED_SESSION_MEMORY = 100;
471
477
  const aiCfo = createSlice({
472
478
  name: 'aiCfo',
473
479
  initialState: initialAiCfoState,
@@ -489,8 +495,25 @@ const aiCfo = createSlice({
489
495
  setSessions(draft, action) {
490
496
  action.payload.forEach((chatSessionPayload) => {
491
497
  const { chat_session_id } = chatSessionPayload;
492
- // This is to prevent overwriting the session if it is already in state
493
- if (draft.aiCfoByChatSessionId[chat_session_id] == null) {
498
+ if (draft.aiCfoByChatSessionId[chat_session_id] != null) {
499
+ // The list is authoritative for a session's metadata, and a session
500
+ // seeded by `setChatHistory` has guessed metadata: no summary, and a
501
+ // `createdAt` taken from one page of a newest-first paginated
502
+ // history, which is later than the session really started.
503
+ // `questionAnswers` is left alone — the list must never wipe a
504
+ // thread that is on screen.
505
+ const entry = draft.aiCfoByChatSessionId[chat_session_id];
506
+ const incoming = toChatSession(chatSessionPayload);
507
+ entry.chatSession = {
508
+ ...incoming,
509
+ // Never downgrade a title already in state: a page of the list can
510
+ // carry a nullish summary, and assigning it wholesale would blank
511
+ // a header that was correct.
512
+ chatSessionSummary: incoming.chatSessionSummary ??
513
+ entry.chatSession.chatSessionSummary,
514
+ };
515
+ }
516
+ else {
494
517
  draft.aiCfoByChatSessionId[chat_session_id] = {
495
518
  chatSession: toChatSession(chatSessionPayload),
496
519
  questionAnswers: [],
@@ -503,8 +526,52 @@ const aiCfo = createSlice({
503
526
  setChatHistory(draft, action) {
504
527
  const { chatSessionId, history, isPaginationComplete = false, } = action.payload;
505
528
  if (draft.aiCfoByChatSessionId[chatSessionId] == null) {
506
- console.warn(`session with id ${chatSessionId} not found in setChatHistory`);
507
- return;
529
+ // Sessions created server-side (a routine run) are opened by deep
530
+ // link, so their history can arrive before the sessions list has
531
+ // registered them. Dropping it here loses the messages for good: the
532
+ // response's null page token sets `hasMore: false`, which is the
533
+ // condition the refetch is guarded on. The response is itself proof
534
+ // the session exists, so seed the entry from the messages.
535
+ if (history.length === 0) {
536
+ return;
537
+ }
538
+ // A deleted session. Its history request is never cancelled
539
+ // (`mergeMap`), so seeding would resurrect the conversation into the
540
+ // rail, where clicking it 404s. Only this path is guarded:
541
+ // `clearSession` is optimistic, so if the server still lists the
542
+ // session, `setSessions` re-adding it is correct.
543
+ if (draft.deletedChatSessionIds?.includes(chatSessionId) === true) {
544
+ return;
545
+ }
546
+ // The human's id, taken off a message they actually sent. The API
547
+ // returns newest first, so `history[0]` is usually the agent's reply,
548
+ // and `chatSession.userId` is read as "whose conversation is this".
549
+ const owner = history.find((message) => message.sender === 'user') ?? history[0];
550
+ // Oldest message in the page, not `history[0]`: a session cannot
551
+ // start after its own messages, and the API returns newest first.
552
+ //
553
+ // Compared as instants, not strings. The wire format is an RFC 1123
554
+ // HTTP-date ("Thu, 03 Sep 2026 12:34:56 GMT") because `set_200`
555
+ // bypasses chat's isoformat encoder, and lexically "Mon…" precedes
556
+ // "Sat…" while being the later day. Unparseable values are filtered
557
+ // out, not compared: every comparison against NaN is false, so one
558
+ // used as the reduce's seed would carry through to an Invalid Date.
559
+ const oldest = history
560
+ .map((message) => ({ at: Date.parse(message.created_at), message }))
561
+ .filter(({ at }) => !Number.isNaN(at))
562
+ .reduce((min, entry) => (min == null || entry.at < min.at ? entry : min), undefined)?.message;
563
+ draft.aiCfoByChatSessionId[chatSessionId] = {
564
+ chatSession: {
565
+ chatSessionId,
566
+ userId: owner.user_id,
567
+ chatSessionSummary: undefined,
568
+ // Nothing in the page carried a usable timestamp. "Now" is a
569
+ // guess, but it is a sane one and it keeps the row out of the
570
+ // Invalid-Date behaviour above; the sessions list corrects it.
571
+ createdAt: oldest != null ? zeniDate(oldest.created_at) : dateNow(),
572
+ },
573
+ questionAnswers: [],
574
+ };
508
575
  }
509
576
  const session = draft.aiCfoByChatSessionId[chatSessionId];
510
577
  // Check if we have an existing partial Q&A pair for this session
@@ -784,6 +851,13 @@ const aiCfo = createSlice({
784
851
  },
785
852
  clearSession(draft, action) {
786
853
  const sessionId = action.payload;
854
+ draft.deletedChatSessionIds ?? (draft.deletedChatSessionIds = []);
855
+ if (!draft.deletedChatSessionIds.includes(sessionId)) {
856
+ draft.deletedChatSessionIds.push(sessionId);
857
+ if (draft.deletedChatSessionIds.length > DELETED_SESSION_MEMORY) {
858
+ draft.deletedChatSessionIds.splice(0, draft.deletedChatSessionIds.length - DELETED_SESSION_MEMORY);
859
+ }
860
+ }
787
861
  delete draft.aiCfoByChatSessionId[sessionId];
788
862
  delete draft.syntheticAnswersByChatSessionId[sessionId];
789
863
  if (draft.partialQuestionAnswers?.[sessionId] != null) {
@@ -72,9 +72,9 @@ exhaustMap((action) => {
72
72
  transaction_sync_token: transactionSyncToken,
73
73
  accrual_ids: accrualIds,
74
74
  };
75
- const openedFromMatchId = state$.value.expenseAutomationActualMatchingViewState.matchModal
76
- .openedFromMatchId;
77
- const matchId = openedFromMatchId ?? `${transactionType}:${transactionIntegrationId}`;
75
+ // Key leftover results and snackbar vendor to the confirmed bill, not the
76
+ // row Match New was opened from (user can pick a different txn).
77
+ const matchId = `${transactionType.toLowerCase()}:${transactionIntegrationId}`;
78
78
  const vendorName = matchVendorName(state$.value, matchId);
79
79
  return zeniAPI
80
80
  .postAndGetJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/accruals/match-and-reverse`, body)
@@ -41,11 +41,10 @@ export const prefetchOtherJeSchedulesTabEpic = (actions$, state$) => actions$.pi
41
41
  month: period.start.month,
42
42
  year: period.start.year,
43
43
  });
44
- const actualMatching = state.expenseAutomationActualMatchingViewState;
45
- const amAlreadyLoading = actualMatching.fetchState === 'In-Progress' ||
46
- actualMatching.refreshStatus.fetchState === 'In-Progress';
47
- if (!amAlreadyLoading &&
48
- actualMatching.matchIdsByPeriod[periodId] == null) {
44
+ // `fetchState` is global across months. Skipping on In-Progress would drop
45
+ // October prefetch while September is still loading; September's success
46
+ // then marks the slice Completed and October looks loaded with no ids.
47
+ if (state.expenseAutomationActualMatchingViewState.matchIdsByPeriod[periodId] == null) {
49
48
  actions.push(fetchActualMatchingPage(period, true));
50
49
  }
51
50
  }
@@ -22,7 +22,7 @@ const isPreviousStepDisabled = (selectedTransactionCategorizationTab, allSteps,
22
22
  return allSteps[stepIndex - 1].isDisabled;
23
23
  };
24
24
  export function getExpenseAutomationView(state) {
25
- const { expenseAutomationViewState, tenantState, expenseAutomationMissingReceiptsViewState, expenseAutomationJESchedulesViewState, expenseAutomationActualMatchingViewState, expenseAutomationTransactionsViewState, expenseAutomationReconciliationViewState, monthEndCloseChecksState, companyState, } = state;
25
+ const { expenseAutomationViewState, tenantState, expenseAutomationMissingReceiptsViewState, expenseAutomationJESchedulesViewState, expenseAutomationTransactionsViewState, expenseAutomationReconciliationViewState, monthEndCloseChecksState, companyState, } = state;
26
26
  const { loggedInUser, currentTenantId } = tenantState;
27
27
  const currentTenant = getCurrentTenant(state);
28
28
  const { currentSelectedView, selectedPeriodByTenantId } = expenseAutomationViewState;
@@ -102,10 +102,12 @@ export function getExpenseAutomationView(state) {
102
102
  };
103
103
  break;
104
104
  }
105
+ // AM is optional (flag-off / background prefetch) and must not keep the
106
+ // page-level AND-complete fetchState stuck on Not-Started. Tab chrome uses
107
+ // currentSelectedViewFetchState, which already switches to AM when selected.
105
108
  const fetchStateVariables = [
106
109
  expenseAutomationMissingReceiptsViewState,
107
110
  expenseAutomationJESchedulesViewState,
108
- expenseAutomationActualMatchingViewState,
109
111
  expenseAutomationTransactionsView,
110
112
  expenseAutomationReconciliationViewState,
111
113
  ];
@@ -33,7 +33,7 @@ export function toActualMatchingTransactionSearchResult(payload, fallbackCurrenc
33
33
  return {
34
34
  transactionId: payload.transaction_id,
35
35
  transactionIntegrationId: payload.transaction_integration_id,
36
- transactionType: payload.transaction_type,
36
+ transactionType: (payload.transaction_type || '').toLowerCase(),
37
37
  lineId: payload.line_id,
38
38
  vendorId: payload.vendor_id ?? undefined,
39
39
  vendorName: payload.vendor_name ?? '',
@@ -22,6 +22,7 @@ export const initialAccrualSearchState = {
22
22
  export const initialMatchModalState = {
23
23
  isOpen: false,
24
24
  openedFromMatchId: undefined,
25
+ didPinPrefillTransactions: false,
25
26
  pinnedTransactionResults: [],
26
27
  transactionSearch: initialTransactionSearchState,
27
28
  accrualSearch: initialAccrualSearchState,
@@ -60,6 +61,7 @@ function resetMatchModalSearches(draft) {
60
61
  };
61
62
  draft.matchModal.lastBatchResults = undefined;
62
63
  draft.matchModal.openedFromMatchId = undefined;
64
+ draft.matchModal.didPinPrefillTransactions = false;
63
65
  draft.matchModal.pinnedTransactionResults = [];
64
66
  }
65
67
  const expenseAutomationActualMatchingView = createSlice({
@@ -84,6 +86,9 @@ const expenseAutomationActualMatchingView = createSlice({
84
86
  const { refreshViewInBackground } = action.payload;
85
87
  if (refreshViewInBackground) {
86
88
  draft.refreshStatus = { fetchState: 'In-Progress', error: undefined };
89
+ if (draft.fetchState === 'Not-Started') {
90
+ draft.fetchState = 'In-Progress';
91
+ }
87
92
  }
88
93
  else {
89
94
  draft.fetchState = 'In-Progress';
@@ -105,6 +110,10 @@ const expenseAutomationActualMatchingView = createSlice({
105
110
  draft.matchIdsByPeriod[periodId] = matchIds;
106
111
  if (refreshViewInBackground) {
107
112
  draft.refreshStatus = { fetchState: 'Completed', error: undefined };
113
+ if (draft.fetchState !== 'Completed') {
114
+ draft.fetchState = 'Completed';
115
+ draft.error = undefined;
116
+ }
108
117
  }
109
118
  else {
110
119
  draft.fetchState = 'Completed';
@@ -115,6 +124,10 @@ const expenseAutomationActualMatchingView = createSlice({
115
124
  const { status, refreshViewInBackground } = action.payload;
116
125
  if (refreshViewInBackground) {
117
126
  draft.refreshStatus = { fetchState: 'Error', error: status };
127
+ if (draft.fetchState !== 'Completed') {
128
+ draft.fetchState = 'Error';
129
+ draft.error = status;
130
+ }
118
131
  }
119
132
  else {
120
133
  draft.fetchState = 'Error';
@@ -254,7 +267,8 @@ const expenseAutomationActualMatchingView = createSlice({
254
267
  else {
255
268
  draft.matchModal.transactionSearch.results = results;
256
269
  if (draft.matchModal.openedFromMatchId != null &&
257
- draft.matchModal.pinnedTransactionResults.length === 0) {
270
+ !draft.matchModal.didPinPrefillTransactions) {
271
+ draft.matchModal.didPinPrefillTransactions = true;
258
272
  draft.matchModal.pinnedTransactionResults = results;
259
273
  }
260
274
  }
@@ -363,6 +377,7 @@ const expenseAutomationActualMatchingView = createSlice({
363
377
  fetchState: 'Completed',
364
378
  error: undefined,
365
379
  };
380
+ delete draft.lastMatchResultsByMatchId[matchId];
366
381
  draft.closeMatchAfterUndo =
367
382
  results.length > 0 &&
368
383
  results.every((result) => result.status === 'undone');
@@ -4,6 +4,7 @@ import { getActualMatchingMatchById } from '../../../entity/actualMatching/actua
4
4
  import { generateJEOneTimeAccrualKey } from '../../../entity/jeSchedules/jeScheduleHelper';
5
5
  import { getJEOneTimeAccrualByKey, } from '../../../entity/jeSchedules/jeSchedulesSelector';
6
6
  import { getCurrentTenant } from '../../../entity/tenant/tenantSelector';
7
+ import { date } from '../../../zeniDayJS';
7
8
  import { actualMatchingTransactionLineKey, } from '../types/actualMatchingViewState';
8
9
  export function getSelectedMonthYearForCurrentTenant(state) {
9
10
  const tenantId = getCurrentTenant(state)?.tenantId;
@@ -92,11 +93,18 @@ export function getClearedMatchedAmount(accrual) {
92
93
  export function getActualMatchingAmount(row) {
93
94
  return row.actualAmount?.amount ?? row.estimatedAmount.amount;
94
95
  }
95
- export function getActualMatchingReversalDate(accrual) {
96
- if (isPendingReviewAccrual(accrual)) {
97
- return accrual.matchSuggestion.matchedTransactionDate;
96
+ function parseMatchTransactionDate(transactionDate) {
97
+ if (transactionDate == null || transactionDate === '') {
98
+ return undefined;
98
99
  }
99
- return accrual.cleared?.matchedTransactionDate;
100
+ const parsed = date(transactionDate);
101
+ return Number.isFinite(parsed.valueOf()) ? parsed : undefined;
102
+ }
103
+ export function getActualMatchingReversalDate(accrual, matchTransactionDate) {
104
+ const matchedDate = isPendingReviewAccrual(accrual)
105
+ ? accrual.matchSuggestion.matchedTransactionDate
106
+ : accrual.cleared?.matchedTransactionDate;
107
+ return matchedDate ?? parseMatchTransactionDate(matchTransactionDate);
100
108
  }
101
109
  function collectGroupedMembers(members) {
102
110
  if (members.length <= 1) {
@@ -150,7 +158,7 @@ function toActualMatchingRow(match) {
150
158
  groupedMembers: collectGroupedMembers(match.members),
151
159
  intendedAccrualIds,
152
160
  leftovers: collectLeftoverMembers(match.leftovers),
153
- reversalDate: getActualMatchingReversalDate(first),
161
+ reversalDate: getActualMatchingReversalDate(first, match.transactionDate),
154
162
  vendorName: match.vendorName != null && match.vendorName !== ''
155
163
  ? match.vendorName
156
164
  : first.vendorName,
@@ -174,8 +182,13 @@ function getActualMatchingSortValue(row, sortKey) {
174
182
  switch (sortKey) {
175
183
  case 'vendor':
176
184
  return (row.vendorName ?? '').toLowerCase();
177
- case 'date':
178
- return row.reversalDate?.valueOf() ?? row.transactionDate;
185
+ case 'date': {
186
+ const millis = row.reversalDate?.valueOf() ??
187
+ (row.transactionDate != null && row.transactionDate !== ''
188
+ ? date(row.transactionDate).valueOf()
189
+ : 0);
190
+ return Number.isFinite(millis) ? millis : 0;
191
+ }
179
192
  case 'amount':
180
193
  return getActualMatchingAmount(row);
181
194
  case 'category':
@@ -363,9 +376,16 @@ export function getExpenseAutomationActualMatchingView(state) {
363
376
  ? { consumedLineIds }
364
377
  : {}),
365
378
  };
379
+ // `fetchState` is a flat slice field that survives a period switch, so it still
380
+ // reads 'Completed' before the newly selected period's ids land. Reporting
381
+ // 'Not-Started' until they do keeps the skeleton up instead of flashing the
382
+ // empty-complete chrome over a month that has not been fetched.
383
+ const fetchStatus = fetchState === 'Completed' && matchIds == null
384
+ ? { fetchState: 'Not-Started', error: undefined }
385
+ : { fetchState, error };
366
386
  return {
367
- fetchState,
368
- error,
387
+ fetchState: fetchStatus.fetchState,
388
+ error: fetchStatus.error,
369
389
  reversed,
370
390
  pendingReview,
371
391
  kpiSummary,
@@ -33,9 +33,18 @@ const financeStatement = createSlice({
33
33
  },
34
34
  },
35
35
  updateFinanceStatementTimeframe(draft, action) {
36
+ // Unrelated UI re-dispatches the *current* timeframe as a side effect (the
37
+ // reports page hangs its table scroll reset off that callback); dropping the
38
+ // anchor there would silently reset the user's period to the latest one.
39
+ if (draft.timeframe === action.payload) {
40
+ return;
41
+ }
36
42
  draft.timeframe = action.payload;
43
+ // A month anchor is meaningless once the timeframe becomes quarter/year, so
44
+ // a real change drops it and returns the width to the default — 12 months
45
+ // must not become 12 quarters. Order is the user's, so it survives.
37
46
  draft.selectedCOABalancesRange = {
38
- numberOfPeriods: draft.selectedCOABalancesRange.numberOfPeriods,
47
+ numberOfPeriods: initialFinanceStatementState.selectedCOABalancesRange.numberOfPeriods,
39
48
  orderBy: draft.selectedCOABalancesRange.orderBy,
40
49
  };
41
50
  },
@@ -77,8 +86,7 @@ const financeStatement = createSlice({
77
86
  draft.selectedReportId = action.payload;
78
87
  },
79
88
  updateFinanceStatementAdditionalBalancesSelection(draft, action) {
80
- const { firstMonthOfFY, additionalBalances: additionalBalancesPayload, coaBalances, maxNumOfPeriodsToHighlight, } = action.payload;
81
- const safeCoaBalances = coaBalances != null ? coaBalances : [];
89
+ const { additionalBalances: additionalBalancesPayload, maxNumOfPeriodsToHighlight, } = action.payload;
82
90
  const additionalBalances = additionalBalancesPayload ?? [];
83
91
  let tempAdditionalBalances = [...additionalBalances];
84
92
  if (additionalBalances.includes('this_period_vs_last_period') ||
@@ -94,30 +102,12 @@ const financeStatement = createSlice({
94
102
  draft.maxNumOfPeriodsToHighlight = maxNumOfPeriodsToHighlight;
95
103
  draft.isAdditionalBalancesShown =
96
104
  additionalBalances.length != 0 ? true : false;
97
- const { timeframe, selectedCOABalancesRange } = draft;
98
- if (safeCoaBalances.length > 0) {
99
- const thisPeriod = extractThisPeriod(action.payload.firstMonthOfFY, timeframe, selectedCOABalancesRange, safeCoaBalances);
100
- if (thisPeriod != null) {
101
- const selectedCoaBalancesRangeWithThisPeriod = {
102
- ...selectedCOABalancesRange,
103
- thisPeriod,
104
- };
105
- const selectionRanges = getSelectedAndHighlightedRangesForThisPeriod({
106
- firstMonthOfFY,
107
- thisPeriod: thisPeriod,
108
- coaBalances: safeCoaBalances,
109
- timeframe,
110
- maxNumOfPeriodsToHighlight: maxNumOfPeriodsToHighlight,
111
- currentSelection: {
112
- selectedCOABalancesRange: selectedCoaBalancesRangeWithThisPeriod,
113
- },
114
- orderBy: 'ascending_date',
115
- maxNumOfPeriodsToSelect: maxNumOfPeriodsToHighlight,
116
- });
117
- draft.selectedCOABalancesRange =
118
- selectionRanges.selectedCOABalancesRange;
119
- }
120
- }
105
+ // Column metadata only must not touch selectedCOABalancesRange. The
106
+ // reports page fires this on load, on resize and on every fetch-state
107
+ // transition (so also on report switch and on remount after a drill-down)
108
+ // with a viewport-derived period count that knows nothing about the range
109
+ // the user picked. The width default comes from initial state, and from
110
+ // updateFinanceStatementTimeframe on a real timeframe change.
121
111
  },
122
112
  updateDownloadState(draft, action) {
123
113
  draft.downloadState = action.payload;
@@ -8,7 +8,7 @@ import { isAwaitingMarkAsPaid, isBulkProcessing, isSodAdminFallbackEligibleForSt
8
8
  import { applyAdvancedFiltersOnList } from '../../spendManagementFilterHelpers';
9
9
  import { getSelectedWithdrawFromAccount } from '../billDetailView/billDetailViewSelector';
10
10
  import { getBillPayConfigBotEmail } from '../billPayConfig/billPayConfigSelector';
11
- import { getCategoryValueForBill } from './billListFilterHelpers';
11
+ import { getActualPaymentDate, getCategoryValueForBill, } from './billListFilterHelpers';
12
12
  import { ALL_BILL_TABS, getBillListKey, } from './billListState';
13
13
  export const getBillList = (state, currentTimePeriod, isBulkActionFeatureEnabled, loggedInUserId) => {
14
14
  const { billListState, billTransactionState, billPayConfigState, entityApprovalStatusState, userState, recurringBillState, billsBulkActionViewState, } = state;
@@ -432,7 +432,7 @@ export const getBillListOnSort = (sortKey, sortOrder, currentTab, transactions)
432
432
  }
433
433
  }
434
434
  else {
435
- if (Boolean(transaction.billPayInfo.paymentDate) === true) {
435
+ if (Boolean(getActualPaymentDate(transaction.billPayInfo)) === true) {
436
436
  transactionsWithSortKeyValue.push(transaction);
437
437
  }
438
438
  else {
@@ -517,7 +517,7 @@ export const getBillListOnSort = (sortKey, sortOrder, currentTab, transactions)
517
517
  case 'dueDate':
518
518
  return currentTab === 'draft'
519
519
  ? transaction.dueDate
520
- : transaction.billPayInfo.paymentDate;
520
+ : getActualPaymentDate(transaction.billPayInfo);
521
521
  case 'status':
522
522
  return getStatusPerTab(transaction);
523
523
  case 'amount':
@@ -75,9 +75,9 @@ const matchAndReverseAccrualsEpic = (actions$, state$, zeniAPI) => actions$.pipe
75
75
  transaction_sync_token: transactionSyncToken,
76
76
  accrual_ids: accrualIds,
77
77
  };
78
- const openedFromMatchId = state$.value.expenseAutomationActualMatchingViewState.matchModal
79
- .openedFromMatchId;
80
- const matchId = openedFromMatchId ?? `${transactionType}:${transactionIntegrationId}`;
78
+ // Key leftover results and snackbar vendor to the confirmed bill, not the
79
+ // row Match New was opened from (user can pick a different txn).
80
+ const matchId = `${transactionType.toLowerCase()}:${transactionIntegrationId}`;
81
81
  const vendorName = matchVendorName(state$.value, matchId);
82
82
  return zeniAPI
83
83
  .postAndGetJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/accruals/match-and-reverse`, body)
@@ -44,11 +44,10 @@ const prefetchOtherJeSchedulesTabEpic = (actions$, state$) => actions$.pipe((0,
44
44
  month: period.start.month,
45
45
  year: period.start.year,
46
46
  });
47
- const actualMatching = state.expenseAutomationActualMatchingViewState;
48
- const amAlreadyLoading = actualMatching.fetchState === 'In-Progress' ||
49
- actualMatching.refreshStatus.fetchState === 'In-Progress';
50
- if (!amAlreadyLoading &&
51
- actualMatching.matchIdsByPeriod[periodId] == null) {
47
+ // `fetchState` is global across months. Skipping on In-Progress would drop
48
+ // October prefetch while September is still loading; September's success
49
+ // then marks the slice Completed and October looks loaded with no ids.
50
+ if (state.expenseAutomationActualMatchingViewState.matchIdsByPeriod[periodId] == null) {
52
51
  actions.push((0, actualMatchingViewReducer_1.fetchActualMatchingPage)(period, true));
53
52
  }
54
53
  }
@@ -25,7 +25,7 @@ const isPreviousStepDisabled = (selectedTransactionCategorizationTab, allSteps,
25
25
  return allSteps[stepIndex - 1].isDisabled;
26
26
  };
27
27
  function getExpenseAutomationView(state) {
28
- const { expenseAutomationViewState, tenantState, expenseAutomationMissingReceiptsViewState, expenseAutomationJESchedulesViewState, expenseAutomationActualMatchingViewState, expenseAutomationTransactionsViewState, expenseAutomationReconciliationViewState, monthEndCloseChecksState, companyState, } = state;
28
+ const { expenseAutomationViewState, tenantState, expenseAutomationMissingReceiptsViewState, expenseAutomationJESchedulesViewState, expenseAutomationTransactionsViewState, expenseAutomationReconciliationViewState, monthEndCloseChecksState, companyState, } = state;
29
29
  const { loggedInUser, currentTenantId } = tenantState;
30
30
  const currentTenant = (0, tenantSelector_1.getCurrentTenant)(state);
31
31
  const { currentSelectedView, selectedPeriodByTenantId } = expenseAutomationViewState;
@@ -105,10 +105,12 @@ function getExpenseAutomationView(state) {
105
105
  };
106
106
  break;
107
107
  }
108
+ // AM is optional (flag-off / background prefetch) and must not keep the
109
+ // page-level AND-complete fetchState stuck on Not-Started. Tab chrome uses
110
+ // currentSelectedViewFetchState, which already switches to AM when selected.
108
111
  const fetchStateVariables = [
109
112
  expenseAutomationMissingReceiptsViewState,
110
113
  expenseAutomationJESchedulesViewState,
111
- expenseAutomationActualMatchingViewState,
112
114
  expenseAutomationTransactionsView,
113
115
  expenseAutomationReconciliationViewState,
114
116
  ];
@@ -39,7 +39,7 @@ function toActualMatchingTransactionSearchResult(payload, fallbackCurrency) {
39
39
  return {
40
40
  transactionId: payload.transaction_id,
41
41
  transactionIntegrationId: payload.transaction_integration_id,
42
- transactionType: payload.transaction_type,
42
+ transactionType: (payload.transaction_type || '').toLowerCase(),
43
43
  lineId: payload.line_id,
44
44
  vendorId: payload.vendor_id ?? undefined,
45
45
  vendorName: payload.vendor_name ?? '',
@@ -26,6 +26,7 @@ exports.initialAccrualSearchState = {
26
26
  exports.initialMatchModalState = {
27
27
  isOpen: false,
28
28
  openedFromMatchId: undefined,
29
+ didPinPrefillTransactions: false,
29
30
  pinnedTransactionResults: [],
30
31
  transactionSearch: exports.initialTransactionSearchState,
31
32
  accrualSearch: exports.initialAccrualSearchState,
@@ -64,6 +65,7 @@ function resetMatchModalSearches(draft) {
64
65
  };
65
66
  draft.matchModal.lastBatchResults = undefined;
66
67
  draft.matchModal.openedFromMatchId = undefined;
68
+ draft.matchModal.didPinPrefillTransactions = false;
67
69
  draft.matchModal.pinnedTransactionResults = [];
68
70
  }
69
71
  const expenseAutomationActualMatchingView = (0, toolkit_1.createSlice)({
@@ -88,6 +90,9 @@ const expenseAutomationActualMatchingView = (0, toolkit_1.createSlice)({
88
90
  const { refreshViewInBackground } = action.payload;
89
91
  if (refreshViewInBackground) {
90
92
  draft.refreshStatus = { fetchState: 'In-Progress', error: undefined };
93
+ if (draft.fetchState === 'Not-Started') {
94
+ draft.fetchState = 'In-Progress';
95
+ }
91
96
  }
92
97
  else {
93
98
  draft.fetchState = 'In-Progress';
@@ -109,6 +114,10 @@ const expenseAutomationActualMatchingView = (0, toolkit_1.createSlice)({
109
114
  draft.matchIdsByPeriod[periodId] = matchIds;
110
115
  if (refreshViewInBackground) {
111
116
  draft.refreshStatus = { fetchState: 'Completed', error: undefined };
117
+ if (draft.fetchState !== 'Completed') {
118
+ draft.fetchState = 'Completed';
119
+ draft.error = undefined;
120
+ }
112
121
  }
113
122
  else {
114
123
  draft.fetchState = 'Completed';
@@ -119,6 +128,10 @@ const expenseAutomationActualMatchingView = (0, toolkit_1.createSlice)({
119
128
  const { status, refreshViewInBackground } = action.payload;
120
129
  if (refreshViewInBackground) {
121
130
  draft.refreshStatus = { fetchState: 'Error', error: status };
131
+ if (draft.fetchState !== 'Completed') {
132
+ draft.fetchState = 'Error';
133
+ draft.error = status;
134
+ }
122
135
  }
123
136
  else {
124
137
  draft.fetchState = 'Error';
@@ -258,7 +271,8 @@ const expenseAutomationActualMatchingView = (0, toolkit_1.createSlice)({
258
271
  else {
259
272
  draft.matchModal.transactionSearch.results = results;
260
273
  if (draft.matchModal.openedFromMatchId != null &&
261
- draft.matchModal.pinnedTransactionResults.length === 0) {
274
+ !draft.matchModal.didPinPrefillTransactions) {
275
+ draft.matchModal.didPinPrefillTransactions = true;
262
276
  draft.matchModal.pinnedTransactionResults = results;
263
277
  }
264
278
  }
@@ -367,6 +381,7 @@ const expenseAutomationActualMatchingView = (0, toolkit_1.createSlice)({
367
381
  fetchState: 'Completed',
368
382
  error: undefined,
369
383
  };
384
+ delete draft.lastMatchResultsByMatchId[matchId];
370
385
  draft.closeMatchAfterUndo =
371
386
  results.length > 0 &&
372
387
  results.every((result) => result.status === 'undone');
@@ -23,7 +23,7 @@ export declare function isPendingReviewAccrual(accrual: JEOneTimeAccrual): accru
23
23
  export declare function isReversedMatchKind(kind: ActualMatchingRowKind): boolean;
24
24
  export declare function getClearedMatchedAmount(accrual: JEOneTimeAccrual): number | undefined;
25
25
  export declare function getActualMatchingAmount(row: ActualMatchingRow): number;
26
- export declare function getActualMatchingReversalDate(accrual: JEOneTimeAccrual): ZeniDate | undefined;
26
+ export declare function getActualMatchingReversalDate(accrual: JEOneTimeAccrual, matchTransactionDate?: string): ZeniDate | undefined;
27
27
  export declare function buildActualMatchingKpiSummary(rows: ActualMatchingRow[], displayCurrencyCode?: string): ActualMatchingKpiSummary;
28
28
  export interface ExpenseAutomationActualMatchingMatchView {
29
29
  detailFetchState: FetchStateAndError;
@@ -23,6 +23,7 @@ const actualMatchingSelector_1 = require("../../../entity/actualMatching/actualM
23
23
  const jeScheduleHelper_1 = require("../../../entity/jeSchedules/jeScheduleHelper");
24
24
  const jeSchedulesSelector_1 = require("../../../entity/jeSchedules/jeSchedulesSelector");
25
25
  const tenantSelector_1 = require("../../../entity/tenant/tenantSelector");
26
+ const zeniDayJS_1 = require("../../../zeniDayJS");
26
27
  const actualMatchingViewState_1 = require("../types/actualMatchingViewState");
27
28
  function getSelectedMonthYearForCurrentTenant(state) {
28
29
  const tenantId = (0, tenantSelector_1.getCurrentTenant)(state)?.tenantId;
@@ -111,11 +112,18 @@ function getClearedMatchedAmount(accrual) {
111
112
  function getActualMatchingAmount(row) {
112
113
  return row.actualAmount?.amount ?? row.estimatedAmount.amount;
113
114
  }
114
- function getActualMatchingReversalDate(accrual) {
115
- if (isPendingReviewAccrual(accrual)) {
116
- return accrual.matchSuggestion.matchedTransactionDate;
115
+ function parseMatchTransactionDate(transactionDate) {
116
+ if (transactionDate == null || transactionDate === '') {
117
+ return undefined;
117
118
  }
118
- return accrual.cleared?.matchedTransactionDate;
119
+ const parsed = (0, zeniDayJS_1.date)(transactionDate);
120
+ return Number.isFinite(parsed.valueOf()) ? parsed : undefined;
121
+ }
122
+ function getActualMatchingReversalDate(accrual, matchTransactionDate) {
123
+ const matchedDate = isPendingReviewAccrual(accrual)
124
+ ? accrual.matchSuggestion.matchedTransactionDate
125
+ : accrual.cleared?.matchedTransactionDate;
126
+ return matchedDate ?? parseMatchTransactionDate(matchTransactionDate);
119
127
  }
120
128
  function collectGroupedMembers(members) {
121
129
  if (members.length <= 1) {
@@ -169,7 +177,7 @@ function toActualMatchingRow(match) {
169
177
  groupedMembers: collectGroupedMembers(match.members),
170
178
  intendedAccrualIds,
171
179
  leftovers: collectLeftoverMembers(match.leftovers),
172
- reversalDate: getActualMatchingReversalDate(first),
180
+ reversalDate: getActualMatchingReversalDate(first, match.transactionDate),
173
181
  vendorName: match.vendorName != null && match.vendorName !== ''
174
182
  ? match.vendorName
175
183
  : first.vendorName,
@@ -193,8 +201,13 @@ function getActualMatchingSortValue(row, sortKey) {
193
201
  switch (sortKey) {
194
202
  case 'vendor':
195
203
  return (row.vendorName ?? '').toLowerCase();
196
- case 'date':
197
- return row.reversalDate?.valueOf() ?? row.transactionDate;
204
+ case 'date': {
205
+ const millis = row.reversalDate?.valueOf() ??
206
+ (row.transactionDate != null && row.transactionDate !== ''
207
+ ? (0, zeniDayJS_1.date)(row.transactionDate).valueOf()
208
+ : 0);
209
+ return Number.isFinite(millis) ? millis : 0;
210
+ }
198
211
  case 'amount':
199
212
  return getActualMatchingAmount(row);
200
213
  case 'category':
@@ -382,9 +395,16 @@ function getExpenseAutomationActualMatchingView(state) {
382
395
  ? { consumedLineIds }
383
396
  : {}),
384
397
  };
398
+ // `fetchState` is a flat slice field that survives a period switch, so it still
399
+ // reads 'Completed' before the newly selected period's ids land. Reporting
400
+ // 'Not-Started' until they do keeps the skeleton up instead of flashing the
401
+ // empty-complete chrome over a month that has not been fetched.
402
+ const fetchStatus = fetchState === 'Completed' && matchIds == null
403
+ ? { fetchState: 'Not-Started', error: undefined }
404
+ : { fetchState, error };
385
405
  return {
386
- fetchState,
387
- error,
406
+ fetchState: fetchStatus.fetchState,
407
+ error: fetchStatus.error,
388
408
  reversed,
389
409
  pendingReview,
390
410
  kpiSummary,
@@ -83,6 +83,7 @@ export interface ActualMatchingAccrualSearchState extends FetchStateAndError {
83
83
  export interface ActualMatchingMatchModalState {
84
84
  accrualSearch: ActualMatchingAccrualSearchState;
85
85
  confirmStatus: FetchStateAndError;
86
+ didPinPrefillTransactions: boolean;
86
87
  isOpen: boolean;
87
88
  pinnedTransactionResults: ActualMatchingTransactionSearchResult[];
88
89
  transactionSearch: ActualMatchingTransactionSearchState;
@@ -37,9 +37,18 @@ const financeStatement = (0, toolkit_1.createSlice)({
37
37
  },
38
38
  },
39
39
  updateFinanceStatementTimeframe(draft, action) {
40
+ // Unrelated UI re-dispatches the *current* timeframe as a side effect (the
41
+ // reports page hangs its table scroll reset off that callback); dropping the
42
+ // anchor there would silently reset the user's period to the latest one.
43
+ if (draft.timeframe === action.payload) {
44
+ return;
45
+ }
40
46
  draft.timeframe = action.payload;
47
+ // A month anchor is meaningless once the timeframe becomes quarter/year, so
48
+ // a real change drops it and returns the width to the default — 12 months
49
+ // must not become 12 quarters. Order is the user's, so it survives.
41
50
  draft.selectedCOABalancesRange = {
42
- numberOfPeriods: draft.selectedCOABalancesRange.numberOfPeriods,
51
+ numberOfPeriods: exports.initialFinanceStatementState.selectedCOABalancesRange.numberOfPeriods,
43
52
  orderBy: draft.selectedCOABalancesRange.orderBy,
44
53
  };
45
54
  },
@@ -81,8 +90,7 @@ const financeStatement = (0, toolkit_1.createSlice)({
81
90
  draft.selectedReportId = action.payload;
82
91
  },
83
92
  updateFinanceStatementAdditionalBalancesSelection(draft, action) {
84
- const { firstMonthOfFY, additionalBalances: additionalBalancesPayload, coaBalances, maxNumOfPeriodsToHighlight, } = action.payload;
85
- const safeCoaBalances = coaBalances != null ? coaBalances : [];
93
+ const { additionalBalances: additionalBalancesPayload, maxNumOfPeriodsToHighlight, } = action.payload;
86
94
  const additionalBalances = additionalBalancesPayload ?? [];
87
95
  let tempAdditionalBalances = [...additionalBalances];
88
96
  if (additionalBalances.includes('this_period_vs_last_period') ||
@@ -98,30 +106,12 @@ const financeStatement = (0, toolkit_1.createSlice)({
98
106
  draft.maxNumOfPeriodsToHighlight = maxNumOfPeriodsToHighlight;
99
107
  draft.isAdditionalBalancesShown =
100
108
  additionalBalances.length != 0 ? true : false;
101
- const { timeframe, selectedCOABalancesRange } = draft;
102
- if (safeCoaBalances.length > 0) {
103
- const thisPeriod = (0, thisPeriodHelpers_1.extractThisPeriod)(action.payload.firstMonthOfFY, timeframe, selectedCOABalancesRange, safeCoaBalances);
104
- if (thisPeriod != null) {
105
- const selectedCoaBalancesRangeWithThisPeriod = {
106
- ...selectedCOABalancesRange,
107
- thisPeriod,
108
- };
109
- const selectionRanges = (0, getSelectedAndHighlightedRanges_1.getSelectedAndHighlightedRangesForThisPeriod)({
110
- firstMonthOfFY,
111
- thisPeriod: thisPeriod,
112
- coaBalances: safeCoaBalances,
113
- timeframe,
114
- maxNumOfPeriodsToHighlight: maxNumOfPeriodsToHighlight,
115
- currentSelection: {
116
- selectedCOABalancesRange: selectedCoaBalancesRangeWithThisPeriod,
117
- },
118
- orderBy: 'ascending_date',
119
- maxNumOfPeriodsToSelect: maxNumOfPeriodsToHighlight,
120
- });
121
- draft.selectedCOABalancesRange =
122
- selectionRanges.selectedCOABalancesRange;
123
- }
124
- }
109
+ // Column metadata only must not touch selectedCOABalancesRange. The
110
+ // reports page fires this on load, on resize and on every fetch-state
111
+ // transition (so also on report switch and on remount after a drill-down)
112
+ // with a viewport-derived period count that knows nothing about the range
113
+ // the user picked. The width default comes from initial state, and from
114
+ // updateFinanceStatementTimeframe on a real timeframe change.
125
115
  },
126
116
  updateDownloadState(draft, action) {
127
117
  draft.downloadState = action.payload;
@@ -440,7 +440,7 @@ const getBillListOnSort = (sortKey, sortOrder, currentTab, transactions) => {
440
440
  }
441
441
  }
442
442
  else {
443
- if (Boolean(transaction.billPayInfo.paymentDate) === true) {
443
+ if (Boolean((0, billListFilterHelpers_1.getActualPaymentDate)(transaction.billPayInfo)) === true) {
444
444
  transactionsWithSortKeyValue.push(transaction);
445
445
  }
446
446
  else {
@@ -525,7 +525,7 @@ const getBillListOnSort = (sortKey, sortOrder, currentTab, transactions) => {
525
525
  case 'dueDate':
526
526
  return currentTab === 'draft'
527
527
  ? transaction.dueDate
528
- : transaction.billPayInfo.paymentDate;
528
+ : (0, billListFilterHelpers_1.getActualPaymentDate)(transaction.billPayInfo);
529
529
  case 'status':
530
530
  return getStatusPerTab(transaction);
531
531
  case 'amount':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeniai/client-epic-state",
3
- "version": "5.2.35-beta8MM",
3
+ "version": "5.2.35-beta9MM",
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",