@zeniai/client-epic-state 5.3.27 → 5.3.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/lib/entity/approvalRule/approvalRuleSelector.d.ts +8 -1
  2. package/lib/entity/approvalRule/approvalRuleSelector.js +12 -0
  3. package/lib/entity/approvalRule/attributeApprovers.d.ts +26 -0
  4. package/lib/entity/approvalRule/attributeApprovers.js +39 -0
  5. package/lib/entity/vendor/vendorPayload.js +1 -1
  6. package/lib/entity/vendor/vendorState.d.ts +6 -1
  7. package/lib/esm/entity/approvalRule/approvalRuleSelector.js +11 -0
  8. package/lib/esm/entity/approvalRule/attributeApprovers.js +35 -0
  9. package/lib/esm/entity/vendor/vendorPayload.js +1 -1
  10. package/lib/esm/index.js +2 -1
  11. package/lib/esm/view/commonVendorView/transactionVendorView/transactionVendorLocalDataHelper.js +1 -1
  12. package/lib/esm/view/commonVendorView/vendorView/epics/vendorDetailLocalDataHelpers.js +1 -1
  13. package/lib/esm/view/commonVendorView/vendorsTabVendorView/epics/vendorsTabVendorDetailLocalDataHelpers.js +1 -1
  14. package/lib/esm/view/spendManagement/billPay/billDetailView/billDetailViewSelector.js +1 -1
  15. package/lib/esm/view/spendManagement/billPay/billPaySetupApproverView/billPaySetupApproverViewSelector.js +30 -16
  16. package/lib/esm/view/spendManagement/billPay/editBillView/editBillViewSelector.js +6 -0
  17. package/lib/esm/view/spendManagement/reimbursement/editRemiView/editRemiViewSelector.js +6 -1
  18. package/lib/esm/view/spendManagement/reimbursement/editRemiView/epics/fetchEditRemiDetailPageEpic.js +6 -0
  19. package/lib/esm/view/spendManagement/reimbursement/remiSetupApproverView/epic/fetchRemiApproversListEpic.js +7 -3
  20. package/lib/esm/view/spendManagement/reimbursement/remiSetupApproverView/remiSetupApproverViewReducer.js +7 -1
  21. package/lib/esm/view/spendManagement/reimbursement/remiSetupApproverView/remiSetupApproverViewSelector.js +31 -17
  22. package/lib/index.d.ts +2 -1
  23. package/lib/index.js +30 -28
  24. package/lib/view/commonVendorView/transactionVendorView/transactionVendorLocalDataHelper.js +1 -1
  25. package/lib/view/commonVendorView/vendorView/epics/vendorDetailLocalDataHelpers.js +1 -1
  26. package/lib/view/commonVendorView/vendorsTabVendorView/epics/vendorsTabVendorDetailLocalDataHelpers.js +1 -1
  27. package/lib/view/spendManagement/billPay/billDetailView/billDetailViewSelector.js +1 -1
  28. package/lib/view/spendManagement/billPay/billPaySetupApproverView/billPaySetupApproverViewSelector.js +29 -15
  29. package/lib/view/spendManagement/billPay/billPaySetupApproverView/billPaySetupApproverViewSelectorTypes.d.ts +1 -0
  30. package/lib/view/spendManagement/billPay/editBillView/editBillViewSelector.d.ts +7 -0
  31. package/lib/view/spendManagement/billPay/editBillView/editBillViewSelector.js +6 -0
  32. package/lib/view/spendManagement/reimbursement/editRemiView/editRemiViewSelector.d.ts +8 -0
  33. package/lib/view/spendManagement/reimbursement/editRemiView/editRemiViewSelector.js +6 -1
  34. package/lib/view/spendManagement/reimbursement/editRemiView/epics/fetchEditRemiDetailPageEpic.d.ts +2 -1
  35. package/lib/view/spendManagement/reimbursement/editRemiView/epics/fetchEditRemiDetailPageEpic.js +6 -0
  36. package/lib/view/spendManagement/reimbursement/remiSetupApproverView/epic/fetchRemiApproversListEpic.js +7 -3
  37. package/lib/view/spendManagement/reimbursement/remiSetupApproverView/remiSetupApproverViewReducer.d.ts +2 -2
  38. package/lib/view/spendManagement/reimbursement/remiSetupApproverView/remiSetupApproverViewReducer.js +7 -1
  39. package/lib/view/spendManagement/reimbursement/remiSetupApproverView/remiSetupApproverViewSelector.js +30 -16
  40. package/lib/view/spendManagement/reimbursement/remiSetupApproverView/remiSetupApproverViewSelectorTypes.d.ts +1 -0
  41. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { ID } from '../../commonStateTypes/common';
2
2
  import { EntityType } from '../entityApprovalStatus/entityApprovalStatusState';
3
- import { AmountCriteria, ApprovalRule, ApprovalRuleState, Criteria, DepartmentCriteria, VendorCriteria } from './approvalRuleState';
3
+ import { Actor, AmountCriteria, ApprovalRule, ApprovalRuleState, Criteria, DepartmentCriteria, Step, VendorCriteria } from './approvalRuleState';
4
4
  export declare function getApprovalRuleById(approvalRuleState: ApprovalRuleState, approvalRuleId: ID): ApprovalRule | undefined;
5
5
  export declare function getApprovalRulesByIds(approvalRuleState: ApprovalRuleState, approvalRuleIds: ID[]): ApprovalRule[];
6
6
  /**
@@ -26,3 +26,10 @@ export declare function getApprovalRulesByEntityType(approvalRuleState: Approval
26
26
  * list page and is used to handle bills/reimbursements that match no other rule.
27
27
  */
28
28
  export declare function getFallbackApprovalRule(approvalRuleState: ApprovalRuleState, entityType: EntityType): ApprovalRule | undefined;
29
+ /**
30
+ * Non-user approvers to show for a fallback-rule step in the draft approval
31
+ * preview. Implicit actors (e.g. the Admin the platform adds) are shown only
32
+ * when the step has no explicit approver, so the step always has someone to
33
+ * approve it without listing Admin next to an explicit approver.
34
+ */
35
+ export declare function getFallbackStepNonUserActors(step: Step): Actor[];
@@ -7,6 +7,7 @@ exports.getVendorCriteria = getVendorCriteria;
7
7
  exports.getDepartmentCriteria = getDepartmentCriteria;
8
8
  exports.getApprovalRulesByEntityType = getApprovalRulesByEntityType;
9
9
  exports.getFallbackApprovalRule = getFallbackApprovalRule;
10
+ exports.getFallbackStepNonUserActors = getFallbackStepNonUserActors;
10
11
  function getApprovalRuleById(approvalRuleState, approvalRuleId) {
11
12
  return approvalRuleState.approvalRuleById[approvalRuleId] ?? undefined;
12
13
  }
@@ -51,3 +52,14 @@ function getApprovalRulesByEntityType(approvalRuleState, entityType) {
51
52
  function getFallbackApprovalRule(approvalRuleState, entityType) {
52
53
  return Object.values(approvalRuleState.approvalRuleById).find((rule) => rule.entityType === entityType && rule.isFallback === true);
53
54
  }
55
+ /**
56
+ * Non-user approvers to show for a fallback-rule step in the draft approval
57
+ * preview. Implicit actors (e.g. the Admin the platform adds) are shown only
58
+ * when the step has no explicit approver, so the step always has someone to
59
+ * approve it without listing Admin next to an explicit approver.
60
+ */
61
+ function getFallbackStepNonUserActors(step) {
62
+ const hasExplicitApprover = step.actors.some((actor) => actor.isImplicitActor === false);
63
+ return step.actors.filter((actor) => actor.type !== 'user' &&
64
+ (!hasExplicitApprover || actor.isImplicitActor === false));
65
+ }
@@ -0,0 +1,26 @@
1
+ import { ID } from '../../commonStateTypes/common';
2
+ import { User, UserState } from '../user/userState';
3
+ import { AttributeType } from './approvalRuleState';
4
+ /**
5
+ * Who a user-attribute approver role (vendor owner, manager, manager's
6
+ * manager) resolves to, for the draft approval preview.
7
+ *
8
+ * Per role:
9
+ * - `User` → resolves to this person.
10
+ * - `null` → known to resolve to no one; the backend routes the step to
11
+ * Admin, so the preview shows "Admin".
12
+ * - absent → not known yet (e.g. no vendor picked, or the mapping was not
13
+ * fetched); the preview keeps the role label.
14
+ */
15
+ export type AttributeApprovers = Partial<Record<AttributeType, User | null>>;
16
+ /**
17
+ * @param vendorOwnerId `undefined` = no vendor selected (unknown),
18
+ * `null` = vendor selected but it has no owner, otherwise the owner's id.
19
+ * @param attributeUserIdMapping backend `attribute_sub_type_to_user_id_mapping`
20
+ * for the submitter; `undefined` when it was not fetched (unknown). When
21
+ * present, a missing manager / manager_of_manager key means "no one".
22
+ */
23
+ export declare const getAttributeApprovers: (userState: UserState, { vendorOwnerId, attributeUserIdMapping, }: {
24
+ attributeUserIdMapping?: Partial<Record<AttributeType, ID>>;
25
+ vendorOwnerId?: ID | null;
26
+ }) => AttributeApprovers;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getAttributeApprovers = void 0;
4
+ const userSelector_1 = require("../user/userSelector");
5
+ const toApprover = (userState, userId) => {
6
+ if (userId === undefined) {
7
+ return undefined;
8
+ }
9
+ if (userId === null || userId === '') {
10
+ return null;
11
+ }
12
+ // An id we can't find in user state is treated as unknown rather than
13
+ // "no one" — the backend still routes to that user.
14
+ return (0, userSelector_1.getUserByUserId)(userState, userId) ?? undefined;
15
+ };
16
+ /**
17
+ * @param vendorOwnerId `undefined` = no vendor selected (unknown),
18
+ * `null` = vendor selected but it has no owner, otherwise the owner's id.
19
+ * @param attributeUserIdMapping backend `attribute_sub_type_to_user_id_mapping`
20
+ * for the submitter; `undefined` when it was not fetched (unknown). When
21
+ * present, a missing manager / manager_of_manager key means "no one".
22
+ */
23
+ const getAttributeApprovers = (userState, { vendorOwnerId, attributeUserIdMapping, }) => {
24
+ const approvers = {};
25
+ const vendorOwner = toApprover(userState, vendorOwnerId);
26
+ if (vendorOwner !== undefined) {
27
+ approvers.vendor_owner = vendorOwner;
28
+ }
29
+ if (attributeUserIdMapping != null) {
30
+ ['manager', 'manager_of_manager'].forEach((attribute) => {
31
+ const approver = toApprover(userState, attributeUserIdMapping[attribute] ?? null);
32
+ if (approver !== undefined) {
33
+ approvers[attribute] = approver;
34
+ }
35
+ });
36
+ }
37
+ return approvers;
38
+ };
39
+ exports.getAttributeApprovers = getAttributeApprovers;
@@ -75,7 +75,7 @@ const mapVendorPayloadToVendor = (payload) => {
75
75
  : undefined,
76
76
  bankAccountIds: payload.bank_accounts?.map((bankAcc) => bankAcc.bank_account_id) ?? [],
77
77
  paymentInstrumentIds: payload.payment_instruments?.map((piAcc) => piAcc.zeni_payment_instrument_id) ?? [],
78
- ownerId: payload.owner?.user_id ?? undefined,
78
+ ownerId: payload.owner === null ? null : payload.owner?.user_id,
79
79
  balance: payload.balance ?? 0,
80
80
  matchScore: payload.match_score?.value != null ? payload.match_score.value : 0,
81
81
  isVendor1099: payload.is_vendor_1099 ?? false,
@@ -67,7 +67,12 @@ export interface Vendor extends VendorBase {
67
67
  last90DaysSpend?: number;
68
68
  lastMonthSpend?: number;
69
69
  legalName?: string;
70
- ownerId?: ID;
70
+ /**
71
+ * `null` = the vendor explicitly has no owner; `undefined` = not known (the
72
+ * payload didn't include `owner`). The approval preview only shows Admin for
73
+ * `null`.
74
+ */
75
+ ownerId?: ID | null;
71
76
  preferredInternationalTransferFeeBearerPaymentMethod?: InternationalTransferFeeBearerPaymentMethod;
72
77
  previousYearSpend?: number;
73
78
  shippingAddressId?: ID;
@@ -42,3 +42,14 @@ export function getApprovalRulesByEntityType(approvalRuleState, entityType) {
42
42
  export function getFallbackApprovalRule(approvalRuleState, entityType) {
43
43
  return Object.values(approvalRuleState.approvalRuleById).find((rule) => rule.entityType === entityType && rule.isFallback === true);
44
44
  }
45
+ /**
46
+ * Non-user approvers to show for a fallback-rule step in the draft approval
47
+ * preview. Implicit actors (e.g. the Admin the platform adds) are shown only
48
+ * when the step has no explicit approver, so the step always has someone to
49
+ * approve it without listing Admin next to an explicit approver.
50
+ */
51
+ export function getFallbackStepNonUserActors(step) {
52
+ const hasExplicitApprover = step.actors.some((actor) => actor.isImplicitActor === false);
53
+ return step.actors.filter((actor) => actor.type !== 'user' &&
54
+ (!hasExplicitApprover || actor.isImplicitActor === false));
55
+ }
@@ -0,0 +1,35 @@
1
+ import { getUserByUserId } from '../user/userSelector';
2
+ const toApprover = (userState, userId) => {
3
+ if (userId === undefined) {
4
+ return undefined;
5
+ }
6
+ if (userId === null || userId === '') {
7
+ return null;
8
+ }
9
+ // An id we can't find in user state is treated as unknown rather than
10
+ // "no one" — the backend still routes to that user.
11
+ return getUserByUserId(userState, userId) ?? undefined;
12
+ };
13
+ /**
14
+ * @param vendorOwnerId `undefined` = no vendor selected (unknown),
15
+ * `null` = vendor selected but it has no owner, otherwise the owner's id.
16
+ * @param attributeUserIdMapping backend `attribute_sub_type_to_user_id_mapping`
17
+ * for the submitter; `undefined` when it was not fetched (unknown). When
18
+ * present, a missing manager / manager_of_manager key means "no one".
19
+ */
20
+ export const getAttributeApprovers = (userState, { vendorOwnerId, attributeUserIdMapping, }) => {
21
+ const approvers = {};
22
+ const vendorOwner = toApprover(userState, vendorOwnerId);
23
+ if (vendorOwner !== undefined) {
24
+ approvers.vendor_owner = vendorOwner;
25
+ }
26
+ if (attributeUserIdMapping != null) {
27
+ ['manager', 'manager_of_manager'].forEach((attribute) => {
28
+ const approver = toApprover(userState, attributeUserIdMapping[attribute] ?? null);
29
+ if (approver !== undefined) {
30
+ approvers[attribute] = approver;
31
+ }
32
+ });
33
+ }
34
+ return approvers;
35
+ };
@@ -69,7 +69,7 @@ export const mapVendorPayloadToVendor = (payload) => {
69
69
  : undefined,
70
70
  bankAccountIds: payload.bank_accounts?.map((bankAcc) => bankAcc.bank_account_id) ?? [],
71
71
  paymentInstrumentIds: payload.payment_instruments?.map((piAcc) => piAcc.zeni_payment_instrument_id) ?? [],
72
- ownerId: payload.owner?.user_id ?? undefined,
72
+ ownerId: payload.owner === null ? null : payload.owner?.user_id,
73
73
  balance: payload.balance ?? 0,
74
74
  matchScore: payload.match_score?.value != null ? payload.match_score.value : 0,
75
75
  isVendor1099: payload.is_vendor_1099 ?? false,
package/lib/esm/index.js CHANGED
@@ -37,6 +37,7 @@ import { getAddressByAddressId } from './entity/address/addressSelector';
37
37
  import { detectRuleOverlap, } from './entity/approvalRule/approvalRuleConflict';
38
38
  import { getAmountCriteria, getApprovalRulesByEntityType, getDepartmentCriteria, getFallbackApprovalRule, getVendorCriteria, } from './entity/approvalRule/approvalRuleSelector';
39
39
  import { toAttributeOrRoleTypeStrict, toAttributeTypeStrict, toRoleTypeStrict, } from './entity/approvalRule/approvalRuleState';
40
+ import { getAttributeApprovers, } from './entity/approvalRule/attributeApprovers';
40
41
  import { toOutsideZeniPaymentModeType, } from './entity/billPay/billTransaction/billTransactionState';
41
42
  import { toRecurringBillFrequency, toRecurringBillFrequencyStrict, } from './entity/billPay/recurringBills/recurringBillsState';
42
43
  import { toCardAddressType, toCreditLimitFrequencyCodeType, toShippingAddressType, } from './entity/chargeCard/chargeCard';
@@ -630,7 +631,7 @@ export { getUserFinancialAccount };
630
631
  export { fetchRemiSetupView, updateMileageDetails, saveRemiSetupViewDataInLocalStore, acceptRemiTerms, acceptEmployeeRemiTerms, getRemiSetupViewDetails, getRemiBusinessVerificationDetails, };
631
632
  export { VERIFIED_PAYMENT_ACCOUNT_PROVIDER_VERIFICATION_STATUS, };
632
633
  export { isSuccessResponse, isInvalidSessionError, isAccessDeniedError, };
633
- export { getAmountCriteria, getApprovalRulesByEntityType, getDepartmentCriteria, getFallbackApprovalRule, getVendorCriteria, toAttributeTypeStrict, toAttributeOrRoleTypeStrict, toRoleTypeStrict, detectRuleOverlap, };
634
+ export { getAmountCriteria, getApprovalRulesByEntityType, getDepartmentCriteria, getAttributeApprovers, getFallbackApprovalRule, getVendorCriteria, toAttributeTypeStrict, toAttributeOrRoleTypeStrict, toRoleTypeStrict, detectRuleOverlap, };
634
635
  export { getBillPaySetupApproverView, getBillPaySetupApproverUpdateDataView, fetchBillPaySetupApproverView, fetchBillPayApproversDetails, fetchBillPayApproversList, deleteBillPayApprovalRule, reorderBillPayApprovalRules, saveBillPaySetupApproverViewUpdateData, saveBillPaySetupApproverViewUpdates, setBillsSetupApproverViewListeningToPusherEvent, initializeBillPaySetupApproverViewUpdateData, clearBillPaySetupApproverViewUpdateData, };
635
636
  export { getRemiSetupApproverView, getRemiSetupApproverUpdateDataView, fetchRemiSetupApproverView, fetchRemiApproversList, fetchRemiApproversDetails, deleteRemiApprovalRule, reorderRemiApprovalRules, saveRemiSetupApproverViewUpdateData, saveRemiSetupApproverViewUpdates, initializeRemiSetupApproverViewUpdateData, setRemiSetupApproverViewListeningToPusherEvent, clearRemiSetupApproverViewUpdateData, };
636
637
  export { getRemiDetailView, checkApproveRejectBtnShowForRemi, };
@@ -3,7 +3,7 @@ export const getInitializedTransactionVendorLocalData = (vendorData) => {
3
3
  vendorName: vendorData.name,
4
4
  vendorCategory: vendorData.accountId,
5
5
  vendorClass: vendorData.classId,
6
- vendorOwner: vendorData.ownerId,
6
+ vendorOwner: vendorData.ownerId ?? undefined,
7
7
  vendorId: vendorData.id,
8
8
  };
9
9
  };
@@ -21,7 +21,7 @@ export const getInitializedVendorDetailLocalData = (vendorData, state, vendorPay
21
21
  vendorCategory: vendorData.accountId,
22
22
  vendorClass: vendorData.classId,
23
23
  vendorType: vendorData.vendorType,
24
- vendorOwner: vendorData.ownerId,
24
+ vendorOwner: vendorData.ownerId ?? undefined,
25
25
  vendorId: vendorData.id,
26
26
  isVendor1099: vendorData.isVendor1099,
27
27
  documentIds: vendorData.documentIds,
@@ -16,7 +16,7 @@ export const getInitializedVendorTabDetailLocalData = (vendorData, state, vendor
16
16
  : getContactsByIds(contactState, vendorData.contactIds ?? []),
17
17
  vendorCategory: vendorData.accountId,
18
18
  vendorClass: vendorData.classId,
19
- vendorOwner: vendorData.ownerId,
19
+ vendorOwner: vendorData.ownerId ?? undefined,
20
20
  vendorId: vendorData.id,
21
21
  recommendations: vendorData.recommendations,
22
22
  recommendedCategory: recommendedAccount,
@@ -236,7 +236,7 @@ export const getBillDetailView = (state, billId) => {
236
236
  filteredAttributes,
237
237
  approverListFetchState: approverListState.fetchStatus,
238
238
  vendorCurrency: currentVendorDetails?.vendorCurrencyCode,
239
- vendorOwnerId: currentVendorDetails?.ownerId,
239
+ vendorOwnerId: currentVendorDetails?.ownerId ?? undefined,
240
240
  billHistory: getActivityHistory(commonHistoryState, billId, 'bill_pay'),
241
241
  realTimeApproval: getActivityRealTimeApproval(realTimeApprovalState, billId, 'bill_pay'),
242
242
  allAccountingClasses,
@@ -1,7 +1,7 @@
1
1
  import { createSelector } from '@reduxjs/toolkit';
2
2
  import cloneDeep from 'lodash/cloneDeep';
3
3
  import { reduceAllFetchState, reduceAnyFetchState, } from '../../../../commonStateTypes/reduceFetchState';
4
- import { getApprovalRulesByIds } from '../../../../entity/approvalRule/approvalRuleSelector';
4
+ import { getApprovalRulesByIds, getFallbackApprovalRule, getFallbackStepNonUserActors, } from '../../../../entity/approvalRule/approvalRuleSelector';
5
5
  import { getClassesByIds } from '../../../../entity/class/classSelector';
6
6
  import { getIsAccountingClassesEnabled } from '../../../../entity/tenant/tenantSelector';
7
7
  import { getUserByUserId } from '../../../../entity/user/userSelector';
@@ -10,22 +10,35 @@ export const getBillPaySetupApproverView = createSelector((state) => state.billP
10
10
  const { approvalRulesDeleteStatusById, approvalRuleIds, approverListState, fetchState, error, listeningToPusherEvent, newApprovalRuleId, } = billPaySetupApproverViewState;
11
11
  const deleteStateList = Object.values(approvalRulesDeleteStatusById);
12
12
  const approvalRules = getApprovalRulesByIds(approvalRuleState, approvalRuleIds);
13
- const allApprovalRules = approvalRules.map((approvalRule) => {
14
- const approvalRuleWithUser = {
15
- ...approvalRule,
16
- steps: approvalRule.steps.map((step) => {
17
- return {
18
- ...step,
19
- actors: step.actors
20
- .filter((actor) => actor.userId != null)
21
- .map((actor) => getUserByUserId(userState, actor.userId))
22
- .filter((user) => user != null),
23
- nonUserActors: step.actors.filter((actor) => actor.type !== 'user' && actor.isImplicitActor === false),
24
- };
25
- }),
26
- };
27
- return approvalRuleWithUser;
13
+ const toStepWithUser = (step) => ({
14
+ ...step,
15
+ actors: step.actors
16
+ .filter((actor) => actor.userId != null)
17
+ .map((actor) => getUserByUserId(userState, actor.userId))
18
+ .filter((user) => user != null),
19
+ nonUserActors: step.actors.filter((actor) => actor.type !== 'user' && actor.isImplicitActor === false),
20
+ });
21
+ const toApprovalRuleWithUser = (approvalRule) => ({
22
+ ...approvalRule,
23
+ steps: approvalRule.steps.map(toStepWithUser),
24
+ });
25
+ // Unlike the rules page, a fallback step keeps its implicit Admin when it
26
+ // has no explicit approver (see getFallbackStepNonUserActors).
27
+ const toFallbackStepWithUser = (step) => ({
28
+ ...toStepWithUser(step),
29
+ nonUserActors: getFallbackStepNonUserActors(step),
28
30
  });
31
+ const allApprovalRules = approvalRules.map(toApprovalRuleWithUser);
32
+ // Exposed separately so the new-bill preview can show it when no other
33
+ // rule matches. (It is also in allApprovalRules; the rules pages filter it
34
+ // out by `isFallback`.)
35
+ const fallbackApprovalRule = getFallbackApprovalRule(approvalRuleState, 'bill_pay');
36
+ const fallbackRule = fallbackApprovalRule != null
37
+ ? {
38
+ ...fallbackApprovalRule,
39
+ steps: fallbackApprovalRule.steps.map(toFallbackStepWithUser),
40
+ }
41
+ : undefined;
29
42
  const approvalRulesStatus = allApprovalRules.length > 0
30
43
  ? {
31
44
  code: 'created',
@@ -40,6 +53,7 @@ export const getBillPaySetupApproverView = createSelector((state) => state.billP
40
53
  const inactiveApproverIds = Array.from(referencedApproverIds).filter((userId) => !activeApproverIds.has(userId));
41
54
  return {
42
55
  allApprovalRules,
56
+ fallbackRule,
43
57
  deleteStatus: reduceAnyFetchState(deleteStateList),
44
58
  approvalRulesStatus,
45
59
  approverListFetchStatus: approverListState.fetchStatus,
@@ -2,6 +2,7 @@ import recordGet from 'lodash/get';
2
2
  import has from 'lodash/has';
3
3
  import { reduceFetchState } from '../../../../commonStateTypes/reduceFetchState';
4
4
  import { getAmountCriteria, getDepartmentCriteria, getVendorCriteria, } from '../../../../entity/approvalRule/approvalRuleSelector';
5
+ import { getAttributeApprovers, } from '../../../../entity/approvalRule/attributeApprovers';
5
6
  import { getBankAccountByBankAccountId, getBankAccountsByBankAccountIds, } from '../../../../entity/bankAccount/bankAccountSelector';
6
7
  import { getContactById, getContactsByIds, } from '../../../../entity/billPay/contact/contactSelector';
7
8
  import { getCompanyByCompanyId } from '../../../../entity/company/companySelector';
@@ -371,6 +372,11 @@ export const getEditBillDetail = (state, isVendorRecommendationFeatureEnabled, i
371
372
  qboId,
372
373
  payNowCutOffTime,
373
374
  approvalRules: getBillPaySetupApproverView(state),
375
+ attributeApprovers: getAttributeApprovers(userState, {
376
+ // Only an explicit "no owner" (null) becomes Admin; an owner that wasn't
377
+ // in the vendor payload stays unknown and keeps the role label.
378
+ vendorOwnerId: vendorDetails?.ownerId,
379
+ }),
374
380
  billPayInfo,
375
381
  billHistory,
376
382
  toAccountVendorEmail,
@@ -1,5 +1,6 @@
1
1
  import recordGet from 'lodash/get';
2
2
  import { reduceFetchState } from '../../../../commonStateTypes/reduceFetchState';
3
+ import { getAttributeApprovers, } from '../../../../entity/approvalRule/attributeApprovers';
3
4
  import { getBankAccountByBankAccountId } from '../../../../entity/bankAccount/bankAccountSelector';
4
5
  import { getAllCurrencies } from '../../../../entity/currency/currencySelector';
5
6
  import { getCurrentTenant, getIsAccountingClassesEnabled, } from '../../../../entity/tenant/tenantSelector';
@@ -16,7 +17,7 @@ import { REIMBURSEMENT_PAYMENT_NUMBER_OF_DAYS } from '../reimbursementConfig/rei
16
17
  import { getRemiActivities } from '../remiDetailView/remiDetailViewSelector';
17
18
  import { getRemiSetupApproverView } from '../remiSetupApproverView/remiSetupApproverViewSelector';
18
19
  export const getEditRemiDetail = (state, reimbursementId) => {
19
- const { accountListState, accountState, remiDetailViewState, reimbursementState, currencyState, classListState, classState, editRemiDetailViewState, reimbursementConfigState, merchantListState, merchantState, userState, bankAccountState, commonHistoryState, } = state;
20
+ const { accountListState, accountState, remiDetailViewState, reimbursementState, currencyState, classListState, classState, editRemiDetailViewState, reimbursementConfigState, merchantListState, merchantState, userState, bankAccountState, commonHistoryState, remiSetupApproverViewState, } = state;
20
21
  const achSameDayMaxDays = getReimbursementConfigByKey(reimbursementConfigState, 'reimbursement_ach_same-day')?.maxPeriod.value ?? REIMBURSEMENT_PAYMENT_NUMBER_OF_DAYS;
21
22
  const sourceRemiDetail = reimbursementId != null
22
23
  ? editRemiDetailViewState.editRemiStateById?.[reimbursementId]
@@ -187,6 +188,10 @@ export const getEditRemiDetail = (state, reimbursementId) => {
187
188
  classHierarchyList,
188
189
  createTime,
189
190
  approvalRules: getRemiSetupApproverView(state, false),
191
+ attributeApprovers: getAttributeApprovers(userState, {
192
+ attributeUserIdMapping: remiSetupApproverViewState.approverListState
193
+ .attributeSubTypeToUserIdMapping,
194
+ }),
190
195
  reimbursementInfo,
191
196
  paymentToBankAccountDetail,
192
197
  remiHistory: getActivityHistory(commonHistoryState, reimbursementId ?? '', 'reimbursement'),
@@ -6,6 +6,7 @@ import { fetchUserFinancialAccount } from '../../../../userFinancialAccount/user
6
6
  import { getLatestCreatedUserBankAccount } from '../../../../userFinancialAccount/userFinancialAccountSelector';
7
7
  import { fetchMerchantList } from '../../../merchantList/merchantListReducer';
8
8
  import { fetchReimbursementConfig } from '../../reimbursementConfig/reimbursementConfigReducer';
9
+ import { fetchRemiApproversList } from '../../remiSetupApproverView/remiSetupApproverViewReducer';
9
10
  import { fetchEditRemiDetailPage, fetchRemiAndInitializeLocalStore, updatesPaymentToInLocalStore, } from '../editRemiViewReducer';
10
11
  export const fetchEditRemiDetailPageEpic = (actions$, state$) => actions$.pipe(filter(fetchEditRemiDetailPage.match), mergeMap((action) => {
11
12
  const { cacheOverride, isLimitedCategoryEnabled, reimbursementId, isShowDeletedReimbursementEnabled, } = action.payload;
@@ -59,5 +60,10 @@ export const fetchEditRemiDetailPageEpic = (actions$, state$) => actions$.pipe(f
59
60
  remiDetailActions.push(fetchRemiAndInitializeLocalStore(reimbursementId, false, isShowDeletedReimbursementEnabled));
60
61
  }
61
62
  }
63
+ // New reimbursement: resolve the submitter's manager / manager's manager
64
+ // so the approval preview can name them (or show Admin when there is none).
65
+ if (reimbursementId == null && userId !== '') {
66
+ remiDetailActions.push(fetchRemiApproversList(true, false, true, userId));
67
+ }
62
68
  return concat(from(remiDetailActions));
63
69
  }));
@@ -8,7 +8,11 @@ export const fetchRemiApproversListEpic = (actions$, state$, zeniAPI) => actions
8
8
  const { cacheOverride, includeUserRoles, includeAttributesUserIds, lastUpdatedByUserId, } = action.payload;
9
9
  const { remiSetupApproverViewState } = state$.value;
10
10
  const approverIds = remiSetupApproverViewState.approverListState.approverIds;
11
- if (cacheOverride === true || approverIds.length === 0) {
11
+ // The attribute mapping depends on `lastUpdatedByUserId`, so it can't be
12
+ // served from the cache.
13
+ if (cacheOverride === true ||
14
+ approverIds.length === 0 ||
15
+ includeAttributesUserIds === true) {
12
16
  return zeniAPI
13
17
  .getJSON(`${zeniAPI.apiEndPoints.approvalMicroServiceBaseUrl}/1.0/reimbursements/approvers?query={"is_include_user_roles_info":${includeUserRoles},"is_include_attribute_user_ids":${includeAttributesUserIds},"last_updated_by_user_id":"${lastUpdatedByUserId}"}`)
14
18
  .pipe(mergeMap((response) => {
@@ -30,7 +34,7 @@ export const fetchRemiApproversListEpic = (actions$, state$, zeniAPI) => actions
30
34
  }
31
35
  else {
32
36
  const { approverListState } = remiSetupApproverViewState;
33
- const { approverIds, attributes } = approverListState;
34
- return of(fetchRemiApproversListSuccess(approverIds, attributes));
37
+ const { approverIds, attributes, attributeSubTypeToUserIdMapping } = approverListState;
38
+ return of(fetchRemiApproversListSuccess(approverIds, attributes, attributeSubTypeToUserIdMapping));
35
39
  }
36
40
  }));
@@ -84,11 +84,17 @@ const remiSetupApproverView = createSlice({
84
84
  },
85
85
  },
86
86
  fetchRemiApproversList: {
87
- reducer(draft) {
87
+ reducer(draft, action) {
88
88
  draft.approverListState.fetchStatus = {
89
89
  fetchState: 'In-Progress',
90
90
  error: undefined,
91
91
  };
92
+ // The attribute mapping is specific to `lastUpdatedByUserId`; drop the
93
+ // previous user's mapping so it is never shown for someone else while
94
+ // the new one loads.
95
+ if (action.payload.includeAttributesUserIds === true) {
96
+ draft.approverListState.attributeSubTypeToUserIdMapping = undefined;
97
+ }
92
98
  },
93
99
  prepare(cacheOverride = false, includeUserRoles = true, includeAttributesUserIds = false, lastUpdatedByUserId = undefined) {
94
100
  return {
@@ -1,7 +1,7 @@
1
1
  import { createSelector } from '@reduxjs/toolkit';
2
2
  import cloneDeep from 'lodash/cloneDeep';
3
3
  import { reduceAllFetchState, reduceAnyFetchState, } from '../../../../commonStateTypes/reduceFetchState';
4
- import { getApprovalRulesByIds } from '../../../../entity/approvalRule/approvalRuleSelector';
4
+ import { getApprovalRulesByIds, getFallbackApprovalRule, getFallbackStepNonUserActors, } from '../../../../entity/approvalRule/approvalRuleSelector';
5
5
  import { getClassesByIds } from '../../../../entity/class/classSelector';
6
6
  import { getUserByUserId } from '../../../../entity/user/userSelector';
7
7
  import { getUserRoleByUserId } from '../../../../entity/userRole/userRoleSelector';
@@ -10,23 +10,36 @@ export const getRemiSetupApproverView = (state, filterAdmins = true) => {
10
10
  const { approvalRulesDeleteStatusById, approvalRuleIds, approverListState, fetchState, error, listeningToPusherEvent, newApprovalRuleId, } = remiSetupApproverViewState;
11
11
  const deleteStateList = Object.values(approvalRulesDeleteStatusById);
12
12
  const approvalRules = getApprovalRulesByIds(approvalRuleState, approvalRuleIds);
13
- const allApprovalRules = approvalRules.map((approvalRule) => {
14
- const approvalRuleWithUser = {
15
- ...approvalRule,
16
- steps: approvalRule.steps.map((step) => {
17
- return {
18
- ...step,
19
- actors: step.actors
20
- .filter((actor) => actor.userId != null)
21
- .map((actor) => getUserByUserId(userState, actor.userId))
22
- .filter((user) => user != null),
23
- nonUserActors: step.actors.filter((actor) => actor.type !== 'user' &&
24
- (filterAdmins ? actor.isImplicitActor === false : true)),
25
- };
26
- }),
27
- };
28
- return approvalRuleWithUser;
13
+ const toStepWithUser = (step) => ({
14
+ ...step,
15
+ actors: step.actors
16
+ .filter((actor) => actor.userId != null)
17
+ .map((actor) => getUserByUserId(userState, actor.userId))
18
+ .filter((user) => user != null),
19
+ nonUserActors: step.actors.filter((actor) => actor.type !== 'user' &&
20
+ (filterAdmins ? actor.isImplicitActor === false : true)),
21
+ });
22
+ const toApprovalRuleWithUser = (approvalRule) => ({
23
+ ...approvalRule,
24
+ steps: approvalRule.steps.map(toStepWithUser),
29
25
  });
26
+ const allApprovalRules = approvalRules.map(toApprovalRuleWithUser);
27
+ // Exposed separately so the new-reimbursement preview can show it when no other
28
+ // rule matches. (It is also in allApprovalRules; the rules pages filter it
29
+ // out by `isFallback`.)
30
+ const fallbackApprovalRule = getFallbackApprovalRule(approvalRuleState, 'reimbursement');
31
+ // A fallback step shows its implicit Admin only when it has no explicit
32
+ // approver, regardless of `filterAdmins` (same as bills; see
33
+ // getFallbackStepNonUserActors).
34
+ const fallbackRule = fallbackApprovalRule != null
35
+ ? {
36
+ ...fallbackApprovalRule,
37
+ steps: fallbackApprovalRule.steps.map((step) => ({
38
+ ...toStepWithUser(step),
39
+ nonUserActors: getFallbackStepNonUserActors(step),
40
+ })),
41
+ }
42
+ : undefined;
30
43
  const approvalRulesStatus = allApprovalRules.length > 0
31
44
  ? {
32
45
  code: 'created',
@@ -41,6 +54,7 @@ export const getRemiSetupApproverView = (state, filterAdmins = true) => {
41
54
  const inactiveApproverIds = Array.from(referencedApproverIds).filter((userId) => !activeApproverIds.has(userId));
42
55
  return {
43
56
  allApprovalRules,
57
+ fallbackRule,
44
58
  deleteStatus: reduceAnyFetchState(deleteStateList),
45
59
  approvalRulesStatus,
46
60
  approverListFetchStatus: approverListState.fetchStatus,
package/lib/index.d.ts CHANGED
@@ -62,6 +62,7 @@ import { DetectRuleOverlapInput, RuleForOverlapCheck, RuleOverlap, detectRuleOve
62
62
  import { getAmountCriteria, getApprovalRulesByEntityType, getDepartmentCriteria, getFallbackApprovalRule, getVendorCriteria } from './entity/approvalRule/approvalRuleSelector';
63
63
  import { Actor, ActorType, AmountComparator, AmountCriteria, ApprovalRule, ApprovalRuleState, Approvers, AttributeType, ConditionType, Criteria, DepartmentCriteria, RoleType, Step, VendorCriteria, toAttributeOrRoleTypeStrict, toAttributeTypeStrict, toRoleTypeStrict } from './entity/approvalRule/approvalRuleState';
64
64
  import { ApprovalActionType, StepOperatorType } from './entity/approvalRule/approvalStepTypes';
65
+ import { AttributeApprovers, getAttributeApprovers } from './entity/approvalRule/attributeApprovers';
65
66
  import { BankAccount } from './entity/bankAccount/bankAccount';
66
67
  import { BillPayInfo, BillPaymentRefundStatus, BillPaymentRefundStatusCodeType, BillPaymentStatus, BillPaymentStatusCodeType, BillStage, BillStageCodeType, BillStatus, BillStatusCodeType, BillTransaction, DeletionSourceType, toOutsideZeniPaymentModeType } from './entity/billPay/billTransaction/billTransactionState';
67
68
  import { InternationalTransferFeeBearerPaymentMethod } from './entity/billPay/billTransaction/internationalTransferFeeBearerPaymentMethod';
@@ -901,7 +902,7 @@ export { RemiSetupViewState, RemiSetupViewLocalData, MileageDetailsLocalData, Mi
901
902
  export { PaymentAccount, Logo, VERIFIED_PAYMENT_ACCOUNT_PROVIDER_VERIFICATION_STATUS, };
902
903
  export { SubscriptionPaymentAccount };
903
904
  export { PreviousBillsSelectorView, InvoiceProcessingPayload, InvoiceProcessingResponse, isSuccessResponse, isInvalidSessionError, isAccessDeniedError, };
904
- export { Step, Actor as ApprovalRuleActor, ActorType, ApprovalActionType, StepOperatorType, ApprovalRuleState, ApprovalRule, AmountComparator, AmountCriteria, ConditionType, Criteria, DepartmentCriteria, VendorCriteria, AttributeType, Approvers, RoleType, EntityApprovalStatusState, EntityApprovalStatus, SpendManagementEntityType, SpendManagementEntityTypeWithOtherConnection, getAmountCriteria, getApprovalRulesByEntityType, getDepartmentCriteria, getFallbackApprovalRule, getVendorCriteria, toAttributeTypeStrict, toAttributeOrRoleTypeStrict, toRoleTypeStrict, DetectRuleOverlapInput, RuleForOverlapCheck, RuleOverlap, detectRuleOverlap, };
905
+ export { Step, Actor as ApprovalRuleActor, ActorType, ApprovalActionType, StepOperatorType, ApprovalRuleState, ApprovalRule, AmountComparator, AmountCriteria, ConditionType, Criteria, DepartmentCriteria, VendorCriteria, AttributeType, Approvers, RoleType, EntityApprovalStatusState, EntityApprovalStatus, SpendManagementEntityType, SpendManagementEntityTypeWithOtherConnection, getAmountCriteria, getApprovalRulesByEntityType, getDepartmentCriteria, AttributeApprovers, getAttributeApprovers, getFallbackApprovalRule, getVendorCriteria, toAttributeTypeStrict, toAttributeOrRoleTypeStrict, toRoleTypeStrict, DetectRuleOverlapInput, RuleForOverlapCheck, RuleOverlap, detectRuleOverlap, };
905
906
  export { ApprovalRuleUpdateData, ApprovalRuleCreateData, BillPayApprovalRuleWithUser, ApprovalUpdateActionType, ApproverViewUpdateData, BillPaySetupApproverViewState, BillPaySetupApproverView, BillPaySetupApproverUpdateDataView, getBillPaySetupApproverView, getBillPaySetupApproverUpdateDataView, fetchBillPaySetupApproverView, fetchBillPayApproversDetails, fetchBillPayApproversList, deleteBillPayApprovalRule, reorderBillPayApprovalRules, saveBillPaySetupApproverViewUpdateData, saveBillPaySetupApproverViewUpdates, setBillsSetupApproverViewListeningToPusherEvent, initializeBillPaySetupApproverViewUpdateData, clearBillPaySetupApproverViewUpdateData, };
906
907
  export { RemiApprovalRuleWithUser, RemiSetupApproverViewState, RemiSetupApproverView, RemiSetupApproverUpdateDataView, getRemiSetupApproverView, getRemiSetupApproverUpdateDataView, fetchRemiSetupApproverView, fetchRemiApproversList, fetchRemiApproversDetails, deleteRemiApprovalRule, reorderRemiApprovalRules, saveRemiSetupApproverViewUpdateData, saveRemiSetupApproverViewUpdates, initializeRemiSetupApproverViewUpdateData, setRemiSetupApproverViewListeningToPusherEvent, clearRemiSetupApproverViewUpdateData, };
907
908
  export { RemiDetailViewSelector, getRemiDetailView, RemiActivity, RemiDetails, checkApproveRejectBtnShowForRemi, };