Skip to content

Commit

Permalink
Merge branch 'main' into fix-split-button-text-switching-briefly
Browse files Browse the repository at this point in the history
  • Loading branch information
FitseTLT committed Nov 14, 2024
2 parents 79e1ccc + b7f466a commit b54c0fb
Show file tree
Hide file tree
Showing 20 changed files with 603 additions and 150 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
title: Adding Attendees
description: How to add attendees to your expenses
---
<!-- The lines above are required by Jekyll to process the .md file -->

# Overview

Expensify allows you to add attendees when you want to associate specific individuals with an expense.

# How to add attendees to an expense

1. Click on the Attendees caret at the bottom of the Request page to expand attendee options.
2. Select the attendees you wish to add from Recents, Contacts, or use the search bar to find specific individuals.
3. If you’re adding external contacts, manually enter their information, and they’ll be saved under Contacts for future requests.
4. Click on each attendee to add them. Selected attendees will display a check mark.

# How to resolve category limit errors by adding attendees

Sometimes, an expense will exceed the category limit set by an Expensify admin, because the amount exceeded the category limit for that expense category. To resolve this error, you can add attendees to the expense.

1. Click the Attendees caret and add attendees to help resolve category limit violations by evenly distributing the total amount across all attendees.
2. Once attendees are added, a dot separator will appear between the Request attendees field title and the amount per person, making the allocation easy to review for approvers.

{% include faq-begin.md %}

## Does Expensify create an audit trail for the addition of attendees to expenses?

Yes, every time an attendee is added or removed from the request, a system message will automatically record the change on the expense report containing the expense.

System messages for attendee changes cannot be edited, ensuring an accurate history of updates to the request.
33 changes: 28 additions & 5 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {TranslationPaths} from '@src/languages/types';
import type {OnyxValues} from '@src/ONYXKEYS';
import ONYXKEYS from '@src/ONYXKEYS';
import type {BankAccountList, Card, CardFeeds, CardList, CompanyCardFeed, PersonalDetailsList, WorkspaceCardsList} from '@src/types/onyx';
import type {CardFeedData} from '@src/types/onyx/CardFeeds';
import type {CompanyCardNicknames, CompanyFeeds} from '@src/types/onyx/CardFeeds';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type IconAsset from '@src/types/utils/IconAsset';
import localeCompare from './LocaleCompare';
Expand Down Expand Up @@ -243,10 +243,20 @@ function getCardFeedIcon(cardFeed: CompanyCardFeed | typeof CONST.EXPENSIFY_CARD
return Illustrations.AmexCompanyCards;
}

function removeExpensifyCardFromCompanyCards(companyCards?: Record<string, CardFeedData>) {
if (!companyCards) {
function isCustomFeed(feed: CompanyCardFeed): boolean {
return [CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD, CONST.COMPANY_CARD.FEED_BANK_NAME.VISA, CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX].some((value) => value === feed);
}

function getCompanyFeeds(cardFeeds: OnyxEntry<CardFeeds>): CompanyFeeds {
return {...cardFeeds?.settings?.companyCards, ...cardFeeds?.settings?.oAuthAccountDetails};
}

function removeExpensifyCardFromCompanyCards(cardFeeds: OnyxEntry<CardFeeds>): CompanyFeeds {
if (!cardFeeds) {
return {};
}

const companyCards = getCompanyFeeds(cardFeeds);
return Object.fromEntries(Object.entries(companyCards).filter(([key]) => key !== CONST.EXPENSIFY_CARD.BANK));
}

Expand Down Expand Up @@ -283,6 +293,16 @@ const getBankCardDetailsImage = (bank: ValueOf<typeof CONST.COMPANY_CARDS.BANKS>
return iconMap[bank];
};

function getCustomOrFormattedFeedName(feed?: CompanyCardFeed, companyCardNicknames?: CompanyCardNicknames): string | undefined {
if (!feed) {
return;
}

const customFeedName = companyCardNicknames?.[feed];
const formattedFeedName = Localize.translateLocal('workspace.companyCards.feedName', {feedName: getCardFeedName(feed)});
return customFeedName ?? formattedFeedName;
}

// We will simplify the logic below once we have #50450 #50451 implemented
const getCorrectStepForSelectedBank = (selectedBank: ValueOf<typeof CONST.COMPANY_CARDS.BANKS>) => {
const banksWithFeedType = [
Expand Down Expand Up @@ -316,8 +336,8 @@ const getCorrectStepForSelectedBank = (selectedBank: ValueOf<typeof CONST.COMPAN
return CONST.COMPANY_CARDS.STEP.CARD_TYPE;
};

function getSelectedFeed(lastSelectedFeed: OnyxEntry<CompanyCardFeed>, cardFeeds: OnyxEntry<CardFeeds>): CompanyCardFeed {
const defaultFeed = Object.keys(removeExpensifyCardFromCompanyCards(cardFeeds?.settings?.companyCards)).at(0) as CompanyCardFeed;
function getSelectedFeed(lastSelectedFeed: OnyxEntry<CompanyCardFeed>, cardFeeds: OnyxEntry<CardFeeds>): CompanyCardFeed | undefined {
const defaultFeed = Object.keys(removeExpensifyCardFromCompanyCards(cardFeeds)).at(0) as CompanyCardFeed | undefined;
return lastSelectedFeed ?? defaultFeed;
}

Expand All @@ -340,8 +360,11 @@ export {
getCompanyCardNumber,
getCardFeedIcon,
getCardFeedName,
getCompanyFeeds,
isCustomFeed,
getBankCardDetailsImage,
getSelectedFeed,
getCorrectStepForSelectedBank,
getCustomOrFormattedFeedName,
removeExpensifyCardFromCompanyCards,
};
2 changes: 1 addition & 1 deletion src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,7 @@ type SettingsNavigatorParamList = {
};
[SCREENS.WORKSPACE.COMPANY_CARD_DETAILS]: {
policyID: string;
bank: string;
bank: CompanyCardFeed;
cardID: string;
backTo?: Routes;
};
Expand Down
44 changes: 19 additions & 25 deletions src/libs/actions/CompanyCards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
UpdateCompanyCardNameParams,
} from '@libs/API/parameters';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as CardUtils from '@libs/CardUtils';
import * as ErrorUtils from '@libs/ErrorUtils';
import * as NetworkStore from '@libs/Network/NetworkStore';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
Expand Down Expand Up @@ -121,19 +122,20 @@ function setWorkspaceCompanyCardFeedName(policyID: string, workspaceAccountID: n
API.write(WRITE_COMMANDS.SET_COMPANY_CARD_FEED_NAME, parameters, onyxData);
}

function setWorkspaceCompanyCardTransactionLiability(workspaceAccountID: number, policyID: string, bankName: string, liabilityType: string) {
function setWorkspaceCompanyCardTransactionLiability(workspaceAccountID: number, policyID: string, bankName: CompanyCardFeed, liabilityType: string) {
const authToken = NetworkStore.getAuthToken();
const isCustomFeed = CardUtils.isCustomFeed(bankName);
const feedUpdates = {
[bankName]: {liabilityType},
};

const onyxData: OnyxData = {
optimisticData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`,
value: {
settings: {
companyCards: {
[bankName]: {liabilityType},
},
},
settings: isCustomFeed ? {companyCards: feedUpdates} : {oAuthAccountDetails: feedUpdates},
},
},
],
Expand All @@ -149,8 +151,10 @@ function setWorkspaceCompanyCardTransactionLiability(workspaceAccountID: number,
API.write(WRITE_COMMANDS.SET_COMPANY_CARD_TRANSACTION_LIABILITY, parameters, onyxData);
}

function deleteWorkspaceCompanyCardFeed(policyID: string, workspaceAccountID: number, bankName: string) {
function deleteWorkspaceCompanyCardFeed(policyID: string, workspaceAccountID: number, bankName: CompanyCardFeed) {
const authToken = NetworkStore.getAuthToken();
const isCustomFeed = CardUtils.isCustomFeed(bankName);
const feedUpdates = {[bankName]: null};

const onyxData: OnyxData = {
optimisticData: [
Expand All @@ -159,9 +163,7 @@ function deleteWorkspaceCompanyCardFeed(policyID: string, workspaceAccountID: nu
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`,
value: {
settings: {
companyCards: {
[bankName]: null,
},
...(isCustomFeed ? {companyCards: feedUpdates} : {oAuthAccountDetails: feedUpdates}),
companyCardNicknames: {
[bankName]: null,
},
Expand Down Expand Up @@ -309,8 +311,12 @@ function unassignWorkspaceCompanyCard(workspaceAccountID: number, bankName: stri
API.write(WRITE_COMMANDS.UNASSIGN_COMPANY_CARD, parameters, onyxData);
}

function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string, bankName: string) {
function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string, bankName: CompanyCardFeed) {
const authToken = NetworkStore.getAuthToken();
const isCustomFeed = CardUtils.isCustomFeed(bankName);
const optimisticFeedUpdates = {[bankName]: {errors: null}};
const failureFeedUpdates = {[bankName]: {errors: {error: CONST.COMPANY_CARDS.CONNECTION_ERROR}}};

const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
Expand Down Expand Up @@ -346,13 +352,7 @@ function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string,
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`,
value: {
settings: {
companyCards: {
[bankName]: {
errors: null,
},
},
},
settings: isCustomFeed ? {companyCards: optimisticFeedUpdates} : {oAuthAccountDetails: optimisticFeedUpdates},
},
},
];
Expand Down Expand Up @@ -419,13 +419,7 @@ function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string,
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`,
value: {
settings: {
companyCards: {
[bankName]: {
errors: {error: CONST.COMPANY_CARDS.CONNECTION_ERROR},
},
},
},
settings: isCustomFeed ? {companyCards: failureFeedUpdates} : {oAuthAccountDetails: failureFeedUpdates},
},
},
];
Expand Down
2 changes: 2 additions & 0 deletions src/pages/settings/Wallet/PaymentMethodList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ function PaymentMethodList({
interactive: false,
canDismissError: false,
errors: card.errors,
pendingAction: card.pendingAction,
brickRoadIndicator:
card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN || card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL
? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR
Expand Down Expand Up @@ -271,6 +272,7 @@ function PaymentMethodList({
interactive: true,
canDismissError: true,
errors: card.errors,
pendingAction: card.pendingAction,
brickRoadIndicator:
card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN || card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL
? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR
Expand Down
5 changes: 4 additions & 1 deletion src/pages/workspace/WorkspaceInitialPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import useSingleExecution from '@hooks/useSingleExecution';
import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
import {isConnectionInProgress} from '@libs/actions/connections';
import * as CardUtils from '@libs/CardUtils';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import getTopmostRouteName from '@libs/Navigation/getTopmostRouteName';
import Navigation from '@libs/Navigation/Navigation';
Expand Down Expand Up @@ -220,12 +221,14 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
}

if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_COMPANY_CARDS_ENABLED]) {
const hasPolicyFeedsError = PolicyUtils.hasPolicyFeedsError(CardUtils.getCompanyFeeds(cardFeeds));

protectedCollectPolicyMenuItems.push({
translationKey: 'workspace.common.companyCards',
icon: Expensicons.CreditCard,
action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)))),
routeName: SCREENS.WORKSPACE.COMPANY_CARDS,
brickRoadIndicator: PolicyUtils.hasPolicyFeedsError(cardFeeds?.settings?.companyCards ?? {}) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
brickRoadIndicator: hasPolicyFeedsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
subtitleTranslationKey: 'workspace.moreFeatures.companyCards.subtitle',
isActive: policy?.areCompanyCardsEnabled ?? false,
pendingAction: policy?.pendingFields?.areCompanyCardsEnabled,
disabled: !isEmptyObject(CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds?.settings?.companyCards)),
disabled: !isEmptyObject(CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds)),
action: (isEnabled: boolean) => {
if (!policyID) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,16 @@ function WorkspaceCompanyCardFeedSelectorPage({route}: WorkspaceCompanyCardFeedS
const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`);
const selectedFeed = CardUtils.getSelectedFeed(lastSelectedFeed, cardFeeds);
const availableCards = CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds?.settings?.companyCards);
const companyFeeds = CardUtils.getCompanyFeeds(cardFeeds);
const availableCards = CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds);

const feeds: CardFeedListItem[] = (Object.keys(availableCards) as CompanyCardFeed[]).map((feed) => ({
value: feed,
text: cardFeeds?.settings?.companyCardNicknames?.[feed] ?? CardUtils.getCardFeedName(feed),
keyForList: feed,
isSelected: feed === selectedFeed,
brickRoadIndicator: cardFeeds?.settings?.companyCards?.[feed]?.errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
canShowSeveralIndicators: !!cardFeeds?.settings?.companyCards?.[feed]?.errors,
brickRoadIndicator: companyFeeds[feed]?.errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
canShowSeveralIndicators: !!companyFeeds[feed]?.errors,
leftElement: (
<Icon
src={CardUtils.getCardFeedIcon(feed)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import * as CardUtils from '@libs/CardUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import Navigation from '@navigation/Navigation';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {CompanyCardFeed} from '@src/types/onyx';
Expand All @@ -39,13 +38,13 @@ function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed}: Worksp
const shouldChangeLayout = isMediumScreenWidth || shouldUseNarrowLayout;
const feedName = CardUtils.getCardFeedName(selectedFeed);
const formattedFeedName = translate('workspace.companyCards.feedName', {feedName});
const isCustomFeed =
CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD === selectedFeed || CONST.COMPANY_CARD.FEED_BANK_NAME.VISA === selectedFeed || CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX === selectedFeed;
const currentFeedData = cardFeeds?.settings?.companyCards?.[selectedFeed] ?? cardFeeds?.settings?.oAuthAccountDetails?.[selectedFeed] ?? {pending: true, errors: {}};
const isCustomFeed = CardUtils.isCustomFeed(selectedFeed);
const companyFeeds = CardUtils.getCompanyFeeds(cardFeeds);
const currentFeedData = companyFeeds?.[selectedFeed];

return (
<OfflineWithFeedback
errors={cardFeeds?.settings?.companyCards?.[selectedFeed]?.errors}
errors={currentFeedData?.errors}
canDismissError={false}
errorRowStyles={styles.ph5}
>
Expand All @@ -65,7 +64,7 @@ function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed}: Worksp
<CaretWrapper>
<Text style={styles.textStrong}>{formattedFeedName}</Text>
</CaretWrapper>
{PolicyUtils.hasPolicyFeedsError(cardFeeds?.settings?.companyCards ?? {}, selectedFeed) && (
{PolicyUtils.hasPolicyFeedsError(companyFeeds, selectedFeed) && (
<Icon
src={Expensicons.DotIndicator}
fill={theme.danger}
Expand All @@ -79,8 +78,7 @@ function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed}: Worksp
<View style={[styles.flexRow, styles.gap2]}>
<Button
success
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
isDisabled={currentFeedData.pending || !!currentFeedData.errors}
isDisabled={!currentFeedData || !!currentFeedData?.pending || !!currentFeedData?.errors}
onPress={() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute(policyID, selectedFeed))}
icon={Expensicons.Plus}
text={translate('workspace.companyCards.assignCard')}
Expand Down
12 changes: 6 additions & 6 deletions src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ function WorkspaceCompanyCardPage({route}: WorkspaceCompanyCardPageProps) {
const selectedFeed = CardUtils.getSelectedFeed(lastSelectedFeed, cardFeeds);
const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${selectedFeed}`);

const companyCards = CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds?.settings?.companyCards);
const companyCards = CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds);
const isLoading = !cardFeeds || !!(cardFeeds.isLoading && !companyCards);
const selectedCompanyCard = companyCards[selectedFeed ?? ''] ?? cardFeeds?.settings?.oAuthAccountDetails?.[selectedFeed ?? ''] ?? null;
const isNoFeed = isEmptyObject(companyCards) && isEmptyObject(cardFeeds?.settings?.oAuthAccountDetails) && !selectedCompanyCard;
const isPending = !!selectedCompanyCard?.pending;
const selectedFeedData = selectedFeed && companyCards[selectedFeed];
const isNoFeed = isEmptyObject(companyCards) && !selectedFeedData;
const isPending = !!selectedFeedData?.pending;
const isFeedAdded = !isPending && !isNoFeed;

const fetchCompanyCards = useCallback(() => {
Expand Down Expand Up @@ -79,10 +79,10 @@ function WorkspaceCompanyCardPage({route}: WorkspaceCompanyCardPageProps) {
includeSafeAreaPaddingBottom
showLoadingAsFirstRender={false}
>
{(isFeedAdded || isPending) && (
{(isFeedAdded || isPending) && !!selectedFeed && (
<WorkspaceCompanyCardsListHeaderButtons
policyID={policyID}
selectedFeed={selectedFeed ?? ''}
selectedFeed={selectedFeed}
/>
)}
{isNoFeed && <WorkspaceCompanyCardPageEmptyState route={route} />}
Expand Down
Loading

0 comments on commit b54c0fb

Please sign in to comment.