Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';

import {getTranslationKeyForLimitType} from '@libs/CardUtils';
import {getTranslationKeyForCardStatus, getTranslationKeyForLimitType} from '@libs/CardUtils';
import {convertToShortDisplayString} from '@libs/CurrencyUtils';
import DateUtils from '@libs/DateUtils';
import {temporaryGetDisplayNameOrDefault} from '@libs/PersonalDetailsUtils';
Expand Down Expand Up @@ -46,6 +46,8 @@ export default function WorkspaceExpensifyCardsTableRow({item, rowIndex, shouldU
const narrowLayoutSubtitle = [item.lastFourPAN, item.name].filter(Boolean).join(` ${CONST.DOT_SEPARATOR} `);
const cardType = item.isVirtual ? translate('workspace.expensifyCard.virtual') : translate('workspace.expensifyCard.physical');
const limitTypeLabel = translate(getTranslationKeyForLimitType(item.limitType));
const statusTranslationKey = getTranslationKeyForCardStatus(item.card.state, item.isVirtual);
const statusLabel = statusTranslationKey ? translate(statusTranslationKey) : '';
const formattedLimit = convertToShortDisplayString(item.limit, item.currency);
const formattedFrozenDate = item.frozenDate ? DateUtils.formatWithUTCTimeZone(item.frozenDate, CONST.DATE.MONTH_DAY_YEAR_ABBR_FORMAT) : '';
let frozenByText: string | undefined;
Expand All @@ -58,7 +60,7 @@ export default function WorkspaceExpensifyCardsTableRow({item, rowIndex, shouldU
}
}

const accessibilityLabel = [cardholderName, item.name, cardType, limitTypeLabel, item.lastFourPAN, formattedLimit, frozenByText].filter(Boolean).join(', ');
const accessibilityLabel = [cardholderName, item.name, cardType, limitTypeLabel, item.lastFourPAN, statusLabel, formattedLimit, frozenByText].filter(Boolean).join(', ');

const frozenByRowFooter = !!frozenByText && (
<View style={[styles.flexRow, styles.alignItemsCenter, styles.mt1]}>
Expand Down Expand Up @@ -137,7 +139,7 @@ export default function WorkspaceExpensifyCardsTableRow({item, rowIndex, shouldU
)}

{!shouldUseNarrowTableLayout && (
<View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter]}>
<View style={[styles.flex1, styles.mnw0, styles.flexRow, styles.alignItemsCenter]}>
<TextWithTooltip
shouldShowTooltip
numberOfLines={1}
Expand All @@ -156,6 +158,16 @@ export default function WorkspaceExpensifyCardsTableRow({item, rowIndex, shouldU
</View>
)}

{!shouldUseNarrowTableLayout && (
<View style={[styles.flex1, styles.mnw0, styles.flexRow, styles.alignItemsCenter]}>
<TextWithTooltip
shouldShowTooltip
numberOfLines={1}
text={statusLabel}
/>
</View>
)}

<View
style={[
shouldUseNarrowTableLayout ? styles.flexColumn : styles.flexRow,
Expand Down
25 changes: 23 additions & 2 deletions src/components/Tables/WorkspaceExpensifyCardsTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';

import {filterCardsByPersonalDetails, getTranslationKeyForLimitType} from '@libs/CardUtils';
import {filterCardsByPersonalDetails, getTranslationKeyForCardStatus, getTranslationKeyForLimitType} from '@libs/CardUtils';
import {getLatestErrorMessage} from '@libs/ErrorUtils';

import WorkspaceCardListLabels from '@pages/workspace/expensifyCard/WorkspaceCardListLabels';
Expand All @@ -28,7 +28,7 @@ import {View} from 'react-native';

import WorkspaceExpensifyCardsTableRow from './WorkspaceExpensifyCardsTableRow';

type WorkspaceExpensifyCardTableColumnKey = 'name' | 'type' | 'limitType' | 'lastFour' | 'limit' | 'actions';
type WorkspaceExpensifyCardTableColumnKey = 'name' | 'type' | 'limitType' | 'lastFour' | 'status' | 'limit' | 'actions';

type WorkspaceExpensifyCardTableRowData = TableData & {
cardID: number;
Expand Down Expand Up @@ -119,12 +119,25 @@ export default function WorkspaceExpensifyCardsTable({
key: 'limitType',
label: translate('workspace.card.issueNewCard.limitType'),
sortable: true,
styling: {
// minWidth: 0 lets the grid track size purely from its 1fr share instead of the cell content,
// so the Limit type and Status columns always render at the same width.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shawnborton do you agree with this: "Limit type and Status columns always render at the same width."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I had asked it to do that, so that this way those columns are a bit smaller than the name column.

containerStyles: [styles.mnw0],
},
},
{
key: 'lastFour',
label: translate('workspace.expensifyCard.lastFour'),
sortable: true,
},
{
key: 'status',
label: translate('common.status'),
sortable: true,
styling: {
containerStyles: [styles.mnw0],
},
},
{
key: 'limit',
label: translate('workspace.expensifyCard.limit'),
Expand Down Expand Up @@ -160,6 +173,14 @@ export default function WorkspaceExpensifyCardsTable({
return localeCompare(item1.lastFourPAN, item2.lastFourPAN) * orderMultiplier;
}

if (activeSorting.columnKey === 'status') {
const status1TranslationKey = getTranslationKeyForCardStatus(item1.card.state, item1.isVirtual);
const status2TranslationKey = getTranslationKeyForCardStatus(item2.card.state, item2.isVirtual);
const status1 = status1TranslationKey ? translate(status1TranslationKey) : '';
const status2 = status2TranslationKey ? translate(status2TranslationKey) : '';
return localeCompare(status1, status2) * orderMultiplier;
}

if (activeSorting.columnKey === 'limit') {
return (item1.limit - item2.limit) * orderMultiplier;
}
Expand Down
4 changes: 4 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5735,6 +5735,10 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
newCard: 'Neue Karte',
name: 'Name',
lastFour: 'Letzte 4',
statusPendingOrder: 'Bestellung ausstehend',
statusShipped: 'Versendet',
statusActive: 'Aktiv',
statusInactive: 'Inaktiv',
limit: 'Limit',
currentBalance: 'Aktueller Kontostand',
currentBalanceDescription: 'Der aktuelle Saldo ist die Summe aller verbuchten Expensify Karte-Transaktionen, die seit dem letzten Abrechnungsdatum erfolgt sind.',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5783,6 +5783,10 @@ const translations = {
newCard: 'New card',
name: 'Name',
lastFour: 'Last 4',
statusPendingOrder: 'Pending order',
statusShipped: 'Shipped',
statusActive: 'Active',
statusInactive: 'Inactive',
limit: 'Limit',
currentBalance: 'Current balance',
currentBalanceDescription: 'Current balance is the sum of all posted Expensify Card transactions that have occurred since the last settlement date.',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5557,6 +5557,10 @@ ${amount} para ${merchant} - ${date}`,
newCard: 'Nueva tarjeta',
name: 'Nombre',
lastFour: '4 últimos',
statusPendingOrder: 'Pedido pendiente',
statusShipped: 'Enviada',
statusActive: 'Activa',
statusInactive: 'Inactiva',
limit: 'Limite',
currentBalance: 'Saldo actual',
currentBalanceDescription:
Expand Down
4 changes: 4 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5745,6 +5745,10 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.
findCard: 'Trouver la carte',
newCard: 'Nouvelle carte',
name: 'Nom',
statusPendingOrder: 'Commande en attente',
statusShipped: 'Expédiée',
statusActive: 'Active',
statusInactive: 'Inactive',
lastFour: '4 derniers',
limit: 'Limite',
currentBalance: 'Solde actuel',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5715,6 +5715,10 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST.
newCard: 'Nuova carta',
name: 'Nome',
lastFour: 'Ultime 4',
statusPendingOrder: 'Ordine in attesa',
statusShipped: 'Spedita',
statusActive: 'Attiva',
statusInactive: 'Inattiva',
limit: 'Limite',
currentBalance: 'Saldo attuale',
currentBalanceDescription: 'Il saldo attuale è la somma di tutte le transazioni contabilizzate della Carta Expensify che si sono verificate dalla data dell’ultima liquidazione.',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5655,6 +5655,10 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO
newCard: '新しいカード',
name: '名前',
lastFour: '下4桁',
statusPendingOrder: '注文待ち',
statusShipped: '発送済み',
statusActive: '有効',
statusInactive: '無効',
limit: '上限',
currentBalance: '現在の残高',
currentBalanceDescription: '現在残高は、前回の精算日以降に発生し記帳されたすべての Expensify カード取引の合計です。',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5705,6 +5705,10 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_
newCard: 'Nieuwe kaart',
name: 'Naam',
lastFour: 'Laatste 4',
statusPendingOrder: 'Bestelling in behandeling',
statusShipped: 'Verzonden',
statusActive: 'Actief',
statusInactive: 'Inactief',
limit: 'Limiet',
currentBalance: 'Huidige saldo',
currentBalanceDescription: 'Het huidige saldo is de som van alle geboekte Expensify Kaart-transacties die hebben plaatsgevonden sinds de laatste afwikkelingsdatum.',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5688,6 +5688,10 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy
newCard: 'Nowa karta',
name: 'Nazwa',
lastFour: 'Ostatnie 4',
statusPendingOrder: 'Oczekujące zamówienie',
statusShipped: 'Wysłana',
statusActive: 'Aktywna',
statusInactive: 'Nieaktywna',
limit: 'Limit',
currentBalance: 'Bieżące saldo',
currentBalanceDescription: 'Bieżące saldo to suma wszystkich zaksięgowanych transakcji Kartą Expensify, które miały miejsce od ostatniej daty rozliczenia.',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5694,6 +5694,10 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS
newCard: 'Novo cartão',
name: 'Nome',
lastFour: 'Últimos 4',
statusPendingOrder: 'Pedido pendente',
statusShipped: 'Enviado',
statusActive: 'Ativo',
statusInactive: 'Inativo',
limit: 'Limite',
currentBalance: 'Saldo atual',
currentBalanceDescription: 'O saldo atual é a soma de todas as transações lançadas do Cartão Expensify que ocorreram desde a última data de liquidação.',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5544,6 +5544,10 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM
newCard: '新卡片',
name: '姓名',
lastFour: '最后 4 位',
statusPendingOrder: '待下单',
statusShipped: '已发货',
statusActive: '有效',
statusInactive: '无效',
limit: '限额',
currentBalance: '当前余额',
currentBalanceDescription: '当前余额是上次结算日期以来已入账的所有 Expensify 卡交易的总和。',
Expand Down
24 changes: 24 additions & 0 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,29 @@ function getTranslationKeyForLimitType(limitType: ValueOf<typeof CONST.EXPENSIFY
}
}

/**
* Maps an Expensify Card `state` to the translation key for its status label shown in the workspace Expensify Card table.
* `Pending order` and `Shipped` are physical-only states, so a virtual card in one of those states has no status to show.
* Only recognized states map to a label; any other state (bad data, or an unexpected state that slips through
* `filterInactiveCardsForWorkspace`) returns `undefined` so the status renders blank rather than defaulting to `Active`.
*/
function getTranslationKeyForCardStatus(state: ValueOf<typeof CONST.EXPENSIFY_CARD.STATE> | undefined, isVirtual: boolean): TranslationPaths | undefined {
switch (state) {
// Pending order and Shipped are physical-only states, so a virtual card in one of them has no meaningful status to show.
case CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED:
return isVirtual ? undefined : 'workspace.expensifyCard.statusPendingOrder';
case CONST.EXPENSIFY_CARD.STATE.NOT_ACTIVATED:
return isVirtual ? undefined : 'workspace.expensifyCard.statusShipped';
case CONST.EXPENSIFY_CARD.STATE.OPEN:
return 'workspace.expensifyCard.statusActive';
case CONST.EXPENSIFY_CARD.STATE.STATE_SUSPENDED:
return 'workspace.expensifyCard.statusInactive';
// Any other state (e.g. bad data) has no status to show, so it's left blank rather than defaulting to Active.
default:
return undefined;
}
}

function maskPin(pin: string | undefined): string {
if (pin === undefined) {
return '••••';
Expand Down Expand Up @@ -1958,6 +1981,7 @@ export {
getCardDescription,
getMCardNumberString,
getTranslationKeyForLimitType,
getTranslationKeyForCardStatus,
maskPin,
getEligibleBankAccountsForCard,
sortCardsByCardholderName,
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/CardUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
getPlaidInstitutionIconUrl,
getPlaidInstitutionId,
getSelectedFeed,
getTranslationKeyForCardStatus,
getYearFromExpirationDateString,
hasAssignedCardMatching,
hasIssuedExpensifyCard,
Expand Down Expand Up @@ -2081,6 +2082,33 @@ describe('CardUtils', () => {
});
});

describe('getTranslationKeyForCardStatus', () => {
it('maps STATE_NOT_ISSUED to pending order for physical cards', () => {
expect(getTranslationKeyForCardStatus(CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED, false)).toBe('workspace.expensifyCard.statusPendingOrder');
});

it('maps NOT_ACTIVATED to shipped for physical cards', () => {
expect(getTranslationKeyForCardStatus(CONST.EXPENSIFY_CARD.STATE.NOT_ACTIVATED, false)).toBe('workspace.expensifyCard.statusShipped');
});

it('maps OPEN to active', () => {
expect(getTranslationKeyForCardStatus(CONST.EXPENSIFY_CARD.STATE.OPEN, false)).toBe('workspace.expensifyCard.statusActive');
});

it('maps STATE_SUSPENDED to inactive', () => {
expect(getTranslationKeyForCardStatus(CONST.EXPENSIFY_CARD.STATE.STATE_SUSPENDED, false)).toBe('workspace.expensifyCard.statusInactive');
});

it('reports no status for a virtual card in a physical-only state', () => {
expect(getTranslationKeyForCardStatus(CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED, true)).toBeUndefined();
expect(getTranslationKeyForCardStatus(CONST.EXPENSIFY_CARD.STATE.NOT_ACTIVATED, true)).toBeUndefined();
});

it('reports no status for an undefined or unrecognized state', () => {
expect(getTranslationKeyForCardStatus(undefined, false)).toBeUndefined();
});
});

describe('getDefaultExpensifyCardLimitType', () => {
it('returns SMART when policy has approvals configured (approvalMode is ADVANCED)', () => {
const policy = createMock<Policy>({
Expand Down
Loading