@zeniai/client-epic-state 5.2.35-beta7MM → 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.
- package/lib/entity/actualMatching/actualMatchingPayload.js +8 -2
- package/lib/entity/aiCfo/aiCfoReducer.js +78 -4
- package/lib/entity/aiCfo/aiCfoState.d.ts +16 -0
- package/lib/entity/jeSchedules/jeSchedulesPayload.d.ts +1 -0
- package/lib/entity/jeSchedules/jeSchedulesPayload.js +3 -0
- package/lib/entity/jeSchedules/jeSchedulesState.d.ts +1 -0
- package/lib/esm/entity/actualMatching/actualMatchingPayload.js +8 -2
- package/lib/esm/entity/aiCfo/aiCfoReducer.js +79 -5
- package/lib/esm/entity/jeSchedules/jeSchedulesPayload.js +3 -0
- package/lib/esm/view/expenseAutomationView/epics/actualMatching/matchAndReverseAccrualsEpic.js +3 -3
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/prefetchOtherJeSchedulesTabEpic.js +4 -5
- package/lib/esm/view/expenseAutomationView/expenseAutomationViewSelector.js +4 -2
- package/lib/esm/view/expenseAutomationView/payload/actualMatchingPayload.js +1 -1
- package/lib/esm/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +16 -1
- package/lib/esm/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +29 -9
- package/lib/esm/view/financeStatement/financeStatementReducer.js +17 -27
- package/lib/esm/view/spendManagement/billPay/billList/billListSelector.js +3 -3
- package/lib/view/expenseAutomationView/epics/actualMatching/matchAndReverseAccrualsEpic.js +3 -3
- package/lib/view/expenseAutomationView/epics/jeSchedule/prefetchOtherJeSchedulesTabEpic.js +4 -5
- package/lib/view/expenseAutomationView/expenseAutomationViewSelector.js +4 -2
- package/lib/view/expenseAutomationView/payload/actualMatchingPayload.js +1 -1
- package/lib/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +16 -1
- package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.d.ts +1 -1
- package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +29 -9
- package/lib/view/expenseAutomationView/types/actualMatchingViewState.d.ts +1 -0
- package/lib/view/financeStatement/financeStatementReducer.js +17 -27
- package/lib/view/spendManagement/billPay/billList/billListSelector.js +2 -2
- 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(
|
|
38
|
-
leftovers: (payload.leftovers ?? []).map(
|
|
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
|
-
|
|
499
|
-
|
|
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
|
-
|
|
513
|
-
|
|
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
|
}
|
|
@@ -184,6 +184,7 @@ export interface JEOneTimeAccrualPayload {
|
|
|
184
184
|
posting_date?: string | null;
|
|
185
185
|
profile_snapshot?: JEOneTimeAccrualProfileSnapshotPayload | null;
|
|
186
186
|
reversal_policy?: string | null;
|
|
187
|
+
reversal_posting_date?: string | null;
|
|
187
188
|
reversal_transaction_id?: ID | null;
|
|
188
189
|
reversal_trigger?: string | null;
|
|
189
190
|
reversed_by_user?: UserPayload | null;
|
|
@@ -535,6 +535,9 @@ function toJEOneTimeAccrual(payload) {
|
|
|
535
535
|
actualReversalPostDate: payload.actual_reversal_post_date != null
|
|
536
536
|
? (0, zeniDayJS_1.date)(payload.actual_reversal_post_date)
|
|
537
537
|
: undefined,
|
|
538
|
+
reversalPostingDate: payload.reversal_posting_date != null
|
|
539
|
+
? (0, zeniDayJS_1.date)(payload.reversal_posting_date)
|
|
540
|
+
: undefined,
|
|
538
541
|
jeDebitAccountId: payload.je_debit_account_id ?? undefined,
|
|
539
542
|
jeDebitAccountName: payload.je_debit_account_name ?? undefined,
|
|
540
543
|
jeDebitAccountingClassId: payload.je_debit_accounting_class_id ?? undefined,
|
|
@@ -266,6 +266,7 @@ export interface JEOneTimeAccrualState {
|
|
|
266
266
|
matchSuggestion?: JEOneTimeAccrualMatchSuggestion;
|
|
267
267
|
postingDate?: ZeniDate;
|
|
268
268
|
profileSnapshot?: JEOneTimeAccrualProfileSnapshot;
|
|
269
|
+
reversalPostingDate?: ZeniDate;
|
|
269
270
|
reversalTransactionId?: ID;
|
|
270
271
|
reversalTrigger?: OneTimeAccrualReversalTriggerType;
|
|
271
272
|
reversedBy?: JEOneTimeAccrualCreator;
|
|
@@ -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(
|
|
35
|
-
leftovers: (payload.leftovers ?? []).map(
|
|
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
|
-
|
|
493
|
-
|
|
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
|
-
|
|
507
|
-
|
|
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) {
|
|
@@ -524,6 +524,9 @@ export function toJEOneTimeAccrual(payload) {
|
|
|
524
524
|
actualReversalPostDate: payload.actual_reversal_post_date != null
|
|
525
525
|
? date(payload.actual_reversal_post_date)
|
|
526
526
|
: undefined,
|
|
527
|
+
reversalPostingDate: payload.reversal_posting_date != null
|
|
528
|
+
? date(payload.reversal_posting_date)
|
|
529
|
+
: undefined,
|
|
527
530
|
jeDebitAccountId: payload.je_debit_account_id ?? undefined,
|
|
528
531
|
jeDebitAccountName: payload.je_debit_account_name ?? undefined,
|
|
529
532
|
jeDebitAccountingClassId: payload.je_debit_accounting_class_id ?? undefined,
|
package/lib/esm/view/expenseAutomationView/epics/actualMatching/matchAndReverseAccrualsEpic.js
CHANGED
|
@@ -72,9 +72,9 @@ exhaustMap((action) => {
|
|
|
72
72
|
transaction_sync_token: transactionSyncToken,
|
|
73
73
|
accrual_ids: accrualIds,
|
|
74
74
|
};
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const matchId =
|
|
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)
|
package/lib/esm/view/expenseAutomationView/epics/jeSchedule/prefetchOtherJeSchedulesTabEpic.js
CHANGED
|
@@ -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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (
|
|
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,
|
|
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.
|
|
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
|
-
|
|
96
|
-
if (
|
|
97
|
-
return
|
|
96
|
+
function parseMatchTransactionDate(transactionDate) {
|
|
97
|
+
if (transactionDate == null || transactionDate === '') {
|
|
98
|
+
return undefined;
|
|
98
99
|
}
|
|
99
|
-
|
|
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
|
-
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
const matchId =
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (
|
|
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,
|
|
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.
|
|
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
|
|
115
|
-
if (
|
|
116
|
-
return
|
|
115
|
+
function parseMatchTransactionDate(transactionDate) {
|
|
116
|
+
if (transactionDate == null || transactionDate === '') {
|
|
117
|
+
return undefined;
|
|
117
118
|
}
|
|
118
|
-
|
|
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
|
-
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
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
|
|
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-
|
|
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",
|