@zeniai/client-epic-state 4.19.89 → 4.19.90-beta0ND
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/account/accountPayload.d.ts +2 -0
- package/lib/entity/account/accountPayload.js +2 -0
- package/lib/entity/account/accountState.d.ts +2 -0
- package/lib/entity/depositAccount/depositAccount.d.ts +1 -1
- package/lib/entity/depositAccount/depositAccount.js +8 -1
- package/lib/entity/reimbursement/reimbursementPayload.d.ts +2 -0
- package/lib/entity/reimbursement/reimbursementPayload.js +7 -0
- package/lib/entity/reimbursement/reimbursementState.d.ts +2 -0
- package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
- package/lib/entity/snackbar/snackbarTypes.js +1 -0
- package/lib/epic.d.ts +12 -1
- package/lib/epic.js +12 -1
- package/lib/esm/entity/account/accountPayload.js +2 -0
- package/lib/esm/entity/depositAccount/depositAccount.js +8 -1
- package/lib/esm/entity/reimbursement/reimbursementPayload.js +7 -0
- package/lib/esm/entity/snackbar/snackbarTypes.js +1 -0
- package/lib/esm/epic.js +12 -1
- package/lib/esm/index.js +12 -5
- package/lib/esm/view/expenseAutomationView/bulkUploadTiming.js +10 -0
- package/lib/esm/view/expenseAutomationView/epics/missingReceipts/bulkUploadReceiptsEpic.js +29 -0
- package/lib/esm/view/expenseAutomationView/epics/missingReceipts/confirmBulkUploadMatchEpic.js +49 -0
- package/lib/esm/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchDetailsEpic.js +40 -0
- package/lib/esm/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchesEpic.js +47 -0
- package/lib/esm/view/expenseAutomationView/epics/missingReceipts/fetchCompletedTransactionsEpic.js +61 -0
- package/lib/esm/view/expenseAutomationView/epics/missingReceipts/fetchMoreBatchDetailsEpic.js +33 -0
- package/lib/esm/view/expenseAutomationView/epics/missingReceipts/fetchMultipleBatchDetailsEpic.js +56 -0
- package/lib/esm/view/expenseAutomationView/epics/missingReceipts/searchTransactionsForManualMatchEpic.js +77 -0
- package/lib/esm/view/expenseAutomationView/epics/missingReceipts/uploadMissingReceiptSuccessEpic.js +7 -5
- package/lib/esm/view/expenseAutomationView/epics/missingReceipts/watchBulkUploadBatchStatusEpic.js +79 -0
- package/lib/esm/view/expenseAutomationView/epics/transactionCategorization/uploadTransactionReceiptSuccessEpic.js +24 -0
- package/lib/esm/view/expenseAutomationView/payload/missingReceiptsPayload.js +189 -1
- package/lib/esm/view/expenseAutomationView/reducers/missingReceiptsViewReducer.js +433 -2
- package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +23 -1
- package/lib/esm/view/expenseAutomationView/selectors/missingReceiptsSelector.js +244 -1
- package/lib/esm/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +2 -1
- package/lib/esm/view/expenseAutomationView/types/missingReceiptsViewState.js +44 -0
- package/lib/esm/view/expenseAutomationView/types/transactionsViewState.js +2 -1
- package/lib/esm/view/financeStatement/financeStatementSelector.js +5 -0
- package/lib/esm/view/profitAndLossClassesView/profitAndLossClassesByClassHorizontalSelector.js +281 -0
- package/lib/esm/view/profitAndLossClassesView/profitAndLossClassesByClassHorizontalSelectorTypes.js +1 -0
- package/lib/esm/view/profitAndLossClassesView/profitAndLossClassesViewReducer.js +5 -1
- package/lib/esm/view/taskManager/taskDetailView/taskDetailSelector.js +1 -1
- package/lib/esm/zeniAPI.js +0 -2
- package/lib/index.d.ts +12 -7
- package/lib/index.js +85 -32
- package/lib/tsconfig.typecheck.tsbuildinfo +1 -1
- package/lib/view/expenseAutomationView/bulkUploadTiming.d.ts +10 -0
- package/lib/view/expenseAutomationView/bulkUploadTiming.js +13 -0
- package/lib/view/expenseAutomationView/epics/missingReceipts/bulkUploadReceiptsEpic.d.ts +18 -0
- package/lib/view/expenseAutomationView/epics/missingReceipts/bulkUploadReceiptsEpic.js +33 -0
- package/lib/view/expenseAutomationView/epics/missingReceipts/confirmBulkUploadMatchEpic.d.ts +7 -0
- package/lib/view/expenseAutomationView/epics/missingReceipts/confirmBulkUploadMatchEpic.js +53 -0
- package/lib/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchDetailsEpic.d.ts +8 -0
- package/lib/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchDetailsEpic.js +44 -0
- package/lib/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchesEpic.d.ts +7 -0
- package/lib/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchesEpic.js +51 -0
- package/lib/view/expenseAutomationView/epics/missingReceipts/fetchCompletedTransactionsEpic.d.ts +8 -0
- package/lib/view/expenseAutomationView/epics/missingReceipts/fetchCompletedTransactionsEpic.js +65 -0
- package/lib/view/expenseAutomationView/epics/missingReceipts/fetchMoreBatchDetailsEpic.d.ts +8 -0
- package/lib/view/expenseAutomationView/epics/missingReceipts/fetchMoreBatchDetailsEpic.js +37 -0
- package/lib/view/expenseAutomationView/epics/missingReceipts/fetchMultipleBatchDetailsEpic.d.ts +9 -0
- package/lib/view/expenseAutomationView/epics/missingReceipts/fetchMultipleBatchDetailsEpic.js +61 -0
- package/lib/view/expenseAutomationView/epics/missingReceipts/searchTransactionsForManualMatchEpic.d.ts +14 -0
- package/lib/view/expenseAutomationView/epics/missingReceipts/searchTransactionsForManualMatchEpic.js +81 -0
- package/lib/view/expenseAutomationView/epics/missingReceipts/uploadMissingReceiptSuccessEpic.js +7 -5
- package/lib/view/expenseAutomationView/epics/missingReceipts/watchBulkUploadBatchStatusEpic.d.ts +25 -0
- package/lib/view/expenseAutomationView/epics/missingReceipts/watchBulkUploadBatchStatusEpic.js +85 -0
- package/lib/view/expenseAutomationView/epics/transactionCategorization/uploadTransactionReceiptSuccessEpic.d.ts +7 -0
- package/lib/view/expenseAutomationView/epics/transactionCategorization/uploadTransactionReceiptSuccessEpic.js +28 -0
- package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
- package/lib/view/expenseAutomationView/payload/missingReceiptsPayload.d.ts +118 -0
- package/lib/view/expenseAutomationView/payload/missingReceiptsPayload.js +201 -0
- package/lib/view/expenseAutomationView/reducers/missingReceiptsViewReducer.d.ts +76 -3
- package/lib/view/expenseAutomationView/reducers/missingReceiptsViewReducer.js +434 -2
- package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.d.ts +11 -3
- package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +24 -2
- package/lib/view/expenseAutomationView/selectorTypes/missingReceiptsSelectorTypes.d.ts +69 -1
- package/lib/view/expenseAutomationView/selectorTypes/transactionsViewSelectorTypes.d.ts +1 -0
- package/lib/view/expenseAutomationView/selectors/missingReceiptsSelector.d.ts +1 -1
- package/lib/view/expenseAutomationView/selectors/missingReceiptsSelector.js +243 -0
- package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +2 -1
- package/lib/view/expenseAutomationView/types/missingReceiptsViewState.d.ts +142 -0
- package/lib/view/expenseAutomationView/types/missingReceiptsViewState.js +54 -1
- package/lib/view/expenseAutomationView/types/transactionsViewState.d.ts +2 -1
- package/lib/view/expenseAutomationView/types/transactionsViewState.js +2 -1
- package/lib/view/financeStatement/financeStatementSelector.d.ts +2 -0
- package/lib/view/financeStatement/financeStatementSelector.js +5 -0
- package/lib/view/profitAndLossClassesView/profitAndLossClassesByClassHorizontalSelector.d.ts +13 -0
- package/lib/view/profitAndLossClassesView/profitAndLossClassesByClassHorizontalSelector.js +285 -0
- package/lib/view/profitAndLossClassesView/profitAndLossClassesByClassHorizontalSelectorTypes.d.ts +57 -0
- package/lib/view/profitAndLossClassesView/profitAndLossClassesByClassHorizontalSelectorTypes.js +2 -0
- package/lib/view/profitAndLossClassesView/profitAndLossClassesViewReducer.d.ts +4 -2
- package/lib/view/profitAndLossClassesView/profitAndLossClassesViewReducer.js +6 -2
- package/lib/view/profitAndLossClassesView/profitAndLossClassesViewState.d.ts +2 -0
- package/lib/view/taskManager/taskDetailView/taskDetailSelector.js +1 -1
- package/lib/zeniAPI.js +0 -2
- package/package.json +1 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import orderBy from 'lodash/orderBy';
|
|
2
2
|
import { toMonthYearPeriodId } from '../../../commonStateTypes/timePeriod';
|
|
3
3
|
import { getCurrentTenant } from '../../../entity/tenant/tenantSelector';
|
|
4
|
-
import { getSupportedTransactionsByIds } from '../../../entity/transaction/transactionSelector';
|
|
4
|
+
import { getSupportedTransactionById, getSupportedTransactionsByIds, } from '../../../entity/transaction/transactionSelector';
|
|
5
|
+
import { isBatchListStatusCompleted } from '../payload/missingReceiptsPayload';
|
|
6
|
+
import { getCompletedTransactionsCacheKey } from '../reducers/missingReceiptsViewReducer';
|
|
5
7
|
import { getAllSteps } from '../selectorTypes/expenseAutomationViewSelectorTypes';
|
|
8
|
+
import { isUnmatchedTabFileStatus, } from '../types/missingReceiptsViewState';
|
|
6
9
|
export function getExpenseAutomationMissingReceiptsView(state) {
|
|
7
10
|
const { expenseAutomationMissingReceiptsViewState, expenseAutomationViewState, transactionState, monthEndCloseChecksState, } = state;
|
|
8
11
|
const currentTenant = getCurrentTenant(state);
|
|
@@ -47,6 +50,131 @@ export function getExpenseAutomationMissingReceiptsView(state) {
|
|
|
47
50
|
const completionStatus = missingReceiptsCompletionStatus != null
|
|
48
51
|
? missingReceiptsCompletionStatus
|
|
49
52
|
: 'incomplete';
|
|
53
|
+
const { bulkUpload, pendingMissingReceiptsTabNavigation } = expenseAutomationMissingReceiptsViewState;
|
|
54
|
+
const currentBatchStatus = bulkUpload.currentBatchId != null
|
|
55
|
+
? bulkUpload.batchStatusById[bulkUpload.currentBatchId]
|
|
56
|
+
: undefined;
|
|
57
|
+
const batchList = monthYearPeriodId != null
|
|
58
|
+
? (bulkUpload.batchListByPeriod[monthYearPeriodId] ?? [])
|
|
59
|
+
: [];
|
|
60
|
+
/**
|
|
61
|
+
* Primary "Unmatched" batch = first completed batch in platform list order, preferring the
|
|
62
|
+
* first batch that still has possible_matches/no_match work when any completed batch has such work.
|
|
63
|
+
* Batches without details yet: wait if the list row still reports unmatched work; otherwise skip
|
|
64
|
+
* so we do not block the whole tab while other rows are not fetched yet.
|
|
65
|
+
*/
|
|
66
|
+
const primaryBatchIdFromDetailsOrder = getPrimaryBatchIdFromBatchListOrder(batchList, bulkUpload.batchDetailsById, bulkUpload.failedBatchDetailIds);
|
|
67
|
+
/**
|
|
68
|
+
* During upload/matching, prefer `currentBatchId` only when its batch details are loaded,
|
|
69
|
+
* completed, and still have unmatched-tab work. Otherwise keep `primaryBatchIdFromDetailsOrder`.
|
|
70
|
+
*
|
|
71
|
+
* If we pinned to `currentBatchId` as soon as it was absent from the batch list (in-flight
|
|
72
|
+
* upload), details are often not loaded yet — `unmatchedFiles` would empty while older batches
|
|
73
|
+
* moved to "past", so the primary "Unmatched Receipts" section vanished during auto-matching.
|
|
74
|
+
*/
|
|
75
|
+
const unmatchedSectionBatchId = (() => {
|
|
76
|
+
const phase = bulkUpload.phase;
|
|
77
|
+
if (phase !== 'matching' && phase !== 'uploading') {
|
|
78
|
+
return primaryBatchIdFromDetailsOrder;
|
|
79
|
+
}
|
|
80
|
+
const currentId = bulkUpload.currentBatchId;
|
|
81
|
+
if (currentId == null) {
|
|
82
|
+
return primaryBatchIdFromDetailsOrder;
|
|
83
|
+
}
|
|
84
|
+
const currentDetails = bulkUpload.batchDetailsById[currentId];
|
|
85
|
+
if (currentDetails != null &&
|
|
86
|
+
currentDetails.status === 'completed' &&
|
|
87
|
+
batchDetailsHasUnmatchedWork(currentDetails)) {
|
|
88
|
+
return currentId;
|
|
89
|
+
}
|
|
90
|
+
return primaryBatchIdFromDetailsOrder;
|
|
91
|
+
})();
|
|
92
|
+
const completedBatchesInApiOrder = batchList.filter((batchListItem) => isBatchListStatusCompleted(batchListItem.status));
|
|
93
|
+
const currentBatchDetails = unmatchedSectionBatchId != null
|
|
94
|
+
? bulkUpload.batchDetailsById[unmatchedSectionBatchId]
|
|
95
|
+
: undefined;
|
|
96
|
+
const resolvedFiles = currentBatchDetails?.files.map((file) => resolveBatchFile(file, currentBatchDetails.batchId, transactionState));
|
|
97
|
+
const failedIds = new Set(bulkUpload.failedBatchDetailIds);
|
|
98
|
+
const hasMoreBatchDetails = completedBatchesInApiOrder.some((batchListItem) => bulkUpload.batchDetailsById[batchListItem.batchId] == null &&
|
|
99
|
+
!failedIds.has(batchListItem.batchId));
|
|
100
|
+
const getSortValue = (file) => {
|
|
101
|
+
const matchedTransaction = file.matchedTransaction;
|
|
102
|
+
if (matchedTransaction == null) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
switch (bulkUpload.sortKey) {
|
|
106
|
+
case 'vendor':
|
|
107
|
+
return ((matchedTransaction.vendorName ?? matchedTransaction.description)?.toLowerCase() ?? null);
|
|
108
|
+
case 'category': {
|
|
109
|
+
const line = matchedTransaction.lines?.[0];
|
|
110
|
+
return line?.account?.accountName?.toLowerCase() ?? null;
|
|
111
|
+
}
|
|
112
|
+
case 'class': {
|
|
113
|
+
const line = matchedTransaction.lines?.[0];
|
|
114
|
+
return line?.class?.className?.toLowerCase() ?? null;
|
|
115
|
+
}
|
|
116
|
+
case 'date':
|
|
117
|
+
return matchedTransaction.date.valueOf();
|
|
118
|
+
case 'amount':
|
|
119
|
+
return matchedTransaction.amount.amount;
|
|
120
|
+
default:
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const bulkUploadSortDirection = bulkUpload.sortOrder === 'ascending' ? 'asc' : 'desc';
|
|
125
|
+
const matchedFiles = orderBy(resolvedFiles?.filter((resolvedFile) => resolvedFile.status === 'exact_match') ?? [], getSortValue, bulkUploadSortDirection);
|
|
126
|
+
const unmatchedFiles = orderBy(resolvedFiles?.filter((resolvedFile) => isUnmatchedTabFileStatus(resolvedFile.status)) ?? [], getSortValue, bulkUploadSortDirection);
|
|
127
|
+
const progress = currentBatchStatus != null
|
|
128
|
+
? {
|
|
129
|
+
...currentBatchStatus.progress,
|
|
130
|
+
percentage: currentBatchStatus.progress.total > 0
|
|
131
|
+
? Math.round((currentBatchStatus.progress.processed /
|
|
132
|
+
currentBatchStatus.progress.total) *
|
|
133
|
+
100)
|
|
134
|
+
: 0,
|
|
135
|
+
}
|
|
136
|
+
: { total: 0, processed: 0, percentage: 0 };
|
|
137
|
+
const batchListFetchState = bulkUpload.batchListFetchState;
|
|
138
|
+
/**
|
|
139
|
+
* When primary batch is not resolved yet, still aggregate unmatched files from every batch
|
|
140
|
+
* that has details loaded (otherwise past is empty and receipts vanish from the UI).
|
|
141
|
+
*/
|
|
142
|
+
const pastBatches = unmatchedSectionBatchId != null
|
|
143
|
+
? batchList.filter((batchListItem) => batchListItem.batchId !== unmatchedSectionBatchId)
|
|
144
|
+
: batchList;
|
|
145
|
+
const pastUnmatchedFiles = pastBatches.flatMap((pastBatch) => {
|
|
146
|
+
const details = bulkUpload.batchDetailsById[pastBatch.batchId];
|
|
147
|
+
if (details == null) {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
return details.files
|
|
151
|
+
.filter((batchFile) => isUnmatchedTabFileStatus(batchFile.status))
|
|
152
|
+
.map((batchFile) => resolveBatchFile(batchFile, pastBatch.batchId, transactionState));
|
|
153
|
+
});
|
|
154
|
+
const completedTransactionsCacheKey = monthYearPeriodId != null
|
|
155
|
+
? getCompletedTransactionsCacheKey(monthYearPeriodId, bulkUpload.sortKey, bulkUpload.sortOrder, bulkUpload.completedSubTab)
|
|
156
|
+
: undefined;
|
|
157
|
+
const completedTransactionsForPeriod = completedTransactionsCacheKey != null
|
|
158
|
+
? bulkUpload.completedTransactionsByPeriod[completedTransactionsCacheKey]
|
|
159
|
+
: undefined;
|
|
160
|
+
const completedTransactionIds = completedTransactionsForPeriod?.transactionIds ?? [];
|
|
161
|
+
const resolvedCompletedTransactions = getSupportedTransactionsByIds(transactionState, completedTransactionIds);
|
|
162
|
+
const resolvedBatchDetails = currentBatchDetails != null && resolvedFiles != null
|
|
163
|
+
? {
|
|
164
|
+
batchId: currentBatchDetails.batchId,
|
|
165
|
+
createdAt: currentBatchDetails.createdAt,
|
|
166
|
+
files: resolvedFiles,
|
|
167
|
+
status: currentBatchDetails.status,
|
|
168
|
+
summary: currentBatchDetails.summary,
|
|
169
|
+
}
|
|
170
|
+
: undefined;
|
|
171
|
+
const manualSearchResults = bulkUpload.manualSearch.results.map((result) => {
|
|
172
|
+
const transaction = getSupportedTransactionById(transactionState, result.transactionId);
|
|
173
|
+
return {
|
|
174
|
+
...result,
|
|
175
|
+
vendorLogo: result.vendorLogo ?? transaction?.logo?.href,
|
|
176
|
+
};
|
|
177
|
+
});
|
|
50
178
|
return {
|
|
51
179
|
fetchState,
|
|
52
180
|
error,
|
|
@@ -55,5 +183,120 @@ export function getExpenseAutomationMissingReceiptsView(state) {
|
|
|
55
183
|
refreshStatus,
|
|
56
184
|
transactions: sortedTransactions,
|
|
57
185
|
completionStatus,
|
|
186
|
+
pendingMissingReceiptsTabNavigation: pendingMissingReceiptsTabNavigation ?? null,
|
|
187
|
+
bulkUpload: {
|
|
188
|
+
phase: bulkUpload.phase,
|
|
189
|
+
uploadStatus: bulkUpload.uploadStatus,
|
|
190
|
+
batchStatus: currentBatchStatus,
|
|
191
|
+
batchDetails: resolvedBatchDetails,
|
|
192
|
+
completedSubTab: bulkUpload.completedSubTab,
|
|
193
|
+
completedTransactions: {
|
|
194
|
+
fetchState: completedTransactionsForPeriod?.fetchState ?? {
|
|
195
|
+
fetchState: 'Not-Started',
|
|
196
|
+
error: undefined,
|
|
197
|
+
},
|
|
198
|
+
nextPageToken: completedTransactionsForPeriod?.nextPageToken ?? null,
|
|
199
|
+
totalCount: completedTransactionsForPeriod?.totalCount ?? 0,
|
|
200
|
+
transactions: resolvedCompletedTransactions,
|
|
201
|
+
},
|
|
202
|
+
matchedFiles,
|
|
203
|
+
unmatchedFiles,
|
|
204
|
+
progress,
|
|
205
|
+
currentResultsTab: bulkUpload.currentResultsTab,
|
|
206
|
+
sortKey: bulkUpload.sortKey,
|
|
207
|
+
sortOrder: bulkUpload.sortOrder,
|
|
208
|
+
batchList,
|
|
209
|
+
batchListFetchState,
|
|
210
|
+
pastBatches,
|
|
211
|
+
pastUnmatchedFiles,
|
|
212
|
+
confirmMatchStatus: bulkUpload.confirmMatchStatus,
|
|
213
|
+
manualSearchUiResetKey: bulkUpload.manualSearchUiResetKey,
|
|
214
|
+
hasMoreBatchDetails,
|
|
215
|
+
batchDetailsPaginationState: bulkUpload.batchDetailsPaginationState,
|
|
216
|
+
manualSearch: {
|
|
217
|
+
...bulkUpload.manualSearch,
|
|
218
|
+
results: manualSearchResults,
|
|
219
|
+
},
|
|
220
|
+
uploadProgress: bulkUpload.uploadProgress,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function batchDetailsHasUnmatchedWork(details) {
|
|
225
|
+
return details.files.some((file) => isUnmatchedTabFileStatus(file.status));
|
|
226
|
+
}
|
|
227
|
+
/** List row summary before batch details load — aligns with unmatched / possible-match work. */
|
|
228
|
+
function batchListItemHasUnmatchedWork(batch) {
|
|
229
|
+
return batch.noMatchCount > 0 || batch.possibleMatchesCount > 0;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* `batchList` must match platform order (same sequence as the bulk-upload batches API). Picks the
|
|
233
|
+
* primary batch for the top "Unmatched" section. Skips failed detail fetches.
|
|
234
|
+
*
|
|
235
|
+
* If details are not loaded for an earlier list row but that row reports no unmatched work, we
|
|
236
|
+
* skip it so later rows with loaded details can be primary (avoids an empty tab when the first
|
|
237
|
+
* fetch page only loaded later batches). If the list reports unmatched work but details are still
|
|
238
|
+
* loading, we wait (undefined) so primary order stays correct once details arrive.
|
|
239
|
+
*/
|
|
240
|
+
function getPrimaryBatchIdFromBatchListOrder(batchListInPlatformOrder, batchDetailsById, failedBatchDetailIds) {
|
|
241
|
+
const failedIds = new Set(failedBatchDetailIds);
|
|
242
|
+
const anyCompletedBatchHasUnmatchedWork = batchListInPlatformOrder.some((batch) => {
|
|
243
|
+
if (failedIds.has(batch.batchId)) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
const details = batchDetailsById[batch.batchId];
|
|
247
|
+
if (details != null &&
|
|
248
|
+
details.status === 'completed' &&
|
|
249
|
+
batchDetailsHasUnmatchedWork(details)) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
return details == null && batchListItemHasUnmatchedWork(batch);
|
|
253
|
+
});
|
|
254
|
+
for (const batch of batchListInPlatformOrder) {
|
|
255
|
+
if (failedIds.has(batch.batchId)) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const details = batchDetailsById[batch.batchId];
|
|
259
|
+
if (details == null) {
|
|
260
|
+
if (batchListItemHasUnmatchedWork(batch)) {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (details.status !== 'completed') {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (anyCompletedBatchHasUnmatchedWork) {
|
|
269
|
+
if (batchDetailsHasUnmatchedWork(details)) {
|
|
270
|
+
return batch.batchId;
|
|
271
|
+
}
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
return batch.batchId;
|
|
275
|
+
}
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
function resolveBatchFile(file, batchId, transactionState) {
|
|
279
|
+
const matchedTransaction = file.matchedTransactionId != null
|
|
280
|
+
? getSupportedTransactionById(transactionState, file.matchedTransactionId)
|
|
281
|
+
: undefined;
|
|
282
|
+
const candidates = file.candidates
|
|
283
|
+
?.map((ref) => {
|
|
284
|
+
const transaction = getSupportedTransactionById(transactionState, ref.transactionId);
|
|
285
|
+
return transaction != null
|
|
286
|
+
? { transaction, matchScore: ref.matchScore }
|
|
287
|
+
: undefined;
|
|
288
|
+
})
|
|
289
|
+
.filter((candidate) => candidate != null);
|
|
290
|
+
return {
|
|
291
|
+
attachmentId: file.attachmentId,
|
|
292
|
+
batchId,
|
|
293
|
+
fileDownloadUrl: file.fileDownloadUrl,
|
|
294
|
+
fileId: file.fileId,
|
|
295
|
+
filename: file.filename,
|
|
296
|
+
filePreviewUrl: file.filePreviewUrl,
|
|
297
|
+
status: file.status,
|
|
298
|
+
matchedTransaction,
|
|
299
|
+
candidates: candidates != null && candidates.length > 0 ? candidates : undefined,
|
|
300
|
+
matchSource: file.matchSource,
|
|
58
301
|
};
|
|
59
302
|
}
|
|
@@ -10,7 +10,7 @@ import { getAllSteps } from '../selectorTypes/expenseAutomationViewSelectorTypes
|
|
|
10
10
|
export function getExpenseAutomationTransactionView(state) {
|
|
11
11
|
const { accountState, classState, classListState, accountListState, expenseAutomationTransactionsViewState, expenseAutomationViewState, transactionState, monthEndCloseChecksState, } = state;
|
|
12
12
|
const { selectedTransactionCategorizationTab } = expenseAutomationTransactionsViewState;
|
|
13
|
-
const { uiState, refreshStatus, saveStatus, markAsNotMiscategorizedStatus, transactionIdsBySelectedPeriod, transactionReviewLocalDataById, selectedCheckBoxTransactionIds, transactionIdsWithUnsavedData, selectedTransactionId, selectedTransactionLineId, } = expenseAutomationTransactionsViewState.transactionCategorizationView[selectedTransactionCategorizationTab];
|
|
13
|
+
const { uiState, refreshStatus, saveStatus, markAsNotMiscategorizedStatus, transactionIdsBySelectedPeriod, transactionReviewLocalDataById, selectedCheckBoxTransactionIds, transactionIdsWithUnsavedData, uploadReceiptStatusById, selectedTransactionId, selectedTransactionLineId, } = expenseAutomationTransactionsViewState.transactionCategorizationView[selectedTransactionCategorizationTab];
|
|
14
14
|
const uncategorizedIncomeExpense = getUncategorizedAccounts(accountState, accountListState, 'accountList');
|
|
15
15
|
const accountList = getAccountList(accountState, accountListState, 'accountList');
|
|
16
16
|
const classList = getClassList(classState, classListState);
|
|
@@ -94,6 +94,7 @@ export function getExpenseAutomationTransactionView(state) {
|
|
|
94
94
|
completionStatus,
|
|
95
95
|
totalCountByTab,
|
|
96
96
|
fetchStateByTransactionTabs: fetchStateByTab,
|
|
97
|
+
uploadReceiptStatusById,
|
|
97
98
|
selectedTransactionId,
|
|
98
99
|
selectedTransactionLineId,
|
|
99
100
|
};
|
|
@@ -7,3 +7,47 @@ const MISSING_RECEIPTS_SORT_KEYS = [
|
|
|
7
7
|
'amount',
|
|
8
8
|
];
|
|
9
9
|
export const toMissingReceiptsSortKey = (v) => stringToUnion(v, MISSING_RECEIPTS_SORT_KEYS);
|
|
10
|
+
// -- Bulk Receipt Upload Types --
|
|
11
|
+
/**
|
|
12
|
+
* Minimum trimmed query length before manual-match search calls the expense-automation API.
|
|
13
|
+
* Kept in sync with `ManualSearchInput` in web-components (fires search only when length ≥ this).
|
|
14
|
+
*/
|
|
15
|
+
export const MIN_MANUAL_TRANSACTION_SEARCH_LENGTH = 2;
|
|
16
|
+
const BATCH_STATUS_VALUES = ['pending', 'processing', 'completed'];
|
|
17
|
+
export const toBatchStatusValue = (v) => stringToUnion(v.trim().toLowerCase(), BATCH_STATUS_VALUES);
|
|
18
|
+
/** Canonical batch file statuses (API ingress may use aliases; see `parseBatchFileStatus`). */
|
|
19
|
+
export const BATCH_FILE_STATUSES = [
|
|
20
|
+
'exact_match',
|
|
21
|
+
'possible_matches',
|
|
22
|
+
'no_match',
|
|
23
|
+
'failed',
|
|
24
|
+
];
|
|
25
|
+
export const toBatchFileStatus = (v) => stringToUnion(v.trim().toLowerCase().replace(/-/g, '_'), BATCH_FILE_STATUSES);
|
|
26
|
+
const UNMATCHED_TAB_FILE_STATUSES = ['possible_matches', 'no_match'];
|
|
27
|
+
/** True for files that belong in the Unmatched tab (possible matches or no match). */
|
|
28
|
+
export function isUnmatchedTabFileStatus(status) {
|
|
29
|
+
return UNMATCHED_TAB_FILE_STATUSES.includes(status);
|
|
30
|
+
}
|
|
31
|
+
const BULK_UPLOAD_PHASES = [
|
|
32
|
+
'idle',
|
|
33
|
+
'uploading',
|
|
34
|
+
'matching',
|
|
35
|
+
'completed',
|
|
36
|
+
];
|
|
37
|
+
export const toBulkUploadPhase = (v) => stringToUnion(v.trim().toLowerCase(), BULK_UPLOAD_PHASES);
|
|
38
|
+
const BULK_UPLOAD_RESULTS_TABS = ['completed', 'unmatched'];
|
|
39
|
+
export const toBulkUploadResultsTab = (v) => stringToUnion(v.trim().toLowerCase(), BULK_UPLOAD_RESULTS_TABS);
|
|
40
|
+
const MISSING_RECEIPTS_TABS = ['missing', 'completed', 'unmatched'];
|
|
41
|
+
export const toMissingReceiptsTab = (v) => stringToUnion(v.trim().toLowerCase(), MISSING_RECEIPTS_TABS);
|
|
42
|
+
const COMPLETED_SUB_TABS = ['all', 'ai_accountant', 'manual'];
|
|
43
|
+
export const toCompletedSubTab = (v) => stringToUnion(v.trim().toLowerCase(), COMPLETED_SUB_TABS);
|
|
44
|
+
const MATCH_SOURCES = ['ai', 'manual'];
|
|
45
|
+
export const toMatchSource = (v) => stringToUnion(v.trim().toLowerCase(), MATCH_SOURCES);
|
|
46
|
+
const BULK_UPLOAD_SORT_KEYS = [
|
|
47
|
+
'amount',
|
|
48
|
+
'category',
|
|
49
|
+
'class',
|
|
50
|
+
'date',
|
|
51
|
+
'vendor',
|
|
52
|
+
];
|
|
53
|
+
export const toBulkUploadSortKey = (v) => stringToUnion(v.trim().toLowerCase(), BULK_UPLOAD_SORT_KEYS);
|
|
@@ -4,10 +4,11 @@ const TRANSACTIONS_SORT_KEYS = [
|
|
|
4
4
|
'vendor',
|
|
5
5
|
'amount',
|
|
6
6
|
'transaction_type',
|
|
7
|
+
'payment_account',
|
|
7
8
|
'date',
|
|
8
9
|
'category',
|
|
9
10
|
'class',
|
|
10
|
-
'
|
|
11
|
+
'memo_and_receipt',
|
|
11
12
|
];
|
|
12
13
|
export const toTransactionsSortKey = (v) => stringToUnion(v, TRANSACTIONS_SORT_KEYS);
|
|
13
14
|
export const TRANSACTIONS_TABS = ['review', 'autoCategorized'];
|
|
@@ -8,6 +8,7 @@ import { extractThisPeriod } from '../../commonStateTypes/viewAndReport/thisPeri
|
|
|
8
8
|
import { getBalanceSheet } from '../balanceSheet/balanceSheetSelector';
|
|
9
9
|
import { getCashFlow } from '../cashFlow/cashFlowSelector';
|
|
10
10
|
import { getProfitAndLossReport, } from '../profitAndLoss/profitAndLossSelector';
|
|
11
|
+
import { getProfitAndLossClassesHorizontalView } from '../profitAndLossClassesView/profitAndLossClassesByClassHorizontalSelector';
|
|
11
12
|
import { getProfitAndLossClassesView } from '../profitAndLossClassesView/profitAndLossClassesViewSelector';
|
|
12
13
|
function getDateRangeForFinanceReports(profitAndLossState, balanceSheetState, cashFlowState) {
|
|
13
14
|
const datesSorted = [
|
|
@@ -60,6 +61,9 @@ export const getFinanceStatement = createSelector((state) => state.accountState,
|
|
|
60
61
|
const profitAndLossReport = getProfitAndLossReport(filter, undefined, accountState, accountGroupState, sectionAccountsViewState, profitAndLossState, forecastState);
|
|
61
62
|
const classFilter = newBalancesFilterClassesView(filter.firstMonthOfFY, filter.timeframe, filter.balancesRange.numberOfPeriods, filter.balancesRange.thisPeriod, filter.balancesRange.orderBy, filter.additionalBalances, profitAndLossClassesViewState.uiState.classesToFilterOut);
|
|
62
63
|
const profitAndLossByClassReport = getProfitAndLossClassesView(classFilter, accountState, classState, sectionsState, profitAndLossClassesViewState);
|
|
64
|
+
const profitAndLossByClassHorizontalReport = profitAndLossClassesViewState.uiState.classViewLayout === 'horizontal'
|
|
65
|
+
? getProfitAndLossClassesHorizontalView(classFilter, accountState, classState, sectionsState, profitAndLossClassesViewState, thisPeriod)
|
|
66
|
+
: getProfitAndLossClassesHorizontalView(classFilter, accountState, classState, sectionsState, profitAndLossClassesViewState, undefined);
|
|
63
67
|
const balanceSheetReport = getBalanceSheet(filter, accountState, accountGroupState, sectionAccountsViewState, balanceSheetState);
|
|
64
68
|
const cashFlowReport = getCashFlow(filter, accountState, accountGroupState, sectionAccountsViewState, cashFlowState);
|
|
65
69
|
const allReports = [
|
|
@@ -107,6 +111,7 @@ export const getFinanceStatement = createSelector((state) => state.accountState,
|
|
|
107
111
|
dataAvailable,
|
|
108
112
|
profitAndLossReport,
|
|
109
113
|
profitAndLossByClassReport,
|
|
114
|
+
profitAndLossByClassHorizontalReport,
|
|
110
115
|
balanceSheetReport,
|
|
111
116
|
cashFlowReport,
|
|
112
117
|
filter,
|
package/lib/esm/view/profitAndLossClassesView/profitAndLossClassesByClassHorizontalSelector.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { toAmountWC } from '../../commonStateTypes/amount';
|
|
2
|
+
import { getSectionClassesViewReport } from '../../entity/sectionClassesViewV2/sectionClassesViewSelector';
|
|
3
|
+
import { ALL_PANDL_REPORT_VIEW_SECTION_IDS, } from './profitAndLossClassesViewSelectorTypes';
|
|
4
|
+
const DEFAULT_CURRENCY = { currencyCode: 'USD', currencySymbol: '$' };
|
|
5
|
+
export const profitAndLossHorizontalIdsInOrder = [
|
|
6
|
+
'income',
|
|
7
|
+
'cogs',
|
|
8
|
+
'grossProfit',
|
|
9
|
+
'expenses',
|
|
10
|
+
'earningsBeforeInterestAndTax',
|
|
11
|
+
'otherIncome',
|
|
12
|
+
'otherExpenses',
|
|
13
|
+
'netOtherIncome',
|
|
14
|
+
'netIncome',
|
|
15
|
+
];
|
|
16
|
+
const SECTION_TITLES = {
|
|
17
|
+
income: 'Income',
|
|
18
|
+
cogs: 'Cost of Goods Sold',
|
|
19
|
+
grossProfit: 'Gross Profit',
|
|
20
|
+
expenses: 'Expenses',
|
|
21
|
+
earningsBeforeInterestAndTax: 'Net Operating Income',
|
|
22
|
+
otherIncome: 'Other Income',
|
|
23
|
+
otherExpenses: 'Other Expenses',
|
|
24
|
+
netOtherIncome: 'Net Other Income',
|
|
25
|
+
netIncome: 'Net Income',
|
|
26
|
+
};
|
|
27
|
+
export const getProfitAndLossClassesHorizontalView = (filter, accountState, classState, sectionsState, profitAndLossClassesViewState, selectedPeriod) => {
|
|
28
|
+
const fetchState = reduceFetchState([
|
|
29
|
+
profitAndLossClassesViewState.fetchState.month,
|
|
30
|
+
profitAndLossClassesViewState.fetchState.quarter,
|
|
31
|
+
profitAndLossClassesViewState.fetchState.year,
|
|
32
|
+
]);
|
|
33
|
+
const reportId = 'profit_and_loss_by_classes';
|
|
34
|
+
// Build class columns from the class hierarchy (top-level only)
|
|
35
|
+
const classColumns = buildClassColumns(profitAndLossClassesViewState.classHierarchy, profitAndLossClassesViewState.uiState.classesToFilterOut);
|
|
36
|
+
const sections = {};
|
|
37
|
+
const calculatedSections = {};
|
|
38
|
+
if (fetchState.fetchState === 'Completed' && selectedPeriod != null) {
|
|
39
|
+
// Get section reports using existing selector infrastructure
|
|
40
|
+
const sectionReports = {};
|
|
41
|
+
ALL_PANDL_REPORT_VIEW_SECTION_IDS.forEach((sectionId) => {
|
|
42
|
+
sectionReports[sectionId] = getSectionClassesViewReport(accountState, classState, sectionsState, { reportId, sectionId }, filter);
|
|
43
|
+
});
|
|
44
|
+
// Pivot each section from class→account to account→class
|
|
45
|
+
const sectionIds = [
|
|
46
|
+
'income',
|
|
47
|
+
'cogs',
|
|
48
|
+
'expenses',
|
|
49
|
+
'otherIncome',
|
|
50
|
+
'otherExpenses',
|
|
51
|
+
];
|
|
52
|
+
sectionIds.forEach((sectionId) => {
|
|
53
|
+
const sectionReport = sectionReports[sectionId];
|
|
54
|
+
if (sectionReport != null) {
|
|
55
|
+
sections[sectionId] = pivotSectionToHorizontal(sectionReport, classColumns, SECTION_TITLES[sectionId] ?? sectionId);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
// Calculate derived sections
|
|
59
|
+
const incomeSection = sections['income'];
|
|
60
|
+
const cogsSection = sections['cogs'];
|
|
61
|
+
const expensesSection = sections['expenses'];
|
|
62
|
+
const otherIncomeSection = sections['otherIncome'];
|
|
63
|
+
const otherExpensesSection = sections['otherExpenses'];
|
|
64
|
+
// Gross Profit = Income - COGS
|
|
65
|
+
if (incomeSection != null && cogsSection != null) {
|
|
66
|
+
calculatedSections['grossProfit'] = computeCalculatedSection(incomeSection.classTotals, cogsSection.classTotals, incomeSection.sectionTotal, cogsSection.sectionTotal, 'subtract', SECTION_TITLES['grossProfit']);
|
|
67
|
+
}
|
|
68
|
+
// EBIT = Gross Profit - Expenses
|
|
69
|
+
const grossProfit = calculatedSections['grossProfit'];
|
|
70
|
+
if (grossProfit != null && expensesSection != null) {
|
|
71
|
+
calculatedSections['earningsBeforeInterestAndTax'] =
|
|
72
|
+
computeCalculatedSection(grossProfit.classTotals, expensesSection.classTotals, grossProfit.sectionTotal, expensesSection.sectionTotal, 'subtract', SECTION_TITLES['earningsBeforeInterestAndTax']);
|
|
73
|
+
}
|
|
74
|
+
// Net Other Income = Other Income - Other Expenses
|
|
75
|
+
if (otherIncomeSection != null && otherExpensesSection != null) {
|
|
76
|
+
calculatedSections['netOtherIncome'] = computeCalculatedSection(otherIncomeSection.classTotals, otherExpensesSection.classTotals, otherIncomeSection.sectionTotal, otherExpensesSection.sectionTotal, 'subtract', SECTION_TITLES['netOtherIncome']);
|
|
77
|
+
}
|
|
78
|
+
// Net Income = EBIT + Net Other Income
|
|
79
|
+
const ebit = calculatedSections['earningsBeforeInterestAndTax'];
|
|
80
|
+
const netOtherIncome = calculatedSections['netOtherIncome'];
|
|
81
|
+
if (ebit != null && netOtherIncome != null) {
|
|
82
|
+
calculatedSections['netIncome'] = computeCalculatedSection(ebit.classTotals, netOtherIncome.classTotals, ebit.sectionTotal, netOtherIncome.sectionTotal, 'add', SECTION_TITLES['netIncome']);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Build synthetic header COABalances for rendering class names as column headers
|
|
86
|
+
const headerBalances = buildHeaderBalances(classColumns, filter.timeframe, selectedPeriod);
|
|
87
|
+
return {
|
|
88
|
+
reportId,
|
|
89
|
+
reportTitle: 'Profit and Loss by Class',
|
|
90
|
+
firstMonthOfFY: profitAndLossClassesViewState.firstMonthOfFY,
|
|
91
|
+
fetchState: fetchState.fetchState,
|
|
92
|
+
bookCloseDate: profitAndLossClassesViewState.bookCloseDate,
|
|
93
|
+
dataAvailable: profitAndLossClassesViewState.dataAvailable,
|
|
94
|
+
selectedPeriod,
|
|
95
|
+
classColumns,
|
|
96
|
+
sections,
|
|
97
|
+
calculatedSections,
|
|
98
|
+
idsInOrder: profitAndLossHorizontalIdsInOrder,
|
|
99
|
+
uiState: profitAndLossClassesViewState.uiState,
|
|
100
|
+
headerBalances,
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Build column definitions from class hierarchy (top-level classes only).
|
|
105
|
+
*/
|
|
106
|
+
function buildClassColumns(classHierarchy, classesToFilterOut) {
|
|
107
|
+
return classHierarchy
|
|
108
|
+
.filter((c) => !classesToFilterOut.some((filteredId) => filteredId[0] === c.classId[0]))
|
|
109
|
+
.map((c) => ({
|
|
110
|
+
classId: c.classId[0],
|
|
111
|
+
className: c.className,
|
|
112
|
+
nestedClassId: c.classId,
|
|
113
|
+
qboId: c.qboId,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Pivot a section from class→account→balances to account→class→balance (single period).
|
|
118
|
+
*/
|
|
119
|
+
function pivotSectionToHorizontal(section, classColumns, title) {
|
|
120
|
+
// Map: accountId -> { accountName, classId -> balance }
|
|
121
|
+
const accountMap = new Map();
|
|
122
|
+
// Also track class totals
|
|
123
|
+
const classTotalMap = new Map();
|
|
124
|
+
classColumns.forEach((col) => classTotalMap.set(col.classId, 0));
|
|
125
|
+
// Iterate over each class in the section
|
|
126
|
+
section.classes.forEach((classReport) => {
|
|
127
|
+
const classId = classReport.id[0];
|
|
128
|
+
// Only process classes that are in our columns
|
|
129
|
+
if (!classColumns.some((col) => col.classId === classId)) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Sum all sliced period balances (same slice as vertical columns; multi-period = range total)
|
|
133
|
+
const classBalance = sumBalancesInSlice(classReport.childrenBalancesTotal.balances);
|
|
134
|
+
classTotalMap.set(classId, classBalance);
|
|
135
|
+
// Iterate through accounts in this class
|
|
136
|
+
collectAccountsFromClass(classReport, classId, accountMap);
|
|
137
|
+
});
|
|
138
|
+
// Build account rows
|
|
139
|
+
const accounts = [];
|
|
140
|
+
accountMap.forEach((accountData, accountId) => {
|
|
141
|
+
const classBalances = classColumns.map((col) => ({
|
|
142
|
+
classId: col.classId,
|
|
143
|
+
balance: toAmountWC(accountData.classBalances.get(col.classId) ?? 0, DEFAULT_CURRENCY),
|
|
144
|
+
}));
|
|
145
|
+
const rowTotal = classBalances.reduce((sum, cb) => sum + cb.balance.amount, 0);
|
|
146
|
+
accounts.push({
|
|
147
|
+
accountId,
|
|
148
|
+
accountName: accountData.accountName,
|
|
149
|
+
classBalances,
|
|
150
|
+
rowTotal: toAmountWC(rowTotal, DEFAULT_CURRENCY),
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
// Build class totals
|
|
154
|
+
const classTotals = classColumns.map((col) => ({
|
|
155
|
+
classId: col.classId,
|
|
156
|
+
balance: toAmountWC(classTotalMap.get(col.classId) ?? 0, DEFAULT_CURRENCY),
|
|
157
|
+
}));
|
|
158
|
+
const sectionTotal = classTotals.reduce((sum, ct) => sum + ct.balance.amount, 0);
|
|
159
|
+
return {
|
|
160
|
+
title,
|
|
161
|
+
accounts,
|
|
162
|
+
classTotals,
|
|
163
|
+
sectionTotal: toAmountWC(sectionTotal, DEFAULT_CURRENCY),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Recursively collect account balances from a NestedClassReportV2 into accountMap.
|
|
168
|
+
*/
|
|
169
|
+
function collectAccountsFromClass(classReport, classId, accountMap) {
|
|
170
|
+
// Process accounts directly on this class
|
|
171
|
+
classReport.class.accounts.forEach((nestedAccount) => {
|
|
172
|
+
collectAccountsFromNestedAccount(nestedAccount, classId, accountMap);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Recursively collect accounts from NestedAccountReport (handles sub-accounts).
|
|
177
|
+
*/
|
|
178
|
+
function collectAccountsFromNestedAccount(nestedAccount, classId, accountMap) {
|
|
179
|
+
const accountId = nestedAccount.account.accountId;
|
|
180
|
+
const accountName = nestedAccount.account.accountName;
|
|
181
|
+
const balance = sumBalancesInSlice(nestedAccount.account.balancesInTimeframe.balances);
|
|
182
|
+
if (!accountMap.has(accountId)) {
|
|
183
|
+
accountMap.set(accountId, {
|
|
184
|
+
accountName,
|
|
185
|
+
classBalances: new Map(),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const accountData = accountMap.get(accountId);
|
|
189
|
+
if (accountData == null) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const existing = accountData.classBalances.get(classId) ?? 0;
|
|
193
|
+
accountData.classBalances.set(classId, existing + balance);
|
|
194
|
+
// Process sub-accounts
|
|
195
|
+
nestedAccount.children.forEach((child) => {
|
|
196
|
+
collectAccountsFromNestedAccount(child, classId, accountMap);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Total amount for the current report slice. `balances` is already narrowed by
|
|
201
|
+
* `getCOABalances` / `sliceCOABalances` to the selected range; summing matches
|
|
202
|
+
* multi-period selections and avoids mismatches when `thisPeriod` does not
|
|
203
|
+
* align with a row (e.g. slice fallback) or when several period rows are shown.
|
|
204
|
+
*/
|
|
205
|
+
function sumBalancesInSlice(balances) {
|
|
206
|
+
let sum = 0;
|
|
207
|
+
for (const bal of balances) {
|
|
208
|
+
const b = bal;
|
|
209
|
+
sum += b.balance?.amount ?? 0;
|
|
210
|
+
}
|
|
211
|
+
return sum;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Compute a calculated section by adding or subtracting two sets of class totals.
|
|
215
|
+
*/
|
|
216
|
+
function computeCalculatedSection(aTotals, bTotals, aTotal, bTotal, operation, title) {
|
|
217
|
+
const classTotals = aTotals.map((a) => {
|
|
218
|
+
const bMatch = bTotals.find((b) => b.classId === a.classId);
|
|
219
|
+
const bAmount = bMatch?.balance.amount ?? 0;
|
|
220
|
+
const resultAmount = operation === 'add'
|
|
221
|
+
? a.balance.amount + bAmount
|
|
222
|
+
: a.balance.amount - bAmount;
|
|
223
|
+
return {
|
|
224
|
+
classId: a.classId,
|
|
225
|
+
balance: toAmountWC(resultAmount, DEFAULT_CURRENCY),
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
const sectionTotalAmount = operation === 'add'
|
|
229
|
+
? aTotal.amount + bTotal.amount
|
|
230
|
+
: aTotal.amount - bTotal.amount;
|
|
231
|
+
return {
|
|
232
|
+
title,
|
|
233
|
+
classTotals,
|
|
234
|
+
sectionTotal: toAmountWC(sectionTotalAmount, DEFAULT_CURRENCY),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Build synthetic COABalance array for header rendering.
|
|
239
|
+
* One entry per class column + one for "Total".
|
|
240
|
+
*/
|
|
241
|
+
function buildHeaderBalances(classColumns, timeframe, selectedPeriod) {
|
|
242
|
+
if (selectedPeriod == null) {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
const { endDate, startDate } = selectedPeriod;
|
|
246
|
+
const balances = classColumns.map((col) => ({
|
|
247
|
+
balance: toAmountWC(0, DEFAULT_CURRENCY),
|
|
248
|
+
balanceTimeFrame: timeframe,
|
|
249
|
+
endDate,
|
|
250
|
+
startDate,
|
|
251
|
+
type: 'default',
|
|
252
|
+
label: col.className,
|
|
253
|
+
}));
|
|
254
|
+
// Add a "Total" column
|
|
255
|
+
balances.push({
|
|
256
|
+
balance: toAmountWC(0, DEFAULT_CURRENCY),
|
|
257
|
+
balanceTimeFrame: timeframe,
|
|
258
|
+
endDate,
|
|
259
|
+
startDate,
|
|
260
|
+
type: 'default',
|
|
261
|
+
label: 'Total',
|
|
262
|
+
});
|
|
263
|
+
return balances;
|
|
264
|
+
}
|
|
265
|
+
function reduceFetchState(fetchStateList) {
|
|
266
|
+
const isAnyProgress = fetchStateList.some((s) => s.fetchState === 'In-Progress');
|
|
267
|
+
const isAllCompleted = fetchStateList.every((s) => s.fetchState === 'Completed');
|
|
268
|
+
const isAnyError = fetchStateList.some((s) => s.fetchState === 'Error');
|
|
269
|
+
const error = fetchStateList.find((s) => s.error != null)?.error;
|
|
270
|
+
let fetchState = 'Not-Started';
|
|
271
|
+
if (isAnyProgress) {
|
|
272
|
+
fetchState = 'In-Progress';
|
|
273
|
+
}
|
|
274
|
+
else if (isAllCompleted) {
|
|
275
|
+
fetchState = 'Completed';
|
|
276
|
+
}
|
|
277
|
+
else if (isAnyError) {
|
|
278
|
+
fetchState = 'Error';
|
|
279
|
+
}
|
|
280
|
+
return { fetchState, error };
|
|
281
|
+
}
|
package/lib/esm/view/profitAndLossClassesView/profitAndLossClassesByClassHorizontalSelectorTypes.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|