@zeniai/client-epic-state 5.2.40 → 5.2.41-beta2MM
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/entity/actualMatching/actualMatchingPayload.d.ts +31 -0
- package/lib/entity/actualMatching/actualMatchingPayload.js +52 -0
- package/lib/entity/actualMatching/actualMatchingReducer.d.ts +11 -0
- package/lib/entity/actualMatching/actualMatchingReducer.js +42 -0
- package/lib/entity/actualMatching/actualMatchingSelector.d.ts +3 -0
- package/lib/entity/actualMatching/actualMatchingSelector.js +6 -0
- package/lib/entity/actualMatching/actualMatchingState.d.ts +23 -0
- package/lib/entity/actualMatching/actualMatchingState.js +2 -0
- package/lib/entity/actualMatching/actualMatchingTypes.d.ts +1 -0
- package/lib/entity/actualMatching/actualMatchingTypes.js +2 -0
- package/lib/entity/file/fileReducer.d.ts +8 -0
- package/lib/entity/file/fileReducer.js +10 -2
- package/lib/entity/invoicing/invoicingCommonPayload.d.ts +14 -1
- package/lib/entity/jeSchedules/jeSchedulesPayload.d.ts +31 -0
- package/lib/entity/jeSchedules/jeSchedulesPayload.js +53 -0
- package/lib/entity/jeSchedules/jeSchedulesState.d.ts +33 -0
- package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
- package/lib/entity/snackbar/snackbarTypes.js +3 -0
- package/lib/epic.d.ts +9 -1
- package/lib/epic.js +16 -0
- package/lib/esm/entity/actualMatching/actualMatchingPayload.js +48 -0
- package/lib/esm/entity/actualMatching/actualMatchingReducer.js +38 -0
- package/lib/esm/entity/actualMatching/actualMatchingSelector.js +3 -0
- package/lib/esm/entity/actualMatching/actualMatchingState.js +1 -0
- package/lib/esm/entity/actualMatching/actualMatchingTypes.js +1 -0
- package/lib/esm/entity/file/fileReducer.js +10 -2
- package/lib/esm/entity/jeSchedules/jeSchedulesPayload.js +53 -0
- package/lib/esm/entity/snackbar/snackbarTypes.js +3 -0
- package/lib/esm/epic.js +16 -0
- package/lib/esm/index.js +7 -3
- package/lib/esm/reducer.js +6 -0
- package/lib/esm/view/expenseAutomationView/epics/common/fetchAllExpenseAutomationTabsEpic.js +25 -12
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/fetchActualMatchingEpic.js +46 -0
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/fetchActualMatchingMatchEpic.js +36 -0
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/fetchActualMatchingPageEpic.js +7 -0
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/ignoreActualMatchingSuggestionEpic.js +49 -0
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/matchAndReverseAccrualsEpic.js +108 -0
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/refreshActualMatchingList.js +16 -0
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/searchMatchAccrualsEpic.js +60 -0
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/searchMatchTransactionsEpic.js +74 -0
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/undoReversalAccrualsEpic.js +78 -0
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulePageEpic.js +13 -3
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/prefetchOtherJeSchedulesTabEpic.js +40 -15
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/switchJESchedulesTabEpic.js +19 -8
- package/lib/esm/view/expenseAutomationView/expenseAutomationViewReducer.js +2 -1
- package/lib/esm/view/expenseAutomationView/expenseAutomationViewSelector.js +24 -6
- package/lib/esm/view/expenseAutomationView/expenseAutomationViewState.js +4 -2
- package/lib/esm/view/expenseAutomationView/payload/actualMatchingPayload.js +68 -0
- package/lib/esm/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +497 -0
- package/lib/esm/view/expenseAutomationView/reducers/jeSchedulesViewReducer.js +2 -1
- package/lib/esm/view/expenseAutomationView/selectorTypes/actualMatchingViewSelectorTypes.js +1 -0
- package/lib/esm/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +432 -0
- package/lib/esm/view/expenseAutomationView/selectors/jeSchedulesViewSelector.js +4 -0
- package/lib/esm/view/expenseAutomationView/types/actualMatchingViewState.js +47 -0
- package/lib/esm/view/expenseAutomationView/types/jeSchedulesViewState.js +8 -1
- package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewSelector.js +1 -1
- package/lib/esm/view/invoicing/settingsView/epics/fetchInvoicingSettingsEpic.js +6 -0
- package/lib/esm/view/invoicing/settingsView/epics/saveInvoicingSettingsEpic.js +7 -0
- package/lib/esm/view/invoicing/settingsView/invoicingBrandingFormConfig.js +1 -1
- package/lib/esm/view/invoicing/settingsView/settingsViewSelector.js +5 -0
- package/lib/esm/view/invoicing/settingsView/submitInvoicingBrandingFormEpic.js +9 -0
- package/lib/index.d.ts +14 -5
- package/lib/index.js +87 -48
- package/lib/reducer.d.ts +2 -0
- package/lib/reducer.js +6 -0
- package/lib/rootStateTypes.d.ts +4 -0
- package/lib/view/expenseAutomationView/epics/common/fetchAllExpenseAutomationTabsEpic.js +25 -12
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/fetchActualMatchingEpic.d.ts +9 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/fetchActualMatchingEpic.js +50 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/fetchActualMatchingMatchEpic.d.ts +12 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/fetchActualMatchingMatchEpic.js +40 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/fetchActualMatchingPageEpic.d.ts +5 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/fetchActualMatchingPageEpic.js +11 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/ignoreActualMatchingSuggestionEpic.d.ts +10 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/ignoreActualMatchingSuggestionEpic.js +53 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/matchAndReverseAccrualsEpic.d.ts +9 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/matchAndReverseAccrualsEpic.js +112 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/refreshActualMatchingList.d.ts +3 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/refreshActualMatchingList.js +19 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/searchMatchAccrualsEpic.d.ts +9 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/searchMatchAccrualsEpic.js +64 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/searchMatchTransactionsEpic.d.ts +8 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/searchMatchTransactionsEpic.js +78 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/undoReversalAccrualsEpic.d.ts +9 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/undoReversalAccrualsEpic.js +82 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulePageEpic.d.ts +2 -1
- package/lib/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulePageEpic.js +13 -3
- package/lib/view/expenseAutomationView/epics/jeSchedule/prefetchOtherJeSchedulesTabEpic.d.ts +5 -1
- package/lib/view/expenseAutomationView/epics/jeSchedule/prefetchOtherJeSchedulesTabEpic.js +39 -14
- package/lib/view/expenseAutomationView/epics/jeSchedule/switchJESchedulesTabEpic.js +18 -7
- package/lib/view/expenseAutomationView/expenseAutomationViewReducer.d.ts +2 -1
- package/lib/view/expenseAutomationView/expenseAutomationViewReducer.js +2 -1
- package/lib/view/expenseAutomationView/expenseAutomationViewSelector.js +24 -6
- package/lib/view/expenseAutomationView/expenseAutomationViewState.d.ts +2 -1
- package/lib/view/expenseAutomationView/expenseAutomationViewState.js +6 -3
- package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
- package/lib/view/expenseAutomationView/payload/actualMatchingPayload.d.ts +91 -0
- package/lib/view/expenseAutomationView/payload/actualMatchingPayload.js +74 -0
- package/lib/view/expenseAutomationView/reducers/actualMatchingViewReducer.d.ts +88 -0
- package/lib/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +501 -0
- package/lib/view/expenseAutomationView/reducers/jeSchedulesViewReducer.d.ts +6 -5
- package/lib/view/expenseAutomationView/reducers/jeSchedulesViewReducer.js +2 -1
- package/lib/view/expenseAutomationView/selectorTypes/actualMatchingViewSelectorTypes.d.ts +88 -0
- package/lib/view/expenseAutomationView/selectorTypes/actualMatchingViewSelectorTypes.js +2 -0
- package/lib/view/expenseAutomationView/selectorTypes/expenseAutomationViewSelectorTypes.d.ts +11 -0
- package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.d.ts +40 -0
- package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +452 -0
- package/lib/view/expenseAutomationView/selectors/jeSchedulesViewSelector.js +4 -0
- package/lib/view/expenseAutomationView/types/actualMatchingViewState.d.ts +116 -0
- package/lib/view/expenseAutomationView/types/actualMatchingViewState.js +56 -0
- package/lib/view/expenseAutomationView/types/jeSchedulesViewState.d.ts +1 -1
- package/lib/view/expenseAutomationView/types/jeSchedulesViewState.js +8 -1
- package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewSelector.js +1 -1
- package/lib/view/invoicing/settingsView/epics/fetchInvoicingSettingsEpic.js +6 -0
- package/lib/view/invoicing/settingsView/epics/saveInvoicingSettingsEpic.js +7 -0
- package/lib/view/invoicing/settingsView/epics/settingsViewActionType.d.ts +2 -1
- package/lib/view/invoicing/settingsView/invoicingBrandingFormConfig.js +1 -1
- package/lib/view/invoicing/settingsView/settingsViewSelector.d.ts +8 -0
- package/lib/view/invoicing/settingsView/settingsViewSelector.js +5 -0
- package/lib/view/invoicing/settingsView/submitInvoicingBrandingFormEpic.d.ts +2 -1
- package/lib/view/invoicing/settingsView/submitInvoicingBrandingFormEpic.js +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import orderBy from 'lodash/orderBy';
|
|
2
|
+
import { toMonthYearPeriodId, } from '../../../commonStateTypes/timePeriod';
|
|
3
|
+
import { getActualMatchingMatchById } from '../../../entity/actualMatching/actualMatchingSelector';
|
|
4
|
+
import { generateJEOneTimeAccrualKey } from '../../../entity/jeSchedules/jeScheduleHelper';
|
|
5
|
+
import { getJEOneTimeAccrualByKey, } from '../../../entity/jeSchedules/jeSchedulesSelector';
|
|
6
|
+
import { getCurrentTenant } from '../../../entity/tenant/tenantSelector';
|
|
7
|
+
import { date } from '../../../zeniDayJS';
|
|
8
|
+
import { actualMatchingTransactionLineKey, } from '../types/actualMatchingViewState';
|
|
9
|
+
export function getSelectedMonthYearForCurrentTenant(state) {
|
|
10
|
+
const tenantId = getCurrentTenant(state)?.tenantId;
|
|
11
|
+
if (tenantId == null) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
return state.expenseAutomationViewState.selectedPeriodByTenantId[tenantId];
|
|
15
|
+
}
|
|
16
|
+
export function getTenantDisplayCurrency(state) {
|
|
17
|
+
const locale = getCurrentTenant(state)?.company?.companyLocaleInfo;
|
|
18
|
+
if (locale?.currencyCode == null || locale.currencyCode === '') {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
currencyCode: locale.currencyCode,
|
|
23
|
+
currencySymbol: locale.currencySymbol != null && locale.currencySymbol !== ''
|
|
24
|
+
? locale.currencySymbol
|
|
25
|
+
: locale.currencyCode,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function toMatchModalAccrual(member) {
|
|
29
|
+
if ('jeOneTimeAccrualKey' in member && member.jeOneTimeAccrualKey != null) {
|
|
30
|
+
return member;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
...member,
|
|
34
|
+
jeOneTimeAccrualKey: generateJEOneTimeAccrualKey(member.accrualId),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function uniqueMatchModalAccruals(pinned, searched) {
|
|
38
|
+
const byId = new Map();
|
|
39
|
+
pinned.forEach((row) => byId.set(row.accrualId, row));
|
|
40
|
+
searched.forEach((row) => {
|
|
41
|
+
if (!byId.has(row.accrualId)) {
|
|
42
|
+
byId.set(row.accrualId, row);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return Array.from(byId.values());
|
|
46
|
+
}
|
|
47
|
+
function uniqueTransactionSearchResults(pinned, searched) {
|
|
48
|
+
const byKey = new Map();
|
|
49
|
+
pinned.forEach((row) => byKey.set(actualMatchingTransactionLineKey(row.transactionType, row.transactionIntegrationId, row.lineId), row));
|
|
50
|
+
searched.forEach((row) => {
|
|
51
|
+
const key = actualMatchingTransactionLineKey(row.transactionType, row.transactionIntegrationId, row.lineId);
|
|
52
|
+
if (!byKey.has(key)) {
|
|
53
|
+
byKey.set(key, row);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return Array.from(byKey.values());
|
|
57
|
+
}
|
|
58
|
+
export function parseActualMatchingMatchId(matchId) {
|
|
59
|
+
const separator = matchId.indexOf(':');
|
|
60
|
+
if (separator <= 0 || separator === matchId.length - 1) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
transactionType: matchId.slice(0, separator).toLowerCase(),
|
|
65
|
+
transactionIntegrationId: matchId.slice(separator + 1),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const NOT_STARTED = {
|
|
69
|
+
fetchState: 'Not-Started',
|
|
70
|
+
error: undefined,
|
|
71
|
+
};
|
|
72
|
+
export function isAutoReversedAccrual(accrual) {
|
|
73
|
+
return (accrual.accrualStatus === 'reversed' &&
|
|
74
|
+
accrual.reversalTrigger === 'bill_match');
|
|
75
|
+
}
|
|
76
|
+
export function isManuallyReversedAccrual(accrual) {
|
|
77
|
+
return (accrual.accrualStatus === 'reversed' &&
|
|
78
|
+
accrual.reversalTrigger === 'manual_reverse');
|
|
79
|
+
}
|
|
80
|
+
export function isPendingReviewAccrual(accrual) {
|
|
81
|
+
if (accrual.accrualStatus !== 'posted' || accrual.matchSuggestion == null) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const resolution = accrual.matchSuggestion.resolution;
|
|
85
|
+
return resolution == null || resolution.length === 0;
|
|
86
|
+
}
|
|
87
|
+
export function isReversedMatchKind(kind) {
|
|
88
|
+
return kind === 'auto_reversed' || kind === 'manually_reversed';
|
|
89
|
+
}
|
|
90
|
+
export function getClearedMatchedAmount(accrual) {
|
|
91
|
+
return accrual.cleared?.matchedAmount?.amount;
|
|
92
|
+
}
|
|
93
|
+
export function getActualMatchingAmount(row) {
|
|
94
|
+
return row.actualAmount?.amount ?? row.estimatedAmount.amount;
|
|
95
|
+
}
|
|
96
|
+
function parseMatchTransactionDate(transactionDate) {
|
|
97
|
+
if (transactionDate == null || transactionDate === '') {
|
|
98
|
+
return undefined;
|
|
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);
|
|
108
|
+
}
|
|
109
|
+
function collectGroupedMembers(members) {
|
|
110
|
+
if (members.length <= 1) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
return members.map((member) => ({
|
|
114
|
+
accrualId: member.accrualId,
|
|
115
|
+
estimatedAmount: member.estimatedAmount,
|
|
116
|
+
jeDebitAccountName: member.jeDebitAccountName,
|
|
117
|
+
memo: member.memo,
|
|
118
|
+
serviceMonth: member.serviceMonth,
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
function collectLeftoverMembers(leftovers) {
|
|
122
|
+
if (leftovers == null || leftovers.length === 0) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
return leftovers.map((member) => ({
|
|
126
|
+
accrualId: member.accrualId,
|
|
127
|
+
estimatedAmount: member.estimatedAmount,
|
|
128
|
+
jeDebitAccountName: member.jeDebitAccountName,
|
|
129
|
+
memo: member.memo,
|
|
130
|
+
serviceMonth: member.serviceMonth,
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
function toActualMatchingRow(match) {
|
|
134
|
+
const firstMember = match.members[0];
|
|
135
|
+
if (firstMember == null) {
|
|
136
|
+
throw new Error('Actual matching row requires at least one member');
|
|
137
|
+
}
|
|
138
|
+
const first = toMatchModalAccrual(firstMember);
|
|
139
|
+
const leftoverMembers = match.leftovers ?? [];
|
|
140
|
+
const fromCleared = firstMember.cleared?.intendedAccrualIds;
|
|
141
|
+
const intendedAccrualIds = fromCleared != null && fromCleared.length > 0
|
|
142
|
+
? fromCleared
|
|
143
|
+
: [
|
|
144
|
+
...match.members.map((member) => member.accrualId),
|
|
145
|
+
...leftoverMembers.map((leftover) => leftover.accrualId),
|
|
146
|
+
];
|
|
147
|
+
return {
|
|
148
|
+
...first,
|
|
149
|
+
matchId: match.matchId,
|
|
150
|
+
kind: match.kind,
|
|
151
|
+
accruedAmount: match.accruedAmount,
|
|
152
|
+
actualAmount: match.actualAmount,
|
|
153
|
+
matchTier: match.matchTier,
|
|
154
|
+
matchShape: match.matchShape,
|
|
155
|
+
transactionType: match.transactionType,
|
|
156
|
+
transactionDate: match.transactionDate,
|
|
157
|
+
groupedAccrualIds: match.members.map((member) => member.accrualId),
|
|
158
|
+
groupedMembers: collectGroupedMembers(match.members),
|
|
159
|
+
intendedAccrualIds,
|
|
160
|
+
leftovers: collectLeftoverMembers(match.leftovers),
|
|
161
|
+
reversalDate: getActualMatchingReversalDate(first, match.transactionDate),
|
|
162
|
+
vendorName: match.vendorName != null && match.vendorName !== ''
|
|
163
|
+
? match.vendorName
|
|
164
|
+
: first.vendorName,
|
|
165
|
+
estimatedAmount: match.accruedAmount,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function matchesSearch(row, searchString) {
|
|
169
|
+
const trimmed = searchString.trim().toLowerCase();
|
|
170
|
+
if (trimmed.length === 0) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
if ((row.vendorName ?? '').toLowerCase().includes(trimmed)) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
if ((row.memo ?? '').toLowerCase().includes(trimmed)) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
return (row.groupedMembers ?? []).some((member) => (member.memo ?? '').toLowerCase().includes(trimmed));
|
|
180
|
+
}
|
|
181
|
+
function getActualMatchingSortValue(row, sortKey) {
|
|
182
|
+
switch (sortKey) {
|
|
183
|
+
case 'vendor':
|
|
184
|
+
return (row.vendorName ?? '').toLowerCase();
|
|
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
|
+
}
|
|
192
|
+
case 'amount':
|
|
193
|
+
return getActualMatchingAmount(row);
|
|
194
|
+
case 'category':
|
|
195
|
+
return row.jeDebitAccountName?.toLowerCase();
|
|
196
|
+
case 'class':
|
|
197
|
+
return row.jeDebitAccountingClassName?.toLowerCase();
|
|
198
|
+
case 'type':
|
|
199
|
+
return row.transactionType?.toLowerCase();
|
|
200
|
+
case 'lifecycle':
|
|
201
|
+
return row.kind;
|
|
202
|
+
case 'service':
|
|
203
|
+
return row.serviceMonth;
|
|
204
|
+
case 'accrued':
|
|
205
|
+
return row.accruedAmount?.amount ?? row.estimatedAmount.amount;
|
|
206
|
+
case 'actual':
|
|
207
|
+
return row.actualAmount?.amount;
|
|
208
|
+
case 'variance': {
|
|
209
|
+
const accrued = row.accruedAmount?.amount ?? row.estimatedAmount.amount;
|
|
210
|
+
const actual = row.actualAmount?.amount;
|
|
211
|
+
if (actual == null || accrued === 0) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
return ((actual - accrued) / Math.abs(accrued)) * 100;
|
|
215
|
+
}
|
|
216
|
+
default:
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function sortActualMatchingRows(rows, uiState) {
|
|
221
|
+
const lodashOrder = uiState.sortOrder === 'ascending' ? 'asc' : 'desc';
|
|
222
|
+
return orderBy(rows, [(row) => getActualMatchingSortValue(row, uiState.sortKey)], [lodashOrder]);
|
|
223
|
+
}
|
|
224
|
+
const ESTIMATED_MINUTES_PER_PENDING_MATCH = 4;
|
|
225
|
+
function emptyKpiSummary() {
|
|
226
|
+
return {
|
|
227
|
+
actualsReceived: { count: 0, expectedCount: 0, vendorsPending: 0 },
|
|
228
|
+
autoReversed: { count: 0, percent: 0, clearedAmount: 0 },
|
|
229
|
+
pendingReview: { count: 0, amount: 0, estimatedMinutesToClear: 0 },
|
|
230
|
+
variance: { percent: 0, averagePercentPerMatch: 0, netAmount: 0 },
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function sumRowAmounts(rows, displayCurrencyCode) {
|
|
234
|
+
return rows.reduce((sum, row) => {
|
|
235
|
+
if (displayCurrencyCode != null &&
|
|
236
|
+
row.accruedAmount.currencyCode !== displayCurrencyCode) {
|
|
237
|
+
return sum;
|
|
238
|
+
}
|
|
239
|
+
return sum + getActualMatchingAmount(row);
|
|
240
|
+
}, 0);
|
|
241
|
+
}
|
|
242
|
+
function isKnownKpiKind(kind) {
|
|
243
|
+
return (kind === 'auto_reversed' ||
|
|
244
|
+
kind === 'pending_review' ||
|
|
245
|
+
kind === 'manually_reversed');
|
|
246
|
+
}
|
|
247
|
+
function rowMatchesDisplayCurrency(row, displayCurrencyCode) {
|
|
248
|
+
if (displayCurrencyCode == null) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
const rowCurrency = row.accruedAmount?.currencyCode ?? row.estimatedAmount.currencyCode;
|
|
252
|
+
return rowCurrency === displayCurrencyCode;
|
|
253
|
+
}
|
|
254
|
+
function uniqueVendorCount(rows) {
|
|
255
|
+
const ids = new Set();
|
|
256
|
+
rows.forEach((row) => {
|
|
257
|
+
if (row.vendorId != null && row.vendorId !== '') {
|
|
258
|
+
ids.add(row.vendorId);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (row.vendorName != null && row.vendorName !== '') {
|
|
262
|
+
ids.add(row.vendorName);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
return ids.size;
|
|
266
|
+
}
|
|
267
|
+
function rowVariancePercent(row) {
|
|
268
|
+
const accrued = row.accruedAmount?.amount ?? row.estimatedAmount.amount;
|
|
269
|
+
const actual = row.actualAmount?.amount;
|
|
270
|
+
if (actual == null || accrued === 0) {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
return ((actual - accrued) / Math.abs(accrued)) * 100;
|
|
274
|
+
}
|
|
275
|
+
export function buildActualMatchingKpiSummary(rows, displayCurrencyCode) {
|
|
276
|
+
// Strip counts and amounts share this set: known kinds only, and (when a
|
|
277
|
+
// display currency is set) that currency only. Lists still render every row.
|
|
278
|
+
const kpiRows = rows.filter((row) => isKnownKpiKind(row.kind) &&
|
|
279
|
+
rowMatchesDisplayCurrency(row, displayCurrencyCode));
|
|
280
|
+
if (kpiRows.length === 0) {
|
|
281
|
+
return emptyKpiSummary();
|
|
282
|
+
}
|
|
283
|
+
const autoReversed = kpiRows.filter((row) => row.kind === 'auto_reversed');
|
|
284
|
+
const pendingReview = kpiRows.filter((row) => row.kind === 'pending_review');
|
|
285
|
+
const receivedCount = kpiRows.length;
|
|
286
|
+
const rowsWithActual = kpiRows.filter((row) => row.actualAmount?.amount != null);
|
|
287
|
+
const accruedTotal = rowsWithActual.reduce((sum, row) => sum + (row.accruedAmount?.amount ?? row.estimatedAmount.amount), 0);
|
|
288
|
+
const actualTotal = rowsWithActual.reduce((sum, row) => sum + (row.actualAmount?.amount ?? 0), 0);
|
|
289
|
+
const variancePercents = kpiRows
|
|
290
|
+
.map(rowVariancePercent)
|
|
291
|
+
.filter((percent) => percent != null);
|
|
292
|
+
return {
|
|
293
|
+
actualsReceived: {
|
|
294
|
+
count: receivedCount,
|
|
295
|
+
// List API does not return still-expected accruals; hide "of N expected"
|
|
296
|
+
// in the strip until expectedCount is greater than received.
|
|
297
|
+
expectedCount: receivedCount,
|
|
298
|
+
vendorsPending: uniqueVendorCount(pendingReview),
|
|
299
|
+
},
|
|
300
|
+
autoReversed: {
|
|
301
|
+
count: autoReversed.length,
|
|
302
|
+
percent: Math.round((autoReversed.length / receivedCount) * 100),
|
|
303
|
+
clearedAmount: sumRowAmounts(autoReversed, displayCurrencyCode),
|
|
304
|
+
},
|
|
305
|
+
pendingReview: {
|
|
306
|
+
count: pendingReview.length,
|
|
307
|
+
amount: sumRowAmounts(pendingReview, displayCurrencyCode),
|
|
308
|
+
estimatedMinutesToClear: pendingReview.length * ESTIMATED_MINUTES_PER_PENDING_MATCH,
|
|
309
|
+
},
|
|
310
|
+
variance: {
|
|
311
|
+
percent: accruedTotal === 0
|
|
312
|
+
? 0
|
|
313
|
+
: ((actualTotal - accruedTotal) / Math.abs(accruedTotal)) * 100,
|
|
314
|
+
averagePercentPerMatch: variancePercents.length === 0
|
|
315
|
+
? 0
|
|
316
|
+
: variancePercents.reduce((sum, percent) => sum + percent, 0) /
|
|
317
|
+
variancePercents.length,
|
|
318
|
+
netAmount: actualTotal - accruedTotal,
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
export function getExpenseAutomationActualMatchingMatchView(state, matchId) {
|
|
323
|
+
const view = state.expenseAutomationActualMatchingViewState;
|
|
324
|
+
return {
|
|
325
|
+
detailFetchState: view.detailFetchStateByMatchId[matchId] ?? NOT_STARTED,
|
|
326
|
+
ignoreStatus: view.ignoreStatusByMatchId[matchId] ?? NOT_STARTED,
|
|
327
|
+
lastMatchResults: view.lastMatchResultsByMatchId?.[matchId],
|
|
328
|
+
lastUndoResults: view.lastUndoResultsByMatchId?.[matchId],
|
|
329
|
+
match: getActualMatchingMatchById(state.actualMatchingState, matchId),
|
|
330
|
+
refreshStatus: view.detailRefreshStatusByMatchId[matchId] ?? NOT_STARTED,
|
|
331
|
+
undoReversalStatus: view.undoReversalStatusByMatchId[matchId] ?? NOT_STARTED,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
export function getCloseMatchAfterUndo(state) {
|
|
335
|
+
return state.expenseAutomationActualMatchingViewState.closeMatchAfterUndo;
|
|
336
|
+
}
|
|
337
|
+
export function getCloseMatchAfterIgnoreMatchId(state) {
|
|
338
|
+
return state.expenseAutomationActualMatchingViewState
|
|
339
|
+
.closeMatchAfterIgnoreMatchId;
|
|
340
|
+
}
|
|
341
|
+
export function getExpenseAutomationActualMatchingView(state) {
|
|
342
|
+
const { expenseAutomationActualMatchingViewState } = state;
|
|
343
|
+
const { matchIdsByPeriod, fetchState, error, refreshStatus, uiState, matchModal, ignoreStatusByMatchId, } = expenseAutomationActualMatchingViewState;
|
|
344
|
+
const selectedPeriod = getSelectedMonthYearForCurrentTenant(state);
|
|
345
|
+
const monthYearPeriodId = selectedPeriod != null ? toMonthYearPeriodId(selectedPeriod) : undefined;
|
|
346
|
+
const matchIds = monthYearPeriodId != null ? matchIdsByPeriod[monthYearPeriodId] : undefined;
|
|
347
|
+
const allRows = (matchIds ?? [])
|
|
348
|
+
.map((matchId) => getActualMatchingMatchById(state.actualMatchingState, matchId))
|
|
349
|
+
.filter((match) => match != null)
|
|
350
|
+
.filter((match) => match.members.length > 0)
|
|
351
|
+
.map(toActualMatchingRow);
|
|
352
|
+
const rows = allRows.filter((row) => matchesSearch(row, uiState.searchString));
|
|
353
|
+
const reversed = sortActualMatchingRows(rows.filter((row) => isReversedMatchKind(row.kind)), uiState);
|
|
354
|
+
const pendingReview = sortActualMatchingRows(rows.filter((row) => row.kind === 'pending_review'), uiState);
|
|
355
|
+
const kpiSummary = buildActualMatchingKpiSummary(allRows, getTenantDisplayCurrency(state)?.currencyCode);
|
|
356
|
+
const searchedAccruals = matchModal.accrualSearch.results
|
|
357
|
+
.map((key) => getJEOneTimeAccrualByKey(key, state.jeSchedulesState))
|
|
358
|
+
.filter((accrual) => accrual != null);
|
|
359
|
+
const openedMatch = matchModal.openedFromMatchId != null
|
|
360
|
+
? getActualMatchingMatchById(state.actualMatchingState, matchModal.openedFromMatchId)
|
|
361
|
+
: undefined;
|
|
362
|
+
const pinnedAccruals = [
|
|
363
|
+
...(openedMatch?.members ?? []),
|
|
364
|
+
...(openedMatch?.leftovers ?? []),
|
|
365
|
+
].map(toMatchModalAccrual);
|
|
366
|
+
const firstMember = openedMatch?.members[0];
|
|
367
|
+
const cleared = firstMember?.cleared;
|
|
368
|
+
const suggestion = firstMember?.matchSuggestion;
|
|
369
|
+
const stamp = cleared ?? suggestion;
|
|
370
|
+
const consumedLineIds = cleared?.consumedLineIds;
|
|
371
|
+
const prefill = openedMatch == null
|
|
372
|
+
? undefined
|
|
373
|
+
: {
|
|
374
|
+
selectedAccrualIds: [
|
|
375
|
+
...openedMatch.members.map((member) => member.accrualId),
|
|
376
|
+
...(openedMatch.leftovers ?? []).map((leftover) => leftover.accrualId),
|
|
377
|
+
],
|
|
378
|
+
actualAmount: openedMatch.actualAmount.amount,
|
|
379
|
+
matchedTransactionId: stamp?.matchedTransactionId,
|
|
380
|
+
matchedLineId: cleared == null ? suggestion?.matchedLineId : undefined,
|
|
381
|
+
...(consumedLineIds != null && consumedLineIds.length > 0
|
|
382
|
+
? { consumedLineIds }
|
|
383
|
+
: {}),
|
|
384
|
+
};
|
|
385
|
+
// `fetchState` is a flat slice field that survives a period switch. Until this
|
|
386
|
+
// month's ids land, leftover Completed/Error is the previous month. Report
|
|
387
|
+
// Not-Started so chrome matches Review's "cache slot missing" skeleton, not
|
|
388
|
+
// empty-complete or a stale error. In-Progress is this month's fetch.
|
|
389
|
+
const fetchStatus = matchIds == null &&
|
|
390
|
+
fetchState !== 'In-Progress' &&
|
|
391
|
+
fetchState !== 'Not-Started'
|
|
392
|
+
? { fetchState: 'Not-Started', error: undefined }
|
|
393
|
+
: { fetchState, error };
|
|
394
|
+
return {
|
|
395
|
+
fetchState: fetchStatus.fetchState,
|
|
396
|
+
error: fetchStatus.error,
|
|
397
|
+
reversed,
|
|
398
|
+
pendingReview,
|
|
399
|
+
kpiSummary,
|
|
400
|
+
uiState,
|
|
401
|
+
refreshStatus,
|
|
402
|
+
ignoreStatusByMatchId,
|
|
403
|
+
hasLoadedForSelectedPeriod: matchIds != null,
|
|
404
|
+
matchModal: {
|
|
405
|
+
isOpen: matchModal.isOpen,
|
|
406
|
+
openedFromMatchId: matchModal.openedFromMatchId,
|
|
407
|
+
prefill,
|
|
408
|
+
confirmStatus: matchModal.confirmStatus,
|
|
409
|
+
lastBatchResults: matchModal.lastBatchResults,
|
|
410
|
+
transactionSearch: {
|
|
411
|
+
query: matchModal.transactionSearch.query,
|
|
412
|
+
results: uniqueTransactionSearchResults(matchModal.pinnedTransactionResults, matchModal.transactionSearch.results),
|
|
413
|
+
fetchState: {
|
|
414
|
+
fetchState: matchModal.transactionSearch.fetchState,
|
|
415
|
+
error: matchModal.transactionSearch.error,
|
|
416
|
+
},
|
|
417
|
+
nextPageToken: matchModal.transactionSearch.nextPageToken,
|
|
418
|
+
isLoadingMore: matchModal.transactionSearch.isLoadingMore,
|
|
419
|
+
},
|
|
420
|
+
accrualSearch: {
|
|
421
|
+
query: matchModal.accrualSearch.query,
|
|
422
|
+
results: uniqueMatchModalAccruals(pinnedAccruals, searchedAccruals),
|
|
423
|
+
fetchState: {
|
|
424
|
+
fetchState: matchModal.accrualSearch.fetchState,
|
|
425
|
+
error: matchModal.accrualSearch.error,
|
|
426
|
+
},
|
|
427
|
+
nextPageToken: matchModal.accrualSearch.nextPageToken,
|
|
428
|
+
isLoadingMore: matchModal.accrualSearch.isLoadingMore,
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
}
|
|
@@ -366,6 +366,10 @@ export function getExpenseAutomationJESchedulesView(state) {
|
|
|
366
366
|
// reports undefined rather than 0, so the UI can hide the badge instead of asserting "none".
|
|
367
367
|
const scheduleCountsByTab = {};
|
|
368
368
|
ALL_JE_PAGE_TABS.forEach((pageTab) => {
|
|
369
|
+
// AM's badge is `actualMatching.kpiSummary`, not schedule row keys.
|
|
370
|
+
if (pageTab === 'actual_matching') {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
369
373
|
const periodTabKey = monthYearPeriodId != null
|
|
370
374
|
? getJESchedulePeriodTabKey(pageTab, monthYearPeriodId)
|
|
371
375
|
: null;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { stringToUnion } from '../../../commonStateTypes/stringToUnion';
|
|
2
|
+
const ACTUAL_MATCHING_SORT_KEYS = [
|
|
3
|
+
'vendor',
|
|
4
|
+
'date',
|
|
5
|
+
'amount',
|
|
6
|
+
'category',
|
|
7
|
+
'class',
|
|
8
|
+
'type',
|
|
9
|
+
'lifecycle',
|
|
10
|
+
'service',
|
|
11
|
+
'accrued',
|
|
12
|
+
'actual',
|
|
13
|
+
'variance',
|
|
14
|
+
];
|
|
15
|
+
export const toActualMatchingSortKey = (v) => stringToUnion(v, ACTUAL_MATCHING_SORT_KEYS);
|
|
16
|
+
export const MATCH_TRANSACTION_SEARCH_PAGE_SIZE = 50;
|
|
17
|
+
export const MATCH_ACCRUAL_SEARCH_LIMIT = 50;
|
|
18
|
+
export const MATCH_SEARCH_DEBOUNCE_MS = 300;
|
|
19
|
+
const MATCH_AND_REVERSE_RESULT_STATUSES = [
|
|
20
|
+
'reversed',
|
|
21
|
+
'noop',
|
|
22
|
+
'conflict',
|
|
23
|
+
'claim_lost',
|
|
24
|
+
'error',
|
|
25
|
+
];
|
|
26
|
+
export const toMatchAndReverseResultStatus = (v) => stringToUnion(v, MATCH_AND_REVERSE_RESULT_STATUSES);
|
|
27
|
+
export function isMatchAndReverseCompleteStatus(status) {
|
|
28
|
+
return status === 'reversed' || status === 'noop';
|
|
29
|
+
}
|
|
30
|
+
const UNDO_REVERSAL_RESULT_STATUSES = [
|
|
31
|
+
'undone',
|
|
32
|
+
'claim_lost',
|
|
33
|
+
'error',
|
|
34
|
+
];
|
|
35
|
+
export const toUndoReversalResultStatus = (v) => stringToUnion(v, UNDO_REVERSAL_RESULT_STATUSES);
|
|
36
|
+
const ACTUAL_MATCHING_IGNORE_REASONS = [
|
|
37
|
+
'wrong_bill',
|
|
38
|
+
'wrong_period',
|
|
39
|
+
'amount_off',
|
|
40
|
+
'already_handled',
|
|
41
|
+
'other_ignore',
|
|
42
|
+
];
|
|
43
|
+
export const toActualMatchingIgnoreReasonType = (v) => stringToUnion(v, ACTUAL_MATCHING_IGNORE_REASONS);
|
|
44
|
+
export const ALL_ACTUAL_MATCHING_IGNORE_REASONS = ACTUAL_MATCHING_IGNORE_REASONS;
|
|
45
|
+
export function actualMatchingTransactionLineKey(transactionType, transactionIntegrationId, lineId) {
|
|
46
|
+
return `${transactionType}:${transactionIntegrationId}:${lineId}`;
|
|
47
|
+
}
|
|
@@ -16,7 +16,14 @@ const JE_SCHEDULE_SORT_KEYS = [
|
|
|
16
16
|
'memo',
|
|
17
17
|
];
|
|
18
18
|
export const toJEScheduleSortKey = (v) => stringToUnion(v, JE_SCHEDULE_SORT_KEYS);
|
|
19
|
-
|
|
19
|
+
// `actual_matching` is chrome only: the JE tab bar includes it, but AM data is
|
|
20
|
+
// `expenseAutomationActualMatchingViewState`, not the schedules period-tab
|
|
21
|
+
// cache below. Walks of ALL_JE_PAGE_TABS that read schedule keys must skip it.
|
|
22
|
+
const JE_PAGE_TABS = [
|
|
23
|
+
'schedules',
|
|
24
|
+
'actual_matching',
|
|
25
|
+
'existing_schedules',
|
|
26
|
+
];
|
|
20
27
|
export const toJEPageTab = (v) => stringToUnion(v, JE_PAGE_TABS);
|
|
21
28
|
export const ALL_JE_PAGE_TABS = JE_PAGE_TABS;
|
|
22
29
|
export const getJESchedulePeriodTabKey = (selectedPageTab, monthYearPeriodId) => `${selectedPageTab}_${monthYearPeriodId}`;
|
|
@@ -36,7 +36,7 @@ export const getInvoicingCustomerRecord = (state, invoicingCustomerID) => {
|
|
|
36
36
|
brandingAccentColor: settings?.accent_color,
|
|
37
37
|
brandingCompanyName: settings?.company_name,
|
|
38
38
|
brandingEmail: settings?.email,
|
|
39
|
-
brandingLogoUrl: settings?.
|
|
39
|
+
brandingLogoUrl: settings?.logo_file?.signed_url,
|
|
40
40
|
stripeConnected: isStripeConnectedFromSettings(settings),
|
|
41
41
|
fetchState: invoicingCustomerDetail?.fetchState ?? 'Not-Started',
|
|
42
42
|
error: invoicingCustomerDetail?.error,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { of } from 'rxjs';
|
|
2
2
|
import { catchError, filter, switchMap } from 'rxjs/operators';
|
|
3
3
|
import { updateAddresses } from '../../../../entity/address/addressReducer';
|
|
4
|
+
import { updateFiles } from '../../../../entity/file/fileReducer';
|
|
4
5
|
import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
|
|
5
6
|
import { fetchInvoicingSettings, updateInvoicingSettings, updateInvoicingSettingsFailure, } from '../settingsViewReducer';
|
|
6
7
|
export const fetchInvoicingSettingsEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchInvoicingSettings.match), switchMap(() => {
|
|
@@ -8,8 +9,13 @@ export const fetchInvoicingSettingsEpic = (actions$, _state$, zeniAPI) => action
|
|
|
8
9
|
return zeniAPI.getJSON(apiUrl).pipe(switchMap((response) => {
|
|
9
10
|
if (isSuccessResponse(response) && response.data != null) {
|
|
10
11
|
const address = response.data.address;
|
|
12
|
+
// The logo record goes to the file entity, like the address goes to
|
|
13
|
+
// the address entity: the settings view keeps only its id and reads
|
|
14
|
+
// the file back through the file selector, so one copy is canonical.
|
|
15
|
+
const logoFile = response.data.logo_file;
|
|
11
16
|
return [
|
|
12
17
|
...(address != null ? [updateAddresses([address])] : []),
|
|
18
|
+
...(logoFile != null ? [updateFiles({ files: [logoFile] })] : []),
|
|
13
19
|
updateInvoicingSettings(response.data),
|
|
14
20
|
];
|
|
15
21
|
}
|
|
@@ -4,6 +4,7 @@ import { apiErrorSnackbarVariables } from '../../../../entity/snackbar/snackbarA
|
|
|
4
4
|
import { openSnackbar } from '../../../../entity/snackbar/snackbarReducer';
|
|
5
5
|
import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
|
|
6
6
|
import { saveInvoicingSettings, saveInvoicingSettingsFailure, saveInvoicingSettingsSuccess, } from '../settingsViewReducer';
|
|
7
|
+
import { updateFiles } from '../../../../entity/file/fileReducer';
|
|
7
8
|
export const saveInvoicingSettingsEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(saveInvoicingSettings.match), mergeMap((action) => {
|
|
8
9
|
const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/settings`;
|
|
9
10
|
const { settings, snackbarSection } = action.payload;
|
|
@@ -22,7 +23,13 @@ export const saveInvoicingSettingsEpic = (actions$, _state$, zeniAPI) => actions
|
|
|
22
23
|
.putAndGetJSON(apiUrl, { ...settings })
|
|
23
24
|
.pipe(switchMap((response) => {
|
|
24
25
|
if (isSuccessResponse(response) && response.data != null) {
|
|
26
|
+
// Keep the file entity in step with what was persisted — the
|
|
27
|
+
// settings view resolves the logo by id, not from the response.
|
|
28
|
+
const savedLogoFile = response.data.logo_file;
|
|
25
29
|
return from([
|
|
30
|
+
...(savedLogoFile != null
|
|
31
|
+
? [updateFiles({ files: [savedLogoFile] })]
|
|
32
|
+
: []),
|
|
26
33
|
saveInvoicingSettingsSuccess(response.data),
|
|
27
34
|
...snackbar('success'),
|
|
28
35
|
]);
|
|
@@ -22,7 +22,7 @@ export const entityToInvoicingBrandingFormLocalData = (settings) => ({
|
|
|
22
22
|
email: settings?.email ?? '',
|
|
23
23
|
invoiceFooterText: settings?.invoice_footer_text ?? '',
|
|
24
24
|
invoicePrefix: settings?.invoice_prefix ?? '',
|
|
25
|
-
logoFileId: settings?.
|
|
25
|
+
logoFileId: settings?.logo_file?.file_id ?? '',
|
|
26
26
|
phone: settings?.phone ?? '',
|
|
27
27
|
taxId: settings?.tax_id ?? '',
|
|
28
28
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getInvoicingQboAccountMappingId, } from '../../../entity/invoicing/invoicingCommonPayload';
|
|
2
|
+
import { getFileByFileId } from '../../../entity/file/fileSelector';
|
|
2
3
|
import { getFormattedAddress } from '../../addressView/addressViewSelector';
|
|
3
4
|
import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
|
|
4
5
|
import { getInvoicingQboView, isInvoicingAccountingPickerDataLoading, } from '../invoicingQboView/invoicingQboViewSelector';
|
|
@@ -25,10 +26,14 @@ export const getInvoicingSettingsFetchState = (state) => ({
|
|
|
25
26
|
*/
|
|
26
27
|
export const getInvoicingSettingsView = (state) => {
|
|
27
28
|
const { error, fetchState, saveFetchState, settings, stripeFetchState } = state.invoicingSettingsViewState;
|
|
29
|
+
const logoFileId = settings?.logo_file?.file_id ?? '';
|
|
28
30
|
return {
|
|
29
31
|
fieldHelp: invoicingFieldHelpForGroup(state, 'settings'),
|
|
30
32
|
error,
|
|
31
33
|
fetchState,
|
|
34
|
+
logoFile: logoFileId !== ''
|
|
35
|
+
? getFileByFileId(state.fileState, logoFileId)
|
|
36
|
+
: undefined,
|
|
32
37
|
saveState: saveFetchState,
|
|
33
38
|
settings,
|
|
34
39
|
stripeConnectState: stripeFetchState,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { of } from 'rxjs';
|
|
2
2
|
import { catchError, filter, mergeMap } from 'rxjs/operators';
|
|
3
3
|
import { updateAddresses } from '../../../entity/address/addressReducer';
|
|
4
|
+
import { updateFiles } from '../../../entity/file/fileReducer';
|
|
4
5
|
import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
|
|
5
6
|
import { resetNewAddressDataInLocalStore } from '../../addressView/addressViewReducer';
|
|
6
7
|
import { INVOICING_BUSINESS_ADDRESS_TYPE, brandingLocalDataToSettingsPayload, entityToInvoicingBrandingFormLocalData, } from './invoicingBrandingFormConfig';
|
|
@@ -23,10 +24,18 @@ export const submitInvoicingBrandingFormEpic = (actions$, state$, zeniAPI) => ac
|
|
|
23
24
|
.pipe(mergeMap((response) => {
|
|
24
25
|
if (isSuccessResponse(response) && response.data != null) {
|
|
25
26
|
const savedAddress = response.data.address;
|
|
27
|
+
// The saved logo goes to the file entity for the same reason the
|
|
28
|
+
// saved address goes to the address entity: the settings view
|
|
29
|
+
// resolves the file by id, so without this a save that changes
|
|
30
|
+
// the logo leaves the view resolving an id nothing holds.
|
|
31
|
+
const savedLogoFile = response.data.logo_file;
|
|
26
32
|
return [
|
|
27
33
|
...(savedAddress != null
|
|
28
34
|
? [updateAddresses([savedAddress])]
|
|
29
35
|
: []),
|
|
36
|
+
...(savedLogoFile != null
|
|
37
|
+
? [updateFiles({ files: [savedLogoFile] })]
|
|
38
|
+
: []),
|
|
30
39
|
saveInvoicingSettingsSuccess(response.data),
|
|
31
40
|
resetNewAddressDataInLocalStore(INVOICING_BUSINESS_ADDRESS_TYPE),
|
|
32
41
|
];
|