From 323bd5d9d367a8e10d90fc3a99f1fba09e5a9296 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 27 Jul 2026 14:14:40 +0700 Subject: [PATCH 01/11] feat: Expanding group & Suggested search --- src/CONST/index.ts | 8 ++ .../ListItem/TransactionGroupListExpanded.tsx | 4 +- .../TransactionListItemNarrow.tsx | 4 +- .../TransactionListItemWide.tsx | 4 +- .../ListItem/TransactionListItem/index.tsx | 8 +- .../ListItem/TransactionListItem/types.ts | 2 +- src/components/Search/SearchTableHeader.tsx | 5 ++ .../TransactionItemRowWide.tsx | 10 +++ src/hooks/useSearchShouldCalculateTotals.ts | 1 + src/hooks/useSearchTypeMenuSections.ts | 1 + src/languages/de.ts | 2 + src/languages/en.ts | 2 + src/languages/es.ts | 2 + src/languages/fr.ts | 2 + src/languages/it.ts | 2 + src/languages/ja.ts | 2 + src/languages/nl.ts | 2 + src/languages/pl.ts | 2 + src/languages/pt-BR.ts | 2 + src/languages/zh-hans.ts | 2 + src/libs/ReportActionsUtils.ts | 1 + src/libs/SearchUIUtils.ts | 90 +++++++++++++++++++ src/pages/Search/SearchTypeMenuNarrow.tsx | 1 + src/pages/Search/SearchTypeMenuWide.tsx | 1 + src/styles/utils/index.ts | 1 + src/types/onyx/OriginalMessage.ts | 12 +++ 26 files changed, 162 insertions(+), 11 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 00de7b87ae51..948ac89b51d9 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6864,6 +6864,11 @@ const CONST = { reportView: true, }, TAG: {column: this.TABLE_COLUMNS.TAG, search: true, reportView: true}, + VIOLATIONS: { + column: this.TABLE_COLUMNS.VIOLATIONS, + search: true, + reportView: true, + }, TAG_GL_CODE: { column: this.TABLE_COLUMNS.TAG_GL_CODE, search: true, @@ -7175,6 +7180,7 @@ const CONST = { TO: 'to', CATEGORY: 'category', TAG: 'tag', + VIOLATIONS: 'violations', ORIGINAL_AMOUNT: 'originalamount', REIMBURSABLE: 'reimbursable', BILLABLE: 'billable', @@ -7401,6 +7407,7 @@ const CONST = { [this.TABLE_COLUMNS.TO]: 'to', [this.TABLE_COLUMNS.CATEGORY]: 'category', [this.TABLE_COLUMNS.TAG]: 'tag', + [this.TABLE_COLUMNS.VIOLATIONS]: 'violations', [this.TABLE_COLUMNS.ORIGINAL_AMOUNT]: 'purchase-amount', [this.TABLE_COLUMNS.REIMBURSABLE]: 'reimbursable', [this.TABLE_COLUMNS.BILLABLE]: 'billable', @@ -7498,6 +7505,7 @@ const CONST = { TOP_CATEGORIES: 'topCategories', TOP_MERCHANTS: 'topMerchants', SPEND_OVER_TIME: 'spendOverTime', + VIOLATIONS_BY_SUBMITTER: 'violationsBySubmitter', }, SAVED_SEARCH_PREFIX: 'savedSearch_', GROUP_PREFIX: 'group_', diff --git a/src/components/Search/SearchList/ListItem/TransactionGroupListExpanded.tsx b/src/components/Search/SearchList/ListItem/TransactionGroupListExpanded.tsx index 47746cb3dc2e..a779cd95f0c3 100644 --- a/src/components/Search/SearchList/ListItem/TransactionGroupListExpanded.tsx +++ b/src/components/Search/SearchList/ListItem/TransactionGroupListExpanded.tsx @@ -330,7 +330,7 @@ function TransactionGroupListExpandedImpl({ )} {visibleTransactions.map((transaction, index) => { const shouldShowBottomBorder = !isLastTransaction(index); - const exportedReportActions = Object.values(transactionsSnapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transaction?.reportID}`] ?? {}); + const reportActions = Object.values(transactionsSnapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transaction?.reportID}`] ?? {}); const isDeletedOrPendingDelete = isDeletedTransaction(transaction) || isTransactionPendingDelete(transaction); return ( @@ -378,7 +378,7 @@ function TransactionGroupListExpandedImpl({ shouldShowBottomBorder={shouldShowBottomBorder} onArrowRightPress={isDeletedOrPendingDelete ? undefined : (event) => openReportInRHP(transaction, event)} shouldShowArrowRightOnNarrowLayout - reportActions={exportedReportActions} + reportActions={reportActions} nonPersonalAndWorkspaceCards={nonPersonalAndWorkspaceCards} isActionColumnWide={isActionColumnWide} isHover={hovered} diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx b/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx index 9981a9360482..ec8302afb964 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx @@ -44,7 +44,7 @@ function TransactionListItemNarrow({ handleActionButtonPress, shouldDisableActionPointerEvents, transactionPreviewData, - exportedReportActions, + reportActions, nonPersonalAndWorkspaceCards, isAttendeesEnabledForMovingPolicy, }: TransactionListItemNarrowProps) { @@ -170,7 +170,7 @@ function TransactionListItemNarrow({ onArrowRightPress={isDeletedTransaction ? undefined : (event) => onSelectRow(item, transactionPreviewData, event)} isHover={false} nonPersonalAndWorkspaceCards={nonPersonalAndWorkspaceCards} - reportActions={exportedReportActions} + reportActions={reportActions} isAttendeesEnabledForMovingPolicy={isAttendeesEnabledForMovingPolicy} /> diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemWide.tsx b/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemWide.tsx index 9fc257f3df30..a93c35e013d6 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemWide.tsx +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemWide.tsx @@ -42,7 +42,7 @@ function TransactionListItemWide({ handleActionButtonPress, shouldDisableActionPointerEvents, transactionPreviewData, - exportedReportActions, + reportActions, policyCategories, policyTagLists, nonPersonalAndWorkspaceCards, @@ -206,7 +206,7 @@ function TransactionListItemWide({ onArrowRightPress={isDeletedTransaction ? undefined : (event) => onSelectRow(item, transactionPreviewData, event)} isHover={hovered} nonPersonalAndWorkspaceCards={nonPersonalAndWorkspaceCards} - reportActions={exportedReportActions} + reportActions={reportActions} isAttendeesEnabledForMovingPolicy={isAttendeesEnabledForMovingPolicy} onEditDate={onEditDate} onEditMerchant={onEditMerchant} diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx b/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx index cb2578f1de73..c91142e69f4d 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx @@ -120,7 +120,7 @@ function TransactionListItemInner({ const snapshotPolicy = (currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${transactionItem.policyID}`] ?? {}) as Policy; const actionsData = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionItem.reportID}`]; - const exportedReportActions = actionsData ? Object.values(actionsData) : []; + const reportActions = actionsData ? Object.values(actionsData) : []; // Fetch policy categories directly from Onyx since they are not included in the search snapshot const [policyCategories] = originalUseOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${getNonEmptyStringOnyxID(policyID)}`); @@ -134,7 +134,7 @@ function TransactionListItemInner({ const [transactionViolationsForRow] = originalUseOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${getNonEmptyStringOnyxID(transactionItem.transactionID)}`); const parentReportActionID = transactionItem?.reportAction?.reportActionID; const [parentReportAction] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(transactionItem.reportID)}`, { - selector: (reportActions: OnyxEntry): OnyxEntry => reportActions?.[`${parentReportActionID}`], + selector: (actions: OnyxEntry): OnyxEntry => actions?.[`${parentReportActionID}`], }); const currentUserDetails = useCurrentUserPersonalDetails(); const [parentChatReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID)}`); @@ -151,7 +151,7 @@ function TransactionListItemInner({ reportID: transactionItem.reportID, itemKey: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionItem.transactionID}`, snapshotData, - snapshotActions: exportedReportActions, + snapshotActions: reportActions, enabled: !!snapshotData, }); const transactionPreviewData: TransactionPreviewData = { @@ -255,7 +255,7 @@ function TransactionListItemInner({ handleActionButtonPress, shouldDisableActionPointerEvents: shouldDisableSearchSubmitPress, transactionPreviewData, - exportedReportActions, + reportActions, policyCategories, policyTagLists, nonPersonalAndWorkspaceCards, diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/types.ts b/src/components/Search/SearchList/ListItem/TransactionListItem/types.ts index 89bf86169640..80d48002debe 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/types.ts +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/types.ts @@ -27,7 +27,7 @@ type TransactionListItemSharedProps = { handleActionButtonPress: (event?: ModifiedMouseEvent) => void; shouldDisableActionPointerEvents?: boolean; transactionPreviewData: TransactionPreviewData; - exportedReportActions: ReportAction[]; + reportActions: ReportAction[]; policyCategories?: PolicyCategories; policyTagLists?: PolicyTagLists; nonPersonalAndWorkspaceCards?: CardList; diff --git a/src/components/Search/SearchTableHeader.tsx b/src/components/Search/SearchTableHeader.tsx index 602343e9c3ea..4e676b14c478 100644 --- a/src/components/Search/SearchTableHeader.tsx +++ b/src/components/Search/SearchTableHeader.tsx @@ -111,6 +111,11 @@ const getExpenseHeaders = (groupBy?: SearchGroupBy): SearchColumnConfig[] => [ translationKey: 'common.tag', canEdit: true, }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS, + translationKey: 'common.violations', + isColumnSortable: false, + }, { columnName: CONST.SEARCH.TABLE_COLUMNS.TAG_GL_CODE, translationKey: 'common.tagGLCode', diff --git a/src/components/TransactionItemRow/TransactionItemRowWide.tsx b/src/components/TransactionItemRow/TransactionItemRowWide.tsx index 7778d3f38883..38d71ddbfb16 100644 --- a/src/components/TransactionItemRow/TransactionItemRowWide.tsx +++ b/src/components/TransactionItemRow/TransactionItemRowWide.tsx @@ -26,6 +26,7 @@ import getBase62ReportID from '@libs/getBase62ReportID'; import {getTagGLCode} from '@libs/PolicyUtils'; import {getReportName} from '@libs/ReportNameUtils'; import {getReimbursableTotal, isExpenseReport} from '@libs/ReportUtils'; +import {getSubmittedViolationsForTransaction} from '@libs/SearchUIUtils'; import { getAmount, getConvertedAmount, @@ -197,6 +198,15 @@ function TransactionItemRowWide({ /> ); + case CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS: + return ( + + + + ); case CONST.SEARCH.TABLE_COLUMNS.TAG_GL_CODE: return ( ): OnyxEntry => achAccount: policy.achAccount, areCategoriesEnabled: policy.areCategoriesEnabled, areWorkflowsEnabled: policy.areWorkflowsEnabled, + areRulesEnabled: policy.areRulesEnabled, }; const currentUserLoginAndAccountIDSelector = (session: OnyxEntry) => ({ diff --git a/src/languages/de.ts b/src/languages/de.ts index 1e5a4131ca3a..29519050c2b2 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -330,6 +330,7 @@ const translations: TranslationDeepObject = { billable: 'Abrechenbar', nonBillable: 'Nicht abrechenbar', tag: 'Tag', + violations: 'Violations', receipt: 'Beleg', verified: 'Verifiziert', replace: 'Ersetzen', @@ -9055,6 +9056,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc topSpenders: 'Höchste Ausgaben', topCategories: 'Topkategorien', topMerchants: 'Top-Händler', + violationsBySubmitter: 'Violations by submitter', }, }, genericErrorPage: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 75bfadee2ca7..e1fa70255145 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -355,6 +355,7 @@ const translations = { billable: 'Billable', nonBillable: 'Non-billable', tag: 'Tag', + violations: 'Violations', receipt: 'Receipt', verified: 'Verified', replace: 'Replace', @@ -8916,6 +8917,7 @@ const translations = { topSpenders: 'Top spenders', topCategories: 'Top categories', topMerchants: 'Top merchants', + violationsBySubmitter: 'Violations by submitter', }, resultsAreLimited: 'Search results are limited.', viewResults: 'View results', diff --git a/src/languages/es.ts b/src/languages/es.ts index 5e173ef30b78..e8a1d6711e4b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -289,6 +289,7 @@ const translations: TranslationDeepObject = { billable: 'Facturable', nonBillable: 'No facturable', tag: 'Etiqueta', + violations: 'Violations', receipt: 'Recibo', verified: 'Verificado', replace: 'Sustituir', @@ -8610,6 +8611,7 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, topSpenders: 'Mayores gastadores', topCategories: 'Categorías principales', topMerchants: 'Principales comerciantes', + violationsBySubmitter: 'Violations by submitter', }, resultsAreLimited: 'Los resultados de búsqueda están limitados.', viewResults: 'Ver resultados', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 664cee6663fb..0e5eba68fdcb 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -330,6 +330,7 @@ const translations: TranslationDeepObject = { billable: 'Facturable', nonBillable: 'Non refacturable', tag: 'Tag', + violations: 'Violations', receipt: 'Reçu', verified: 'Vérifié', replace: 'Remplacer', @@ -9091,6 +9092,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e topSpenders: 'Plus gros dépensiers', topCategories: 'Catégories principales', topMerchants: 'Meilleurs commerçants', + violationsBySubmitter: 'Violations by submitter', }, }, genericErrorPage: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 6dc456a27c0c..00077cb3a2ce 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -330,6 +330,7 @@ const translations: TranslationDeepObject = { billable: 'Fatturabile', nonBillable: 'Non fatturabile', tag: 'Etichetta', + violations: 'Violations', receipt: 'Ricevuta', verified: 'Verificato', replace: 'Sostituisci', @@ -9036,6 +9037,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, topSpenders: 'Maggiori spendaccioni', topCategories: 'Categorie principali', topMerchants: 'Principali esercenti', + violationsBySubmitter: 'Violations by submitter', }, }, genericErrorPage: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 11db3c3def10..b39318555a06 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -330,6 +330,7 @@ const translations: TranslationDeepObject = { billable: '請求可能', nonBillable: '請求不可', tag: 'タグ', + violations: 'Violations', receipt: 'レシート', verified: '確認済み', replace: '置換', @@ -8916,6 +8917,7 @@ ${reportName}`, topSpenders: '上位の支出者', topCategories: '上位カテゴリ', topMerchants: '上位加盟店', + violationsBySubmitter: 'Violations by submitter', }, }, genericErrorPage: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index c774982e5ffe..7e2fe02bfe1b 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -330,6 +330,7 @@ const translations: TranslationDeepObject = { billable: 'Factureerbaar', nonBillable: 'Niet-factureerbaar', tag: 'Label', + violations: 'Violations', receipt: 'Bonnetje', verified: 'Geverifieerd', replace: 'Vervangen', @@ -9008,6 +9009,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, topSpenders: 'Grootste uitgaven doeners', topCategories: 'Topcategorieën', topMerchants: 'Topverkopers', + violationsBySubmitter: 'Violations by submitter', }, }, genericErrorPage: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index ac30cc2c6efc..01f2943d1839 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -330,6 +330,7 @@ const translations: TranslationDeepObject = { billable: 'Fakturowalne', nonBillable: 'Nierozliczalne', tag: 'Tag', + violations: 'Violations', receipt: 'Paragon', verified: 'Zweryfikowano', replace: 'Zastąp', @@ -8987,6 +8988,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, topSpenders: 'Najwięksi wydający', topCategories: 'Najpopularniejsze kategorie', topMerchants: 'Najważniejsi sprzedawcy', + violationsBySubmitter: 'Violations by submitter', }, }, genericErrorPage: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index fe6a996bfccf..f1af0dc9a97a 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -330,6 +330,7 @@ const translations: TranslationDeepObject = { billable: 'Faturável', nonBillable: 'Não faturável', tag: 'Etiqueta', + violations: 'Violations', receipt: 'Recibo', verified: 'Verificado', replace: 'Substituir', @@ -8995,6 +8996,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, topSpenders: 'Maiores gastadores', topCategories: 'Principais categorias', topMerchants: 'Principais estabelecimentos', + violationsBySubmitter: 'Violations by submitter', }, }, genericErrorPage: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 487d47fe7611..71f7313641fd 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -330,6 +330,7 @@ const translations: TranslationDeepObject = { billable: '可计费', nonBillable: '不可计费', tag: '标签', + violations: 'Violations', receipt: '收据', verified: '已验证', replace: '替换', @@ -8700,6 +8701,7 @@ ${reportName}`, topSpenders: '最高消费者', topCategories: '热门类别', topMerchants: '热门商家', + violationsBySubmitter: 'Violations by submitter', }, }, genericErrorPage: { diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index fac43f97c415..2c1ced756e4d 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -4886,6 +4886,7 @@ export { isHoldAction, isWhisperAction, isSubmittedAction, + isSubmittedAndClosedAction, isDynamicExternalWorkflowSubmitAction, isMarkAsClosedAction, isForwardedAction, diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index e1e6c25d7569..b95dee34aa5e 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -118,6 +118,7 @@ import Parser from './Parser'; import {getLoginByAccountID, temporaryGetDisplayNameOrDefault} from './PersonalDetailsUtils'; import { arePaymentsEnabled, + arePolicyRulesEnabled, canSendInvoice, getCleanedTagName, getCommaSeparatedTagNameWithSanitizedColons, @@ -139,6 +140,8 @@ import { isMoneyRequestAction, isReportActionVisible, isResolvedActionableWhisper, + isSubmittedAction, + isSubmittedAndClosedAction, isWhisperActionTargetedToOthers, } from './ReportActionsUtils'; import {deprecatedGetReportName} from './ReportNameUtils'; @@ -552,6 +555,7 @@ type SearchTypeMenuItem = { | 'CreditCardHourglass' | 'Bank' | 'User' + | 'UserEye' | 'Folder' | 'Basket' | 'CalendarSolid' @@ -1023,6 +1027,38 @@ function getSuggestedSearches( CONST.SEARCH.TOP_SEARCH_LIMIT, CONST.SEARCH.VIEW.PIE, ), + [CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]: { + key: CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER, + translationPath: 'search.tabs.violationsBySubmitter', + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + icon: 'UserEye', + searchQuery: buildQueryStringFromFilterFormValues( + { + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + groupBy: CONST.SEARCH.GROUP_BY.FROM, + dateOn: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, + has: [CONST.SEARCH.HAS_VALUES.SUBMITTED_VIOLATION], + view: CONST.SEARCH.VIEW.TABLE, + limit: '10', + }, + { + sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_EXPENSES, + sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + }, + ), + get searchQueryJSON() { + return buildSearchQueryJSON(this.searchQuery); + }, + get hash() { + return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID; + }, + get similarSearchHash() { + return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID; + }, + get recentSearchHash() { + return this.searchQueryJSON?.recentSearchHash ?? CONST.DEFAULT_NUMBER_ID; + }, + }, [CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]: { key: CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME, translationPath: 'search.spendOverTime', @@ -1094,6 +1130,7 @@ function getSuggestedSearchesVisibility( let shouldShowTopSpendersSuggestion = false; let shouldShowTopCategoriesSuggestion = false; let shouldShowTopMerchantsSuggestion = false; + let shouldShowViolationsBySubmitterSuggestion = false; let hasGroupPoliciesWithExpenseChat = false; let shouldShowSpendOverTimeSuggestion = false; const topSpendersPolicyIDs: string[] = []; @@ -1139,6 +1176,7 @@ function getSuggestedSearchesVisibility( const isEligibleForTopSpendersSuggestion = isGroupPolicyEligible && (isAdmin || isAuditor || isUserApprover) && memberCount >= 2; const isEligibleForTopCategoriesSuggestion = isGroupPolicyEligible && policy.areCategoriesEnabled === true; const isEligibleForTopMerchantsSuggestion = isGroupPolicyEligible; + const isEligibleForViolationsBySubmitterSuggestion = isGroupPolicyEligible && (isAdmin || isAuditor) && arePolicyRulesEnabled(policy) && memberCount >= 2; shouldShowSubmitSuggestion ||= isEligibleForSubmitSuggestion; shouldShowPaySuggestion ||= isEligibleForPaySuggestion; @@ -1155,6 +1193,7 @@ function getSuggestedSearchesVisibility( } shouldShowTopCategoriesSuggestion ||= isEligibleForTopCategoriesSuggestion; shouldShowTopMerchantsSuggestion ||= isEligibleForTopMerchantsSuggestion; + shouldShowViolationsBySubmitterSuggestion ||= isEligibleForViolationsBySubmitterSuggestion; hasGroupPoliciesWithExpenseChat ||= isGroupPolicyEligible && !!policy.isPolicyExpenseChatEnabled && @@ -1179,6 +1218,7 @@ function getSuggestedSearchesVisibility( [CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS]: shouldShowTopSpendersSuggestion, [CONST.SEARCH.SEARCH_KEYS.TOP_CATEGORIES]: shouldShowTopCategoriesSuggestion, [CONST.SEARCH.SEARCH_KEYS.TOP_MERCHANTS]: shouldShowTopMerchantsSuggestion, + [CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]: shouldShowViolationsBySubmitterSuggestion, [CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]: shouldShowSpendOverTimeSuggestion, }, hasGroupPoliciesWithExpenseChat, @@ -4395,6 +4435,8 @@ function getSearchColumnTranslationKey(column: SearchSortBy): TranslationPaths { return 'common.type'; case CONST.SEARCH.TABLE_COLUMNS.TAG: return 'common.tag'; + case CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS: + return 'common.violations'; case CONST.SEARCH.TABLE_COLUMNS.ORIGINAL_AMOUNT: return 'common.purchaseAmount'; case CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE: @@ -4778,6 +4820,7 @@ function createTypeMenuSections(params: TypeMenuSectionsParams): SearchTypeMenuS ...(!isTrackIntentWithWorkflowsDisabled ? [CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS] : []), CONST.SEARCH.SEARCH_KEYS.TOP_CATEGORIES, CONST.SEARCH.SEARCH_KEYS.TOP_MERCHANTS, + CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER, ]; for (const key of insightsSearchKeys) { @@ -4877,6 +4920,42 @@ function getHasOptions(translate: LocalizedTranslate, type: SearchDataTypes) { } } +/** + * Collects a transaction's submitted violations from its report's submit actions. + * A report can be submitted more than once, so this aggregates across every submit action, + * dedupes by violation name, and returns a comma-separated display string. + */ +function getSubmittedViolationsForTransaction(reportActions: OnyxTypes.ReportAction[] | undefined, transactionID: string | undefined): string | undefined { + if (!reportActions?.length || !transactionID) { + return undefined; + } + + const violationNames = new Set(); + for (const action of reportActions) { + if (!isSubmittedAction(action) && !isSubmittedAndClosedAction(action)) { + continue; + } + + const originalMessage = getOriginalMessage(action); + const transactionViolations = originalMessage?.violations?.transactions?.[transactionID]; + if (!transactionViolations?.length) { + continue; + } + + for (const violation of transactionViolations) { + if (violation.name) { + violationNames.add(violation.name); + } + } + } + + if (violationNames.size === 0) { + return undefined; + } + + return Array.from(violationNames).join(', '); +} + function getTypeOptions(translate: LocalizedTranslate, policies: OnyxCollection, currentUserLogin?: string) { const typeOptions: Array> = [ {text: translate('common.expense'), value: CONST.SEARCH.DATA_TYPES.EXPENSE}, @@ -5923,6 +6002,7 @@ function getColumnsToShow({ [CONST.SEARCH.TABLE_COLUMNS.CATEGORY_GL_CODE]: false, [CONST.SEARCH.TABLE_COLUMNS.TAG]: false, [CONST.SEARCH.TABLE_COLUMNS.TAG_GL_CODE]: false, + [CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS]: false, [CONST.SEARCH.TABLE_COLUMNS.CARD]: false, [CONST.SEARCH.TABLE_COLUMNS.MCC]: false, [CONST.SEARCH.TABLE_COLUMNS.TAX_CODE]: false, @@ -5956,6 +6036,7 @@ function getColumnsToShow({ [CONST.SEARCH.TABLE_COLUMNS.CATEGORY_GL_CODE]: false, [CONST.SEARCH.TABLE_COLUMNS.TAG]: false, [CONST.SEARCH.TABLE_COLUMNS.TAG_GL_CODE]: false, + [CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS]: false, [CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE]: false, [CONST.SEARCH.TABLE_COLUMNS.BILLABLE]: false, [CONST.SEARCH.TABLE_COLUMNS.MCC]: false, @@ -6066,6 +6147,13 @@ function getColumnsToShow({ columns[CONST.SEARCH.TABLE_COLUMNS.TAG] = !isExpenseReportViewFromIOUReport; } + if (!Array.isArray(data)) { + const reportActions = Object.values(data[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transaction.reportID}`] ?? {}); + if (getSubmittedViolationsForTransaction(reportActions, transaction.transactionID)) { + columns[CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS] = true; + } + } + // Data-presence checks for columns that are hidden when empty. // Only update when we have custom columns to filter (customResult) or in expense report view, // so that the default search page path doesn't show extra columns. @@ -6444,6 +6532,7 @@ const FLEX_COLUMNS = new Set([ CONST.SEARCH.TABLE_COLUMNS.CATEGORY_GL_CODE, CONST.SEARCH.TABLE_COLUMNS.TAG, CONST.SEARCH.TABLE_COLUMNS.TAG_GL_CODE, + CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS, CONST.SEARCH.TABLE_COLUMNS.TAX_RATE, CONST.SEARCH.TABLE_COLUMNS.CARD, CONST.SEARCH.TABLE_COLUMNS.EXCHANGE_RATE, @@ -6575,6 +6664,7 @@ export { getWithdrawalStatusDisplayText, getColumnsToShow, getHasOptions, + getSubmittedViolationsForTransaction, getSettlementStatus, getSettlementStatusBadgeProps, getSearchColumnTranslationKey, diff --git a/src/pages/Search/SearchTypeMenuNarrow.tsx b/src/pages/Search/SearchTypeMenuNarrow.tsx index 61910d27e2f9..9704b77783b7 100644 --- a/src/pages/Search/SearchTypeMenuNarrow.tsx +++ b/src/pages/Search/SearchTypeMenuNarrow.tsx @@ -139,6 +139,7 @@ function SearchTypeMenuNarrow({queryJSON, onTabPress}: SearchTypeMenuNarrowProps 'CreditCardHourglass', 'Bank', 'User', + 'UserEye', 'Folder', 'Basket', 'CalendarSolid', diff --git a/src/pages/Search/SearchTypeMenuWide.tsx b/src/pages/Search/SearchTypeMenuWide.tsx index d0dd4c2e6191..d592146cc722 100644 --- a/src/pages/Search/SearchTypeMenuWide.tsx +++ b/src/pages/Search/SearchTypeMenuWide.tsx @@ -60,6 +60,7 @@ function Section({section, hash, activeItemIndex, sectionStartIndex, reportCount 'CreditCardHourglass', 'Bank', 'User', + 'UserEye', 'Folder', 'Document', 'Pencil', diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 89716ac31b22..b9327ec609dd 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1932,6 +1932,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ case CONST.SEARCH.TABLE_COLUMNS.GROUP_QUARTER: case CONST.SEARCH.TABLE_COLUMNS.TAG: case CONST.SEARCH.TABLE_COLUMNS.GROUP_TAG: + case CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS: columnWidth = {...getWidthStyle(variables.w36), ...styles.flex1}; break; case CONST.SEARCH.TABLE_COLUMNS.TAX_AMOUNT: diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index f585b2d54719..46820c48851d 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -238,6 +238,12 @@ type OriginalMessagePolicyExpenseChatWelcomeWhisper = { type?: string; }; +/** Model of a violation captured on a submitted report action */ +type SubmittedTransactionViolation = { + /** Violation identifier/name captured at submit time */ + name: string; +}; + /** Model of `submitted` report action */ type OriginalMessageSubmitted = { /** The login of the admin (used in admin-submit) */ @@ -272,6 +278,12 @@ type OriginalMessageSubmitted = { /** The workflow the report is submitted on */ workflow?: ValueOf; + + /** Snapshot of transaction violations present when the report was submitted */ + violations?: { + /** Violations keyed by transaction ID */ + transactions: Record; + }; }; /** Model of `created` report action */ From 3643b734f965adc47deff4e63b7823c3f210af0d Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 27 Jul 2026 14:40:59 +0700 Subject: [PATCH 02/11] fix test --- tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap b/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap index 23b2ccc516da..ef38241eee27 100644 --- a/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap +++ b/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap @@ -13,6 +13,7 @@ exports[`Column availability single source of truth the derived picker lists mat "attendees", "totalPerAttendee", "tag", + "violations", "tagGLCode", "exchangeRate", "originalamount", @@ -51,6 +52,7 @@ exports[`Column availability single source of truth the derived picker lists mat "attendees", "totalPerAttendee", "tag", + "violations", "tagGLCode", "exchangeRate", "originalamount", From d43b90fa274b91f1f1fdc991026844513898e37b Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 27 Jul 2026 14:45:14 +0700 Subject: [PATCH 03/11] add unit test --- tests/unit/Search/SearchUIUtilsTest.ts | 110 +++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index fa39006076e2..f2c66d6ca48a 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -11183,6 +11183,116 @@ describe('SearchUIUtils', () => { }); }); + describe('getSubmittedViolationsForTransaction', () => { + const transactionIDForViolations = 'tx-violations-1'; + const otherTransactionID = 'tx-violations-2'; + + const createSubmittedAction = ( + actionName: typeof CONST.REPORT.ACTIONS.TYPE.SUBMITTED | typeof CONST.REPORT.ACTIONS.TYPE.SUBMITTED_AND_CLOSED, + violations?: {transactions: Record>}, + reportActionID = 'submit-action-1', + ): OnyxTypes.ReportAction => + ({ + reportActionID, + actionName, + created: '2025-01-01 00:00:00', + originalMessage: { + amount: 1000, + currency: CONST.CURRENCY.USD, + ...(violations ? {violations} : {}), + }, + }) as OnyxTypes.ReportAction; + + test('returns undefined when reportActions or transactionID is missing', () => { + expect(SearchUIUtils.getSubmittedViolationsForTransaction(undefined, transactionIDForViolations)).toBeUndefined(); + expect(SearchUIUtils.getSubmittedViolationsForTransaction([], transactionIDForViolations)).toBeUndefined(); + expect(SearchUIUtils.getSubmittedViolationsForTransaction([createSubmittedAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED)], undefined)).toBeUndefined(); + }); + + test('ignores non-submit report actions', () => { + const iouAction: OnyxTypes.ReportAction = { + reportActionID: 'iou-1', + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + created: '2025-01-01 00:00:00', + originalMessage: { + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + IOUTransactionID: transactionIDForViolations, + }, + }; + + expect(SearchUIUtils.getSubmittedViolationsForTransaction([iouAction], transactionIDForViolations)).toBeUndefined(); + }); + + test('returns comma-separated violation names from a SUBMITTED action', () => { + const submitAction = createSubmittedAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED, { + transactions: { + [transactionIDForViolations]: [{name: CONST.VIOLATIONS.MISSING_CATEGORY}, {name: CONST.VIOLATIONS.MISSING_COMMENT}], + }, + }); + + expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitAction], transactionIDForViolations)).toBe( + `${CONST.VIOLATIONS.MISSING_CATEGORY}, ${CONST.VIOLATIONS.MISSING_COMMENT}`, + ); + }); + + test('includes violations from SUBMITTED_AND_CLOSED actions', () => { + const submitAndCloseAction = createSubmittedAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED_AND_CLOSED, { + transactions: { + [transactionIDForViolations]: [{name: CONST.VIOLATIONS.RECEIPT_REQUIRED}], + }, + }); + + expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitAndCloseAction], transactionIDForViolations)).toBe(CONST.VIOLATIONS.RECEIPT_REQUIRED); + }); + + test('aggregates across multiple submit actions and dedupes by name', () => { + const firstSubmit = createSubmittedAction( + CONST.REPORT.ACTIONS.TYPE.SUBMITTED, + { + transactions: { + [transactionIDForViolations]: [{name: CONST.VIOLATIONS.MISSING_CATEGORY}, {name: CONST.VIOLATIONS.MISSING_TAG}], + }, + }, + 'submit-1', + ); + const secondSubmit = createSubmittedAction( + CONST.REPORT.ACTIONS.TYPE.SUBMITTED_AND_CLOSED, + { + transactions: { + [transactionIDForViolations]: [{name: CONST.VIOLATIONS.MISSING_CATEGORY}, {name: CONST.VIOLATIONS.RECEIPT_REQUIRED}], + }, + }, + 'submit-2', + ); + + expect(SearchUIUtils.getSubmittedViolationsForTransaction([firstSubmit, secondSubmit], transactionIDForViolations)).toBe( + `${CONST.VIOLATIONS.MISSING_CATEGORY}, ${CONST.VIOLATIONS.MISSING_TAG}, ${CONST.VIOLATIONS.RECEIPT_REQUIRED}`, + ); + }); + + test('ignores violations for other transaction IDs', () => { + const submitAction = createSubmittedAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED, { + transactions: { + [otherTransactionID]: [{name: CONST.VIOLATIONS.MISSING_CATEGORY}], + }, + }); + + expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitAction], transactionIDForViolations)).toBeUndefined(); + }); + + test('returns undefined when submit actions have no violations for the transaction', () => { + const submitAction = createSubmittedAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED, { + transactions: { + [transactionIDForViolations]: [], + }, + }); + const submitActionWithoutViolations = createSubmittedAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED); + + expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitAction], transactionIDForViolations)).toBeUndefined(); + expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitActionWithoutViolations], transactionIDForViolations)).toBeUndefined(); + }); + }); + describe('getDisplayValue', () => { test('returns translated has option labels from getHasOptions', () => { const result = SearchUIUtils.getDisplayValue('has', {has: [CONST.SEARCH.HAS_VALUES.SUBMITTED_VIOLATION]}, CONST.SEARCH.DATA_TYPES.EXPENSE, translateLocal, localeCompare); From 1eb0ffb001f9ce8f1d417c3bfcb8867475ab31d8 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 28 Jul 2026 11:15:51 +0700 Subject: [PATCH 04/11] update translation for violation name --- .../TransactionItemRowWide.tsx | 2 +- src/languages/de.ts | 51 +++++++++++++ src/languages/en.ts | 50 +++++++++++++ src/languages/es.ts | 51 +++++++++++++ src/languages/fr.ts | 51 +++++++++++++ src/languages/it.ts | 51 +++++++++++++ src/languages/ja.ts | 51 +++++++++++++ src/languages/nl.ts | 51 +++++++++++++ src/languages/pl.ts | 51 +++++++++++++ src/languages/pt-BR.ts | 51 +++++++++++++ src/languages/zh-hans.ts | 51 +++++++++++++ src/libs/SearchUIUtils.ts | 74 ++++++++++++++++++- tests/unit/Search/SearchUIUtilsTest.ts | 53 +++++++++---- 13 files changed, 620 insertions(+), 18 deletions(-) diff --git a/src/components/TransactionItemRow/TransactionItemRowWide.tsx b/src/components/TransactionItemRow/TransactionItemRowWide.tsx index 38d71ddbfb16..1d2763aece42 100644 --- a/src/components/TransactionItemRow/TransactionItemRowWide.tsx +++ b/src/components/TransactionItemRow/TransactionItemRowWide.tsx @@ -204,7 +204,7 @@ function TransactionItemRowWide({ key={column} style={[StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS)]} > - + ); case CONST.SEARCH.TABLE_COLUMNS.TAG_GL_CODE: diff --git a/src/languages/de.ts b/src/languages/de.ts index 50f5b6c59f16..20320c8b2f4e 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -9726,6 +9726,57 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc customUnitRateOutOfDateRangeStartOnly: ({startDate}: {startDate: string}) => `Der Satz gilt erst ab dem ${startDate}`, customUnitRateOutOfDateRangeEndOnly: ({endDate}: {endDate: string}) => `Satz gilt nur bis ${endDate}`, }, + + /** + * Parameter-free labels for submitted transaction violations shown in Search table columns. + * Prefer these over `violations.*` when violation data/context is unavailable. + */ + transactionViolations: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, + reportOrFieldViolations: { + fieldRequired: 'Report field required', + }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} ist erforderlich`, reportContainsExpensesWithViolations: 'Der Bericht enthält Ausgaben mit Verstößen.', diff --git a/src/languages/en.ts b/src/languages/en.ts index 07e20fe83121..3bce48259e72 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -9858,6 +9858,56 @@ const translations = { companyCardRequired: 'Company card purchases required', noRoute: 'Please select a valid address', }, + /** + * Parameter-free labels for submitted transaction violations shown in Search table columns. + * Prefer these over `violations.*` when violation data/context is unavailable. + */ + transactionViolations: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, + reportOrFieldViolations: { + fieldRequired: 'Report field required', + }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} is required`, reportContainsExpensesWithViolations: 'Report contains expenses with violations.', diff --git a/src/languages/es.ts b/src/languages/es.ts index f5cf8e520ab9..10c513d27a1e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -9902,6 +9902,57 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, customUnitRateOutOfDateRangeStartOnly: ({startDate}: {startDate: string}) => `La tasa solo es válida desde ${startDate}`, customUnitRateOutOfDateRangeEndOnly: ({endDate}: {endDate: string}) => `La tasa solo es válida hasta ${endDate}`, }, + + /** + * Parameter-free labels for submitted transaction violations shown in Search table columns. + * Prefer these over `violations.*` when violation data/context is unavailable. + */ + transactionViolations: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, + reportOrFieldViolations: { + fieldRequired: 'Report field required', + }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName) => `${fieldName} es obligatorio`, reportContainsExpensesWithViolations: 'El informe contiene gastos con violaciones.', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 54c1bacc8e36..077b5806db57 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -9762,6 +9762,57 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e customUnitRateOutOfDateRangeStartOnly: ({startDate}: {startDate: string}) => `Le taux n’est valable qu’à partir du ${startDate}`, customUnitRateOutOfDateRangeEndOnly: ({endDate}: {endDate: string}) => `Le taux n’est valable que jusqu’au ${endDate}`, }, + + /** + * Parameter-free labels for submitted transaction violations shown in Search table columns. + * Prefer these over `violations.*` when violation data/context is unavailable. + */ + transactionViolations: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, + reportOrFieldViolations: { + fieldRequired: 'Report field required', + }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} est obligatoire`, reportContainsExpensesWithViolations: 'La note de frais contient des dépenses avec des violations.', diff --git a/src/languages/it.ts b/src/languages/it.ts index 839cdb4a91b5..5be0a6ec8a66 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -9708,6 +9708,57 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, customUnitRateOutOfDateRangeStartOnly: ({startDate}: {startDate: string}) => `La tariffa è valida solo a partire dal ${startDate}`, customUnitRateOutOfDateRangeEndOnly: ({endDate}: {endDate: string}) => `La tariffa è valida solo fino al ${endDate}`, }, + + /** + * Parameter-free labels for submitted transaction violations shown in Search table columns. + * Prefer these over `violations.*` when violation data/context is unavailable. + */ + transactionViolations: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, + reportOrFieldViolations: { + fieldRequired: 'Report field required', + }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} è obbligatorio`, reportContainsExpensesWithViolations: 'Il report contiene spese con violazioni.', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 2806c305f011..3dfc96b79f6e 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -9578,6 +9578,57 @@ ${reportName}`, customUnitRateOutOfDateRangeStartOnly: ({startDate}: {startDate: string}) => `料金は${startDate}からのみ有効です`, customUnitRateOutOfDateRangeEndOnly: ({endDate}: {endDate: string}) => `料金は${endDate}までのみ有効です`, }, + + /** + * Parameter-free labels for submitted transaction violations shown in Search table columns. + * Prefer these over `violations.*` when violation data/context is unavailable. + */ + transactionViolations: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, + reportOrFieldViolations: { + fieldRequired: 'Report field required', + }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} は必須です`, reportContainsExpensesWithViolations: 'レポートに違反のある経費が含まれています。', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 4ec8465eccf5..6c7bf0f37e7e 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -9679,6 +9679,57 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, customUnitRateOutOfDateRangeStartOnly: ({startDate}: {startDate: string}) => `Tarief is alleen geldig vanaf ${startDate}`, customUnitRateOutOfDateRangeEndOnly: ({endDate}: {endDate: string}) => `Tarief is alleen geldig tot ${endDate}`, }, + + /** + * Parameter-free labels for submitted transaction violations shown in Search table columns. + * Prefer these over `violations.*` when violation data/context is unavailable. + */ + transactionViolations: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, + reportOrFieldViolations: { + fieldRequired: 'Report field required', + }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} is verplicht`, reportContainsExpensesWithViolations: 'Rapport bevat onkosten met overtredingen.', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 8099b6a7cbf6..09ab42c60e4e 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -9652,6 +9652,57 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, customUnitRateOutOfDateRangeStartOnly: ({startDate}: {startDate: string}) => `Stawka jest ważna tylko od ${startDate}`, customUnitRateOutOfDateRangeEndOnly: ({endDate}: {endDate: string}) => `Stawka jest ważna tylko do ${endDate}`, }, + + /** + * Parameter-free labels for submitted transaction violations shown in Search table columns. + * Prefer these over `violations.*` when violation data/context is unavailable. + */ + transactionViolations: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, + reportOrFieldViolations: { + fieldRequired: 'Report field required', + }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `Pole ${fieldName} jest wymagane`, reportContainsExpensesWithViolations: 'Raport zawiera wydatki z naruszeniami.', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 0a720e62c9ed..78644909ca04 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -9665,6 +9665,57 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, customUnitRateOutOfDateRangeStartOnly: ({startDate}: {startDate: string}) => `A tarifa só é válida a partir de ${startDate}`, customUnitRateOutOfDateRangeEndOnly: ({endDate}: {endDate: string}) => `A tarifa só é válida até ${endDate}`, }, + + /** + * Parameter-free labels for submitted transaction violations shown in Search table columns. + * Prefer these over `violations.*` when violation data/context is unavailable. + */ + transactionViolations: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, + reportOrFieldViolations: { + fieldRequired: 'Report field required', + }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} é obrigatório`, reportContainsExpensesWithViolations: 'O relatório contém despesas com violações.', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 2f2e46f12839..da7ee57de2e4 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -9356,6 +9356,57 @@ ${reportName}`, customUnitRateOutOfDateRangeStartOnly: ({startDate}: {startDate: string}) => `费率仅自 ${startDate} 起有效`, customUnitRateOutOfDateRangeEndOnly: ({endDate}: {endDate: string}) => `该费率仅在 ${endDate} 之前有效`, }, + + /** + * Parameter-free labels for submitted transaction violations shown in Search table columns. + * Prefer these over `violations.*` when violation data/context is unavailable. + */ + transactionViolations: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, + reportOrFieldViolations: { + fieldRequired: 'Report field required', + }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} 为必填项`, reportContainsExpensesWithViolations: '报表包含有违规的报销。', diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 7923c61043d8..6ba1ca5f8d0a 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1041,7 +1041,7 @@ function getSuggestedSearches( dateOn: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, has: [CONST.SEARCH.HAS_VALUES.SUBMITTED_VIOLATION], view: CONST.SEARCH.VIEW.TABLE, - limit: '10', + limit: String(CONST.SEARCH.TOP_SEARCH_LIMIT), }, { sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_EXPENSES, @@ -4938,12 +4938,75 @@ function getHasOptions(translate: LocalizedTranslate, type: SearchDataTypes) { } } +/** Parameter-free short labels used when rendering submitted violations in Search columns. */ +const SUBMITTED_TRANSACTION_VIOLATION_SHORT_NAMES = new Set([ + CONST.VIOLATIONS.ALL_TAG_LEVELS_REQUIRED, + CONST.VIOLATIONS.AUTO_REPORTED_REJECTED_EXPENSE, + CONST.VIOLATIONS.BILLABLE_EXPENSE, + CONST.VIOLATIONS.CASH_EXPENSE_WITH_NO_RECEIPT, + CONST.VIOLATIONS.CATEGORY_OUT_OF_POLICY, + CONST.VIOLATIONS.COMPANY_CARD_REQUIRED, + CONST.VIOLATIONS.CONVERSION_SURCHARGE, + CONST.VIOLATIONS.CUSTOM_UNIT_OUT_OF_POLICY, + CONST.VIOLATIONS.CUSTOM_UNIT_RATE_OUT_OF_DATE_RANGE, + CONST.VIOLATIONS.DUPLICATED_TRANSACTION, + CONST.VIOLATIONS.FUTURE_DATE, + CONST.VIOLATIONS.HOLD, + CONST.VIOLATIONS.INACTIVE_VENDOR, + CONST.VIOLATIONS.INCREASED_DISTANCE, + CONST.VIOLATIONS.INVOICE_MARKUP, + CONST.VIOLATIONS.ITEMIZED_RECEIPT_REQUIRED, + CONST.VIOLATIONS.MAX_AGE, + CONST.VIOLATIONS.MISSING_ATTENDEES, + CONST.VIOLATIONS.MISSING_CATEGORY, + CONST.VIOLATIONS.MISSING_COMMENT, + CONST.VIOLATIONS.MISSING_TAG, + CONST.VIOLATIONS.MODIFIED_AMOUNT, + CONST.VIOLATIONS.MODIFIED_DATE, + CONST.VIOLATIONS.NO_ROUTE, + CONST.VIOLATIONS.NON_EXPENSIWORKS_EXPENSE, + CONST.VIOLATIONS.OVER_AUTO_APPROVAL_LIMIT, + CONST.VIOLATIONS.OVER_CATEGORY_LIMIT, + CONST.VIOLATIONS.OVER_LIMIT, + CONST.VIOLATIONS.OVER_TRIP_LIMIT, + CONST.VIOLATIONS.PER_DAY_LIMIT, + CONST.VIOLATIONS.PROHIBITED_EXPENSE, + CONST.VIOLATIONS.RECEIPT_GENERATED_WITH_AI, + CONST.VIOLATIONS.RECEIPT_NOT_SMART_SCANNED, + CONST.VIOLATIONS.RECEIPT_REQUIRED, + CONST.VIOLATIONS.RTER, + CONST.VIOLATIONS.SMARTSCAN_FAILED, + CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED, + CONST.VIOLATIONS.TAX_AMOUNT_CHANGED, + CONST.VIOLATIONS.TAX_OUT_OF_POLICY, + CONST.VIOLATIONS.TAX_RATE_CHANGED, + CONST.VIOLATIONS.TAX_REQUIRED, +]); + +/** + * Returns a parameter-free display label for a submitted violation name. + * Falls back to the raw identifier when no short-name translation exists. + */ +function getSubmittedViolationDisplayName(violationName: string, translate: LocalizedTranslate): string { + if (violationName === CONST.VIOLATIONS.FIELD_REQUIRED) { + return translate('reportOrFieldViolations.fieldRequired'); + } + + if (SUBMITTED_TRANSACTION_VIOLATION_SHORT_NAMES.has(violationName)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + return translate(`transactionViolations.${violationName}` as TranslationPaths); + } + + return violationName; +} + /** * Collects a transaction's submitted violations from its report's submit actions. * A report can be submitted more than once, so this aggregates across every submit action, * dedupes by violation name, and returns a comma-separated display string. + * When `translate` is provided, violation identifiers are converted to localized short labels. */ -function getSubmittedViolationsForTransaction(reportActions: OnyxTypes.ReportAction[] | undefined, transactionID: string | undefined): string | undefined { +function getSubmittedViolationsForTransaction(reportActions: OnyxTypes.ReportAction[] | undefined, transactionID: string | undefined, translate?: LocalizedTranslate): string | undefined { if (!reportActions?.length || !transactionID) { return undefined; } @@ -4971,7 +5034,12 @@ function getSubmittedViolationsForTransaction(reportActions: OnyxTypes.ReportAct return undefined; } - return Array.from(violationNames).join(', '); + const names = Array.from(violationNames); + if (!translate) { + return names.join(', '); + } + + return names.map((name) => getSubmittedViolationDisplayName(name, translate)).join(', '); } function getTypeOptions(translate: LocalizedTranslate, policies: OnyxCollection, currentUserLogin?: string) { diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 00503f023e54..3d5e04d57e1f 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -11314,9 +11314,9 @@ describe('SearchUIUtils', () => { }) as OnyxTypes.ReportAction; test('returns undefined when reportActions or transactionID is missing', () => { - expect(SearchUIUtils.getSubmittedViolationsForTransaction(undefined, transactionIDForViolations)).toBeUndefined(); - expect(SearchUIUtils.getSubmittedViolationsForTransaction([], transactionIDForViolations)).toBeUndefined(); - expect(SearchUIUtils.getSubmittedViolationsForTransaction([createSubmittedAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED)], undefined)).toBeUndefined(); + expect(SearchUIUtils.getSubmittedViolationsForTransaction(undefined, transactionIDForViolations, translateLocal)).toBeUndefined(); + expect(SearchUIUtils.getSubmittedViolationsForTransaction([], transactionIDForViolations, translateLocal)).toBeUndefined(); + expect(SearchUIUtils.getSubmittedViolationsForTransaction([createSubmittedAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED)], undefined, translateLocal)).toBeUndefined(); }); test('ignores non-submit report actions', () => { @@ -11330,29 +11330,31 @@ describe('SearchUIUtils', () => { }, }; - expect(SearchUIUtils.getSubmittedViolationsForTransaction([iouAction], transactionIDForViolations)).toBeUndefined(); + expect(SearchUIUtils.getSubmittedViolationsForTransaction([iouAction], transactionIDForViolations, translateLocal)).toBeUndefined(); }); - test('returns comma-separated violation names from a SUBMITTED action', () => { + test('returns comma-separated translated violation labels from a SUBMITTED action', () => { const submitAction = createSubmittedAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED, { transactions: { [transactionIDForViolations]: [{name: CONST.VIOLATIONS.MISSING_CATEGORY}, {name: CONST.VIOLATIONS.MISSING_COMMENT}], }, }); - expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitAction], transactionIDForViolations)).toBe( - `${CONST.VIOLATIONS.MISSING_CATEGORY}, ${CONST.VIOLATIONS.MISSING_COMMENT}`, + expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitAction], transactionIDForViolations, translateLocal)).toBe( + `${translateLocal('transactionViolations.missingCategory')}, ${translateLocal('transactionViolations.missingComment')}`, ); }); - test('includes violations from SUBMITTED_AND_CLOSED actions', () => { + test('includes translated labels from SUBMITTED_AND_CLOSED actions', () => { const submitAndCloseAction = createSubmittedAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED_AND_CLOSED, { transactions: { [transactionIDForViolations]: [{name: CONST.VIOLATIONS.RECEIPT_REQUIRED}], }, }); - expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitAndCloseAction], transactionIDForViolations)).toBe(CONST.VIOLATIONS.RECEIPT_REQUIRED); + expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitAndCloseAction], transactionIDForViolations, translateLocal)).toBe( + translateLocal('transactionViolations.receiptRequired'), + ); }); test('aggregates across multiple submit actions and dedupes by name', () => { @@ -11375,11 +11377,34 @@ describe('SearchUIUtils', () => { 'submit-2', ); - expect(SearchUIUtils.getSubmittedViolationsForTransaction([firstSubmit, secondSubmit], transactionIDForViolations)).toBe( - `${CONST.VIOLATIONS.MISSING_CATEGORY}, ${CONST.VIOLATIONS.MISSING_TAG}, ${CONST.VIOLATIONS.RECEIPT_REQUIRED}`, + expect(SearchUIUtils.getSubmittedViolationsForTransaction([firstSubmit, secondSubmit], transactionIDForViolations, translateLocal)).toBe( + `${translateLocal('transactionViolations.missingCategory')}, ${translateLocal('transactionViolations.missingTag')}, ${translateLocal('transactionViolations.receiptRequired')}`, + ); + }); + + test('translates fieldRequired via reportOrFieldViolations', () => { + const submitAction = createSubmittedAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED, { + transactions: { + [transactionIDForViolations]: [{name: CONST.VIOLATIONS.FIELD_REQUIRED}], + }, + }); + + expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitAction], transactionIDForViolations, translateLocal)).toBe( + translateLocal('reportOrFieldViolations.fieldRequired'), ); }); + test('falls back to the raw identifier when no short-name translation exists', () => { + const unknownViolationName = 'unknownSubmittedViolation'; + const submitAction = createSubmittedAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED, { + transactions: { + [transactionIDForViolations]: [{name: unknownViolationName}], + }, + }); + + expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitAction], transactionIDForViolations, translateLocal)).toBe(unknownViolationName); + }); + test('ignores violations for other transaction IDs', () => { const submitAction = createSubmittedAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED, { transactions: { @@ -11387,7 +11412,7 @@ describe('SearchUIUtils', () => { }, }); - expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitAction], transactionIDForViolations)).toBeUndefined(); + expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitAction], transactionIDForViolations, translateLocal)).toBeUndefined(); }); test('returns undefined when submit actions have no violations for the transaction', () => { @@ -11398,8 +11423,8 @@ describe('SearchUIUtils', () => { }); const submitActionWithoutViolations = createSubmittedAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED); - expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitAction], transactionIDForViolations)).toBeUndefined(); - expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitActionWithoutViolations], transactionIDForViolations)).toBeUndefined(); + expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitAction], transactionIDForViolations, translateLocal)).toBeUndefined(); + expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitActionWithoutViolations], transactionIDForViolations, translateLocal)).toBeUndefined(); }); }); From 4d4cea424739b48f154d353e6ce86d4c6aba6d10 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 28 Jul 2026 11:19:58 +0700 Subject: [PATCH 05/11] refactor code --- src/CONST/index.ts | 2 +- src/libs/SearchUIUtils.ts | 2 +- src/styles/utils/index.ts | 5 ++++- .../unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap | 1 - 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 457df4d8adee..246f641faa53 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6865,7 +6865,7 @@ const CONST = { VIOLATIONS: { column: this.TABLE_COLUMNS.VIOLATIONS, search: true, - reportView: true, + reportView: false, }, TAG_GL_CODE: { column: this.TABLE_COLUMNS.TAG_GL_CODE, diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 6ba1ca5f8d0a..d39d99cba0d6 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -6234,7 +6234,7 @@ function getColumnsToShow({ columns[CONST.SEARCH.TABLE_COLUMNS.TAG] = !isExpenseReportViewFromIOUReport; } - if (!Array.isArray(data)) { + if (!isExpenseReportView && !Array.isArray(data)) { const reportActions = Object.values(data[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transaction.reportID}`] ?? {}); if (getSubmittedViolationsForTransaction(reportActions, transaction.transactionID)) { columns[CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS] = true; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index b9327ec609dd..27c6a2ba260a 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1932,9 +1932,12 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ case CONST.SEARCH.TABLE_COLUMNS.GROUP_QUARTER: case CONST.SEARCH.TABLE_COLUMNS.TAG: case CONST.SEARCH.TABLE_COLUMNS.GROUP_TAG: - case CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS: columnWidth = {...getWidthStyle(variables.w36), ...styles.flex1}; break; + case CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS: + // Wider than category/tag so short violation labels are less likely to truncate. + columnWidth = {...getWidthStyle(variables.w130), ...styles.flex1}; + break; case CONST.SEARCH.TABLE_COLUMNS.TAX_AMOUNT: columnWidth = {...getWidthStyle(isTaxAmountColumnWide ? variables.w130 : variables.w96), ...styles.alignItemsEnd}; break; diff --git a/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap b/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap index ef38241eee27..d415876f37bb 100644 --- a/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap +++ b/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap @@ -13,7 +13,6 @@ exports[`Column availability single source of truth the derived picker lists mat "attendees", "totalPerAttendee", "tag", - "violations", "tagGLCode", "exchangeRate", "originalamount", From 36668378d0ead8cb8f8940360a849d12209a55e3 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 28 Jul 2026 11:24:30 +0700 Subject: [PATCH 06/11] update unit test --- tests/unit/Search/SearchQueryUtilsTest.ts | 20 +++ tests/unit/Search/SearchUIUtilsTest.ts | 172 ++++++++++++++++++++++ tests/unit/SearchRouterNavigationTest.ts | 1 + 3 files changed, 193 insertions(+) diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index 79b59f0ececd..f2a6f1b4bc16 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -491,6 +491,26 @@ describe('SearchQueryUtils', () => { expect(result).toEqual('type:expense tag:Engineering'); }); + test('builds Violations by submitter query from filter form values', () => { + const filterValues: Partial = { + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + groupBy: CONST.SEARCH.GROUP_BY.FROM, + dateOn: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, + has: [CONST.SEARCH.HAS_VALUES.SUBMITTED_VIOLATION], + view: CONST.SEARCH.VIEW.TABLE, + limit: String(CONST.SEARCH.TOP_SEARCH_LIMIT), + }; + + const result = buildQueryStringFromFilterFormValues(filterValues, { + sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_EXPENSES, + sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + }); + + expect(result).toEqual( + `sortBy:${CONST.SEARCH.TABLE_COLUMNS.GROUP_EXPENSES} sortOrder:${CONST.SEARCH.SORT_ORDER.DESC} type:${CONST.SEARCH.DATA_TYPES.EXPENSE} groupBy:${CONST.SEARCH.GROUP_BY.FROM} view:${CONST.SEARCH.VIEW.TABLE} has:${CONST.SEARCH.HAS_VALUES.SUBMITTED_VIOLATION} date:${CONST.SEARCH.DATE_PRESETS.LAST_MONTH} limit:${CONST.SEARCH.TOP_SEARCH_LIMIT}`, + ); + }); + test('empty filter values', () => { const filterValues: Partial = {}; diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 3d5e04d57e1f..8e22e3205646 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -8941,6 +8941,114 @@ describe('SearchUIUtils', () => { expect(response.visibility.topCategories).toBe(false); }); + test('Should show Violations by submitter for Admin on a Control policy with rules and 2+ members', () => { + const policies: OnyxCollection = { + [`policy_${policyID}`]: createMock({ + id: policyID, + type: CONST.POLICY.TYPE.CORPORATE, + role: CONST.POLICY.ROLE.ADMIN, + areRulesEnabled: true, + employeeList: { + 'employee1@policy.com': {email: 'employee1@policy.com'}, + 'employee2@policy.com': {email: 'employee2@policy.com'}, + }, + }), + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); + expect(response.visibility[CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]).toBe(true); + }); + + test('Should show Violations by submitter for Auditor on a Control policy with rules and 2+ members', () => { + const auditorEmail = 'auditor@policy.com'; + const policies: OnyxCollection = { + [`policy_${policyID}`]: createMock({ + id: policyID, + type: CONST.POLICY.TYPE.CORPORATE, + role: CONST.POLICY.ROLE.AUDITOR, + areRulesEnabled: true, + employeeList: { + 'employee1@policy.com': {email: 'employee1@policy.com'}, + 'employee2@policy.com': {email: 'employee2@policy.com'}, + }, + }), + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(auditorEmail, {}, policies, undefined); + expect(response.visibility[CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]).toBe(true); + }); + + test('Should hide Violations by submitter for User role even on a Control policy with rules', () => { + const policies: OnyxCollection = { + [`policy_${policyID}`]: createMock({ + id: policyID, + type: CONST.POLICY.TYPE.CORPORATE, + role: CONST.POLICY.ROLE.USER, + areRulesEnabled: true, + employeeList: { + 'employee1@policy.com': {email: 'employee1@policy.com'}, + 'employee2@policy.com': {email: 'employee2@policy.com'}, + }, + }), + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility('user@policy.com', {}, policies, undefined); + expect(response.visibility[CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]).toBe(false); + }); + + test('Should hide Violations by submitter when rules are disabled', () => { + const policies: OnyxCollection = { + [`policy_${policyID}`]: createMock({ + id: policyID, + type: CONST.POLICY.TYPE.CORPORATE, + role: CONST.POLICY.ROLE.ADMIN, + areRulesEnabled: false, + employeeList: { + 'employee1@policy.com': {email: 'employee1@policy.com'}, + 'employee2@policy.com': {email: 'employee2@policy.com'}, + }, + }), + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); + expect(response.visibility[CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]).toBe(false); + }); + + test('Should hide Violations by submitter for Collect/Team policies even when areRulesEnabled is true', () => { + const policies: OnyxCollection = { + [`policy_${policyID}`]: createMock({ + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.ADMIN, + areRulesEnabled: true, + employeeList: { + 'employee1@policy.com': {email: 'employee1@policy.com'}, + 'employee2@policy.com': {email: 'employee2@policy.com'}, + }, + }), + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); + expect(response.visibility[CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]).toBe(false); + }); + + test('Should hide Violations by submitter when the workspace has fewer than 2 members', () => { + const policies: OnyxCollection = { + [`policy_${policyID}`]: createMock({ + id: policyID, + type: CONST.POLICY.TYPE.CORPORATE, + role: CONST.POLICY.ROLE.ADMIN, + areRulesEnabled: true, + employeeList: { + 'employee1@policy.com': {email: 'employee1@policy.com'}, + }, + }), + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); + expect(response.visibility[CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]).toBe(false); + }); + test('Should show Spend Over Time for Admin role in paid policy', () => { const policyKey = `policy_${policyID}`; @@ -9124,6 +9232,63 @@ describe('SearchUIUtils', () => { expect(searchQuery).toContain(`view:${CONST.SEARCH.VIEW.PIE}`); }); + test('Should return Violations by submitter search with correct properties', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined); + const violationsBySubmitterSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]; + + expect(violationsBySubmitterSearch).toBeDefined(); + expect(violationsBySubmitterSearch.key).toBe(CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER); + expect(violationsBySubmitterSearch.translationPath).toBe('search.tabs.violationsBySubmitter'); + expect(violationsBySubmitterSearch.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); + expect(violationsBySubmitterSearch.icon).toBe('UserEye'); + }); + + test('Should return Violations by submitter search query with correct parameters', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined); + const violationsBySubmitterSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]; + const searchQueryJSON = violationsBySubmitterSearch.searchQueryJSON; + + expect(searchQueryJSON).toBeDefined(); + expect(searchQueryJSON?.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); + expect(searchQueryJSON?.groupBy).toBe(CONST.SEARCH.GROUP_BY.FROM); + expect(searchQueryJSON?.view).toBe(CONST.SEARCH.VIEW.TABLE); + expect(searchQueryJSON?.sortBy).toBe(CONST.SEARCH.TABLE_COLUMNS.GROUP_EXPENSES); + expect(searchQueryJSON?.sortOrder).toBe(CONST.SEARCH.SORT_ORDER.DESC); + + const dateFilter = searchQueryJSON?.flatFilters?.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); + expect(dateFilter).toBeDefined(); + expect(dateFilter?.filters?.some((filter) => filter.value === CONST.SEARCH.DATE_PRESETS.LAST_MONTH)).toBe(true); + + const hasFilter = searchQueryJSON?.flatFilters?.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.HAS); + expect(hasFilter).toBeDefined(); + expect(hasFilter?.filters?.some((filter) => filter.value === CONST.SEARCH.HAS_VALUES.SUBMITTED_VIOLATION)).toBe(true); + + expect(searchQueryJSON?.limit).toBe(CONST.SEARCH.TOP_SEARCH_LIMIT); + }); + + test('Should return Violations by submitter search query string with correct format', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined); + const violationsBySubmitterSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]; + const searchQuery = violationsBySubmitterSearch.searchQuery; + + expect(searchQuery).toContain(`type:${CONST.SEARCH.DATA_TYPES.EXPENSE}`); + expect(searchQuery).toContain(`groupBy:${CONST.SEARCH.GROUP_BY.FROM}`); + expect(searchQuery).toContain(`date:${CONST.SEARCH.DATE_PRESETS.LAST_MONTH}`); + expect(searchQuery).toContain(`has:${CONST.SEARCH.HAS_VALUES.SUBMITTED_VIOLATION}`); + expect(searchQuery).toContain(`view:${CONST.SEARCH.VIEW.TABLE}`); + expect(searchQuery).toContain(`limit:${CONST.SEARCH.TOP_SEARCH_LIMIT}`); + expect(searchQuery).toContain(`sortBy:${CONST.SEARCH.TABLE_COLUMNS.GROUP_EXPENSES}`); + expect(searchQuery).toContain(`sortOrder:${CONST.SEARCH.SORT_ORDER.DESC}`); + }); + + test('Should return Violations by submitter search with valid hash', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined); + const violationsBySubmitterSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]; + + expect(violationsBySubmitterSearch.hash).toBeGreaterThan(0); + expect(violationsBySubmitterSearch.similarSearchHash).toBeGreaterThan(0); + }); + test('Should show Top Spenders for workflow approver (submitsTo) in paid policy', () => { const workflowApproverEmail = 'workflow-approver@policy.com'; const policyKey = `policy_${policyID}`; @@ -9420,6 +9585,13 @@ describe('SearchUIUtils', () => { expect(topMerchants.searchQueryJSON?.sortBy).toBe(CONST.SEARCH.TABLE_COLUMNS.GROUP_TOTAL); expect(topMerchants.searchQueryJSON?.sortOrder).toBe(CONST.SEARCH.SORT_ORDER.DESC); }); + + test('Should default Violations by submitter to sortBy groupExpenses and sortOrder desc', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID); + const violationsBySubmitter = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]; + expect(violationsBySubmitter.searchQueryJSON?.sortBy).toBe(CONST.SEARCH.TABLE_COLUMNS.GROUP_EXPENSES); + expect(violationsBySubmitter.searchQueryJSON?.sortOrder).toBe(CONST.SEARCH.SORT_ORDER.DESC); + }); }); describe('Test getColumnsToShow', () => { diff --git a/tests/unit/SearchRouterNavigationTest.ts b/tests/unit/SearchRouterNavigationTest.ts index 6d0d58b6b0bc..8c80aa9871e9 100644 --- a/tests/unit/SearchRouterNavigationTest.ts +++ b/tests/unit/SearchRouterNavigationTest.ts @@ -100,6 +100,7 @@ const spendIcons = { Pencil: mockIcon, ThumbsUp: mockIcon, CheckCircle: mockIcon, + UserEye: mockIcon, }; function createSpendMenuItem( From 7f502aaac875427f012f8c923827fdeef232d9ad Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 28 Jul 2026 11:31:04 +0700 Subject: [PATCH 07/11] remove disable eslint --- src/libs/SearchUIUtils.ts | 22 +++++++++++++++++----- src/libs/Violations/ViolationsUtils.ts | 9 ++++++++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index d39d99cba0d6..5d8db05247af 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -227,6 +227,7 @@ import { import getFormattedPostedDate from './TransactionUtils/getFormattedPostedDate'; import shouldShowTransactionPostedYear from './TransactionUtils/shouldShowTransactionPostedYear'; import {isInvalidMerchantValue} from './ValidationUtils'; +import {isValidViolationName} from './Violations/ViolationsUtils'; type ColumnSortMapping = Partial>; type ColumnVisibility = Partial>; @@ -4939,7 +4940,7 @@ function getHasOptions(translate: LocalizedTranslate, type: SearchDataTypes) { } /** Parameter-free short labels used when rendering submitted violations in Search columns. */ -const SUBMITTED_TRANSACTION_VIOLATION_SHORT_NAMES = new Set([ +const SUBMITTED_TRANSACTION_VIOLATION_SHORT_NAMES = [ CONST.VIOLATIONS.ALL_TAG_LEVELS_REQUIRED, CONST.VIOLATIONS.AUTO_REPORTED_REJECTED_EXPENSE, CONST.VIOLATIONS.BILLABLE_EXPENSE, @@ -4981,20 +4982,31 @@ const SUBMITTED_TRANSACTION_VIOLATION_SHORT_NAMES = new Set([ CONST.VIOLATIONS.TAX_OUT_OF_POLICY, CONST.VIOLATIONS.TAX_RATE_CHANGED, CONST.VIOLATIONS.TAX_REQUIRED, -]); +] as const satisfies ReadonlyArray>; + +type SubmittedTransactionViolationShortName = TupleToUnion; + +const SUBMITTED_TRANSACTION_VIOLATION_SHORT_NAME_SET = new Set(SUBMITTED_TRANSACTION_VIOLATION_SHORT_NAMES); + +function isSubmittedTransactionViolationShortName(name: ValueOf): name is SubmittedTransactionViolationShortName { + return SUBMITTED_TRANSACTION_VIOLATION_SHORT_NAME_SET.has(name); +} /** * Returns a parameter-free display label for a submitted violation name. * Falls back to the raw identifier when no short-name translation exists. */ function getSubmittedViolationDisplayName(violationName: string, translate: LocalizedTranslate): string { + if (!isValidViolationName(violationName)) { + return violationName; + } + if (violationName === CONST.VIOLATIONS.FIELD_REQUIRED) { return translate('reportOrFieldViolations.fieldRequired'); } - if (SUBMITTED_TRANSACTION_VIOLATION_SHORT_NAMES.has(violationName)) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - return translate(`transactionViolations.${violationName}` as TranslationPaths); + if (isSubmittedTransactionViolationShortName(violationName)) { + return translate(`transactionViolations.${violationName}`); } return violationName; diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 53dd27f6aa0e..b3494973ebf9 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -67,6 +67,13 @@ type ViolationTranslationParams = { distanceUnit?: Unit; }; +/** + * Type guard that narrows a string to a known transaction violation name. + */ +function isValidViolationName(name: string): name is ViolationName { + return Object.values(CONST.VIOLATIONS).some((violationName) => violationName === name); +} + /** * Filters out receiptRequired violation when itemizedReceiptRequired is also present. * Itemized receipt requirement supersedes regular receipt requirement. @@ -1190,6 +1197,6 @@ const ViolationsUtils = { }, }; -export {getIsViolationFixed, isHardViolationOrRateDateWarning, syncCustomUnitRateOutOfDateRangeViolation}; +export {getIsViolationFixed, isHardViolationOrRateDateWarning, isValidViolationName, syncCustomUnitRateOutOfDateRangeViolation}; export default ViolationsUtils; export {filterReceiptViolations}; From 348611454e479da99467cad7cb038389fee29106 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 29 Jul 2026 13:50:35 +0700 Subject: [PATCH 08/11] move translation to existing ke --- .../TransactionItemRowWide.tsx | 3 +- src/languages/de.ts | 94 +++++++++--------- src/languages/en.ts | 98 +++++++++---------- src/languages/es.ts | 94 +++++++++--------- src/languages/fr.ts | 94 +++++++++--------- src/languages/it.ts | 94 +++++++++--------- src/languages/ja.ts | 94 +++++++++--------- src/languages/nl.ts | 94 +++++++++--------- src/languages/pl.ts | 94 +++++++++--------- src/languages/pt-BR.ts | 94 +++++++++--------- src/languages/zh-hans.ts | 94 +++++++++--------- src/libs/SearchUIUtils.ts | 7 +- tests/unit/Search/SearchUIUtilsTest.ts | 12 +-- 13 files changed, 453 insertions(+), 513 deletions(-) diff --git a/src/components/TransactionItemRow/TransactionItemRowWide.tsx b/src/components/TransactionItemRow/TransactionItemRowWide.tsx index a4802c48cc58..fc6a04ea5559 100644 --- a/src/components/TransactionItemRow/TransactionItemRowWide.tsx +++ b/src/components/TransactionItemRow/TransactionItemRowWide.tsx @@ -155,6 +155,7 @@ function TransactionItemRowWide({ const submitterUserID = reportForCustomColumns?.submitterUserID; const submitterPayrollID = reportForCustomColumns?.submitterPayrollID; const orderDealNumbers = reportForCustomColumns?.orderDealNumbers; + const submittedViolations = getSubmittedViolationsForTransaction(reportActions, transactionItem.transactionID, translate); const renderColumn = (column: SearchColumnType): React.ReactNode => { switch (column) { @@ -206,7 +207,7 @@ function TransactionItemRowWide({ key={column} style={[StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS)]} > - + ); case CONST.SEARCH.TABLE_COLUMNS.TAG_GL_CODE: diff --git a/src/languages/de.ts b/src/languages/de.ts index 7ad3f248944f..05d1b8af162e 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -9781,58 +9781,52 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc customUnitRateOutOfDateRange: ({startDate, endDate}: {startDate: string; endDate: string}) => `Preis ist nur gültig von ${startDate} bis ${endDate}`, customUnitRateOutOfDateRangeStartOnly: ({startDate}: {startDate: string}) => `Der Satz gilt erst ab dem ${startDate}`, customUnitRateOutOfDateRangeEndOnly: ({endDate}: {endDate: string}) => `Satz gilt nur bis ${endDate}`, + shortName: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + fieldRequired: 'Report field required', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, }, - /** - * Parameter-free labels for submitted transaction violations shown in Search table columns. - * Prefer these over `violations.*` when violation data/context is unavailable. - */ - transactionViolations: { - allTagLevelsRequired: 'All tags required', - autoReportedRejectedExpense: 'Expense rejected', - billableExpense: 'Billable no longer valid', - cashExpenseWithNoReceipt: 'Receipt required', - categoryOutOfPolicy: 'Category no longer valid', - companyCardRequired: 'Company card required', - conversionSurcharge: 'Conversion surcharge applied', - customUnitOutOfPolicy: 'Rate not valid for workspace', - customUnitRateOutOfDateRange: 'Rate outside valid dates', - duplicatedTransaction: 'Potential duplicate', - futureDate: 'Future date not allowed', - hold: 'Expense on hold', - inactiveVendor: 'Vendor no longer valid', - increasedDistance: 'Distance exceeds route', - invoiceMarkup: 'Invoice marked up', - itemizedReceiptRequired: 'Itemized receipt required', - maxAge: 'Expense too old', - missingAttendees: 'Attendees required', - missingCategory: 'Missing category', - missingComment: 'Description required', - missingTag: 'Missing tag', - modifiedAmount: 'Amount modified', - modifiedDate: 'Date modified', - noRoute: 'No valid route', - nonExpensiworksExpense: 'Non-Expensiworks expense', - overAutoApprovalLimit: 'Over auto-approval limit', - overCategoryLimit: 'Over category limit', - overLimit: 'Over limit', - overTripLimit: 'Over trip limit', - perDayLimit: 'Over daily limit', - prohibitedExpense: 'Prohibited expense', - receiptGeneratedWithAI: 'Possible AI-generated receipt', - receiptNotSmartScanned: 'Receipt added manually', - receiptRequired: 'Receipt required', - rter: 'Awaiting card match', - smartscanFailed: 'Receipt scanning failed', - someTagLevelsRequired: 'Tag required', - taxAmountChanged: 'Tax amount modified', - taxOutOfPolicy: 'Tax rate no longer valid', - taxRateChanged: 'Tax rate modified', - taxRequired: 'Missing tax rate', - }, - reportOrFieldViolations: { - fieldRequired: 'Report field required', - }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} ist erforderlich`, reportContainsExpensesWithViolations: 'Der Bericht enthält Ausgaben mit Verstößen.', diff --git a/src/languages/en.ts b/src/languages/en.ts index b1b18b9a1770..5703064d7ba9 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -9922,56 +9922,54 @@ const translations = { resolvedDuplicates: 'resolved the duplicate', companyCardRequired: 'Company card purchases required', noRoute: 'Please select a valid address', - }, - /** - * Parameter-free labels for submitted transaction violations shown in Search table columns. - * Prefer these over `violations.*` when violation data/context is unavailable. - */ - transactionViolations: { - allTagLevelsRequired: 'All tags required', - autoReportedRejectedExpense: 'Expense rejected', - billableExpense: 'Billable no longer valid', - cashExpenseWithNoReceipt: 'Receipt required', - categoryOutOfPolicy: 'Category no longer valid', - companyCardRequired: 'Company card required', - conversionSurcharge: 'Conversion surcharge applied', - customUnitOutOfPolicy: 'Rate not valid for workspace', - customUnitRateOutOfDateRange: 'Rate outside valid dates', - duplicatedTransaction: 'Potential duplicate', - futureDate: 'Future date not allowed', - hold: 'Expense on hold', - inactiveVendor: 'Vendor no longer valid', - increasedDistance: 'Distance exceeds route', - invoiceMarkup: 'Invoice marked up', - itemizedReceiptRequired: 'Itemized receipt required', - maxAge: 'Expense too old', - missingAttendees: 'Attendees required', - missingCategory: 'Missing category', - missingComment: 'Description required', - missingTag: 'Missing tag', - modifiedAmount: 'Amount modified', - modifiedDate: 'Date modified', - noRoute: 'No valid route', - nonExpensiworksExpense: 'Non-Expensiworks expense', - overAutoApprovalLimit: 'Over auto-approval limit', - overCategoryLimit: 'Over category limit', - overLimit: 'Over limit', - overTripLimit: 'Over trip limit', - perDayLimit: 'Over daily limit', - prohibitedExpense: 'Prohibited expense', - receiptGeneratedWithAI: 'Possible AI-generated receipt', - receiptNotSmartScanned: 'Receipt added manually', - receiptRequired: 'Receipt required', - rter: 'Awaiting card match', - smartscanFailed: 'Receipt scanning failed', - someTagLevelsRequired: 'Tag required', - taxAmountChanged: 'Tax amount modified', - taxOutOfPolicy: 'Tax rate no longer valid', - taxRateChanged: 'Tax rate modified', - taxRequired: 'Missing tax rate', - }, - reportOrFieldViolations: { - fieldRequired: 'Report field required', + /** + * Parameter-free labels for submitted violations shown in Search table columns. + * Prefer these over sibling `violations.*` keys when violation data/context is unavailable. + */ + shortName: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + fieldRequired: 'Report field required', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} is required`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 37c7ee932f9f..04570181177e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -9954,58 +9954,52 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, customUnitRateOutOfDateRange: ({startDate, endDate}: {startDate: string; endDate: string}) => `La tasa solo es válida desde ${startDate} hasta ${endDate}`, customUnitRateOutOfDateRangeStartOnly: ({startDate}: {startDate: string}) => `La tasa solo es válida desde ${startDate}`, customUnitRateOutOfDateRangeEndOnly: ({endDate}: {endDate: string}) => `La tasa solo es válida hasta ${endDate}`, + shortName: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + fieldRequired: 'Report field required', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, }, - /** - * Parameter-free labels for submitted transaction violations shown in Search table columns. - * Prefer these over `violations.*` when violation data/context is unavailable. - */ - transactionViolations: { - allTagLevelsRequired: 'All tags required', - autoReportedRejectedExpense: 'Expense rejected', - billableExpense: 'Billable no longer valid', - cashExpenseWithNoReceipt: 'Receipt required', - categoryOutOfPolicy: 'Category no longer valid', - companyCardRequired: 'Company card required', - conversionSurcharge: 'Conversion surcharge applied', - customUnitOutOfPolicy: 'Rate not valid for workspace', - customUnitRateOutOfDateRange: 'Rate outside valid dates', - duplicatedTransaction: 'Potential duplicate', - futureDate: 'Future date not allowed', - hold: 'Expense on hold', - inactiveVendor: 'Vendor no longer valid', - increasedDistance: 'Distance exceeds route', - invoiceMarkup: 'Invoice marked up', - itemizedReceiptRequired: 'Itemized receipt required', - maxAge: 'Expense too old', - missingAttendees: 'Attendees required', - missingCategory: 'Missing category', - missingComment: 'Description required', - missingTag: 'Missing tag', - modifiedAmount: 'Amount modified', - modifiedDate: 'Date modified', - noRoute: 'No valid route', - nonExpensiworksExpense: 'Non-Expensiworks expense', - overAutoApprovalLimit: 'Over auto-approval limit', - overCategoryLimit: 'Over category limit', - overLimit: 'Over limit', - overTripLimit: 'Over trip limit', - perDayLimit: 'Over daily limit', - prohibitedExpense: 'Prohibited expense', - receiptGeneratedWithAI: 'Possible AI-generated receipt', - receiptNotSmartScanned: 'Receipt added manually', - receiptRequired: 'Receipt required', - rter: 'Awaiting card match', - smartscanFailed: 'Receipt scanning failed', - someTagLevelsRequired: 'Tag required', - taxAmountChanged: 'Tax amount modified', - taxOutOfPolicy: 'Tax rate no longer valid', - taxRateChanged: 'Tax rate modified', - taxRequired: 'Missing tax rate', - }, - reportOrFieldViolations: { - fieldRequired: 'Report field required', - }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName) => `${fieldName} es obligatorio`, reportContainsExpensesWithViolations: 'El informe contiene gastos con violaciones.', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 5ffffd669148..a06500d8495f 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -9817,58 +9817,52 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e customUnitRateOutOfDateRange: ({startDate, endDate}: {startDate: string; endDate: string}) => `Le taux n’est valable que du ${startDate} au ${endDate}`, customUnitRateOutOfDateRangeStartOnly: ({startDate}: {startDate: string}) => `Le taux n’est valable qu’à partir du ${startDate}`, customUnitRateOutOfDateRangeEndOnly: ({endDate}: {endDate: string}) => `Le taux n’est valable que jusqu’au ${endDate}`, + shortName: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + fieldRequired: 'Report field required', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, }, - /** - * Parameter-free labels for submitted transaction violations shown in Search table columns. - * Prefer these over `violations.*` when violation data/context is unavailable. - */ - transactionViolations: { - allTagLevelsRequired: 'All tags required', - autoReportedRejectedExpense: 'Expense rejected', - billableExpense: 'Billable no longer valid', - cashExpenseWithNoReceipt: 'Receipt required', - categoryOutOfPolicy: 'Category no longer valid', - companyCardRequired: 'Company card required', - conversionSurcharge: 'Conversion surcharge applied', - customUnitOutOfPolicy: 'Rate not valid for workspace', - customUnitRateOutOfDateRange: 'Rate outside valid dates', - duplicatedTransaction: 'Potential duplicate', - futureDate: 'Future date not allowed', - hold: 'Expense on hold', - inactiveVendor: 'Vendor no longer valid', - increasedDistance: 'Distance exceeds route', - invoiceMarkup: 'Invoice marked up', - itemizedReceiptRequired: 'Itemized receipt required', - maxAge: 'Expense too old', - missingAttendees: 'Attendees required', - missingCategory: 'Missing category', - missingComment: 'Description required', - missingTag: 'Missing tag', - modifiedAmount: 'Amount modified', - modifiedDate: 'Date modified', - noRoute: 'No valid route', - nonExpensiworksExpense: 'Non-Expensiworks expense', - overAutoApprovalLimit: 'Over auto-approval limit', - overCategoryLimit: 'Over category limit', - overLimit: 'Over limit', - overTripLimit: 'Over trip limit', - perDayLimit: 'Over daily limit', - prohibitedExpense: 'Prohibited expense', - receiptGeneratedWithAI: 'Possible AI-generated receipt', - receiptNotSmartScanned: 'Receipt added manually', - receiptRequired: 'Receipt required', - rter: 'Awaiting card match', - smartscanFailed: 'Receipt scanning failed', - someTagLevelsRequired: 'Tag required', - taxAmountChanged: 'Tax amount modified', - taxOutOfPolicy: 'Tax rate no longer valid', - taxRateChanged: 'Tax rate modified', - taxRequired: 'Missing tax rate', - }, - reportOrFieldViolations: { - fieldRequired: 'Report field required', - }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} est obligatoire`, reportContainsExpensesWithViolations: 'La note de frais contient des dépenses avec des violations.', diff --git a/src/languages/it.ts b/src/languages/it.ts index 4ba0704cd3a6..e281c8fba352 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -9763,58 +9763,52 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, customUnitRateOutOfDateRange: ({startDate, endDate}: {startDate: string; endDate: string}) => `La tariffa è valida solo dal ${startDate} al ${endDate}`, customUnitRateOutOfDateRangeStartOnly: ({startDate}: {startDate: string}) => `La tariffa è valida solo a partire dal ${startDate}`, customUnitRateOutOfDateRangeEndOnly: ({endDate}: {endDate: string}) => `La tariffa è valida solo fino al ${endDate}`, + shortName: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + fieldRequired: 'Report field required', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, }, - /** - * Parameter-free labels for submitted transaction violations shown in Search table columns. - * Prefer these over `violations.*` when violation data/context is unavailable. - */ - transactionViolations: { - allTagLevelsRequired: 'All tags required', - autoReportedRejectedExpense: 'Expense rejected', - billableExpense: 'Billable no longer valid', - cashExpenseWithNoReceipt: 'Receipt required', - categoryOutOfPolicy: 'Category no longer valid', - companyCardRequired: 'Company card required', - conversionSurcharge: 'Conversion surcharge applied', - customUnitOutOfPolicy: 'Rate not valid for workspace', - customUnitRateOutOfDateRange: 'Rate outside valid dates', - duplicatedTransaction: 'Potential duplicate', - futureDate: 'Future date not allowed', - hold: 'Expense on hold', - inactiveVendor: 'Vendor no longer valid', - increasedDistance: 'Distance exceeds route', - invoiceMarkup: 'Invoice marked up', - itemizedReceiptRequired: 'Itemized receipt required', - maxAge: 'Expense too old', - missingAttendees: 'Attendees required', - missingCategory: 'Missing category', - missingComment: 'Description required', - missingTag: 'Missing tag', - modifiedAmount: 'Amount modified', - modifiedDate: 'Date modified', - noRoute: 'No valid route', - nonExpensiworksExpense: 'Non-Expensiworks expense', - overAutoApprovalLimit: 'Over auto-approval limit', - overCategoryLimit: 'Over category limit', - overLimit: 'Over limit', - overTripLimit: 'Over trip limit', - perDayLimit: 'Over daily limit', - prohibitedExpense: 'Prohibited expense', - receiptGeneratedWithAI: 'Possible AI-generated receipt', - receiptNotSmartScanned: 'Receipt added manually', - receiptRequired: 'Receipt required', - rter: 'Awaiting card match', - smartscanFailed: 'Receipt scanning failed', - someTagLevelsRequired: 'Tag required', - taxAmountChanged: 'Tax amount modified', - taxOutOfPolicy: 'Tax rate no longer valid', - taxRateChanged: 'Tax rate modified', - taxRequired: 'Missing tax rate', - }, - reportOrFieldViolations: { - fieldRequired: 'Report field required', - }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} è obbligatorio`, reportContainsExpensesWithViolations: 'Il report contiene spese con violazioni.', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index f2da242740cc..d8d1962a725a 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -9632,58 +9632,52 @@ ${reportName}`, customUnitRateOutOfDateRange: ({startDate, endDate}: {startDate: string; endDate: string}) => `料金は${startDate}から${endDate}までのみ有効です`, customUnitRateOutOfDateRangeStartOnly: ({startDate}: {startDate: string}) => `料金は${startDate}からのみ有効です`, customUnitRateOutOfDateRangeEndOnly: ({endDate}: {endDate: string}) => `料金は${endDate}までのみ有効です`, + shortName: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + fieldRequired: 'Report field required', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, }, - /** - * Parameter-free labels for submitted transaction violations shown in Search table columns. - * Prefer these over `violations.*` when violation data/context is unavailable. - */ - transactionViolations: { - allTagLevelsRequired: 'All tags required', - autoReportedRejectedExpense: 'Expense rejected', - billableExpense: 'Billable no longer valid', - cashExpenseWithNoReceipt: 'Receipt required', - categoryOutOfPolicy: 'Category no longer valid', - companyCardRequired: 'Company card required', - conversionSurcharge: 'Conversion surcharge applied', - customUnitOutOfPolicy: 'Rate not valid for workspace', - customUnitRateOutOfDateRange: 'Rate outside valid dates', - duplicatedTransaction: 'Potential duplicate', - futureDate: 'Future date not allowed', - hold: 'Expense on hold', - inactiveVendor: 'Vendor no longer valid', - increasedDistance: 'Distance exceeds route', - invoiceMarkup: 'Invoice marked up', - itemizedReceiptRequired: 'Itemized receipt required', - maxAge: 'Expense too old', - missingAttendees: 'Attendees required', - missingCategory: 'Missing category', - missingComment: 'Description required', - missingTag: 'Missing tag', - modifiedAmount: 'Amount modified', - modifiedDate: 'Date modified', - noRoute: 'No valid route', - nonExpensiworksExpense: 'Non-Expensiworks expense', - overAutoApprovalLimit: 'Over auto-approval limit', - overCategoryLimit: 'Over category limit', - overLimit: 'Over limit', - overTripLimit: 'Over trip limit', - perDayLimit: 'Over daily limit', - prohibitedExpense: 'Prohibited expense', - receiptGeneratedWithAI: 'Possible AI-generated receipt', - receiptNotSmartScanned: 'Receipt added manually', - receiptRequired: 'Receipt required', - rter: 'Awaiting card match', - smartscanFailed: 'Receipt scanning failed', - someTagLevelsRequired: 'Tag required', - taxAmountChanged: 'Tax amount modified', - taxOutOfPolicy: 'Tax rate no longer valid', - taxRateChanged: 'Tax rate modified', - taxRequired: 'Missing tax rate', - }, - reportOrFieldViolations: { - fieldRequired: 'Report field required', - }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} は必須です`, reportContainsExpensesWithViolations: 'レポートに違反のある経費が含まれています。', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 92b90a43d143..651e5a817e98 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -9734,58 +9734,52 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, customUnitRateOutOfDateRange: ({startDate, endDate}: {startDate: string; endDate: string}) => `Tarief is alleen geldig van ${startDate} tot ${endDate}`, customUnitRateOutOfDateRangeStartOnly: ({startDate}: {startDate: string}) => `Tarief is alleen geldig vanaf ${startDate}`, customUnitRateOutOfDateRangeEndOnly: ({endDate}: {endDate: string}) => `Tarief is alleen geldig tot ${endDate}`, + shortName: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + fieldRequired: 'Report field required', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, }, - /** - * Parameter-free labels for submitted transaction violations shown in Search table columns. - * Prefer these over `violations.*` when violation data/context is unavailable. - */ - transactionViolations: { - allTagLevelsRequired: 'All tags required', - autoReportedRejectedExpense: 'Expense rejected', - billableExpense: 'Billable no longer valid', - cashExpenseWithNoReceipt: 'Receipt required', - categoryOutOfPolicy: 'Category no longer valid', - companyCardRequired: 'Company card required', - conversionSurcharge: 'Conversion surcharge applied', - customUnitOutOfPolicy: 'Rate not valid for workspace', - customUnitRateOutOfDateRange: 'Rate outside valid dates', - duplicatedTransaction: 'Potential duplicate', - futureDate: 'Future date not allowed', - hold: 'Expense on hold', - inactiveVendor: 'Vendor no longer valid', - increasedDistance: 'Distance exceeds route', - invoiceMarkup: 'Invoice marked up', - itemizedReceiptRequired: 'Itemized receipt required', - maxAge: 'Expense too old', - missingAttendees: 'Attendees required', - missingCategory: 'Missing category', - missingComment: 'Description required', - missingTag: 'Missing tag', - modifiedAmount: 'Amount modified', - modifiedDate: 'Date modified', - noRoute: 'No valid route', - nonExpensiworksExpense: 'Non-Expensiworks expense', - overAutoApprovalLimit: 'Over auto-approval limit', - overCategoryLimit: 'Over category limit', - overLimit: 'Over limit', - overTripLimit: 'Over trip limit', - perDayLimit: 'Over daily limit', - prohibitedExpense: 'Prohibited expense', - receiptGeneratedWithAI: 'Possible AI-generated receipt', - receiptNotSmartScanned: 'Receipt added manually', - receiptRequired: 'Receipt required', - rter: 'Awaiting card match', - smartscanFailed: 'Receipt scanning failed', - someTagLevelsRequired: 'Tag required', - taxAmountChanged: 'Tax amount modified', - taxOutOfPolicy: 'Tax rate no longer valid', - taxRateChanged: 'Tax rate modified', - taxRequired: 'Missing tax rate', - }, - reportOrFieldViolations: { - fieldRequired: 'Report field required', - }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} is verplicht`, reportContainsExpensesWithViolations: 'Rapport bevat onkosten met overtredingen.', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 3d4df7930deb..24ab6c030f00 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -9707,58 +9707,52 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, customUnitRateOutOfDateRange: ({startDate, endDate}: {startDate: string; endDate: string}) => `Stawka jest ważna tylko od ${startDate} do ${endDate}`, customUnitRateOutOfDateRangeStartOnly: ({startDate}: {startDate: string}) => `Stawka jest ważna tylko od ${startDate}`, customUnitRateOutOfDateRangeEndOnly: ({endDate}: {endDate: string}) => `Stawka jest ważna tylko do ${endDate}`, + shortName: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + fieldRequired: 'Report field required', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, }, - /** - * Parameter-free labels for submitted transaction violations shown in Search table columns. - * Prefer these over `violations.*` when violation data/context is unavailable. - */ - transactionViolations: { - allTagLevelsRequired: 'All tags required', - autoReportedRejectedExpense: 'Expense rejected', - billableExpense: 'Billable no longer valid', - cashExpenseWithNoReceipt: 'Receipt required', - categoryOutOfPolicy: 'Category no longer valid', - companyCardRequired: 'Company card required', - conversionSurcharge: 'Conversion surcharge applied', - customUnitOutOfPolicy: 'Rate not valid for workspace', - customUnitRateOutOfDateRange: 'Rate outside valid dates', - duplicatedTransaction: 'Potential duplicate', - futureDate: 'Future date not allowed', - hold: 'Expense on hold', - inactiveVendor: 'Vendor no longer valid', - increasedDistance: 'Distance exceeds route', - invoiceMarkup: 'Invoice marked up', - itemizedReceiptRequired: 'Itemized receipt required', - maxAge: 'Expense too old', - missingAttendees: 'Attendees required', - missingCategory: 'Missing category', - missingComment: 'Description required', - missingTag: 'Missing tag', - modifiedAmount: 'Amount modified', - modifiedDate: 'Date modified', - noRoute: 'No valid route', - nonExpensiworksExpense: 'Non-Expensiworks expense', - overAutoApprovalLimit: 'Over auto-approval limit', - overCategoryLimit: 'Over category limit', - overLimit: 'Over limit', - overTripLimit: 'Over trip limit', - perDayLimit: 'Over daily limit', - prohibitedExpense: 'Prohibited expense', - receiptGeneratedWithAI: 'Possible AI-generated receipt', - receiptNotSmartScanned: 'Receipt added manually', - receiptRequired: 'Receipt required', - rter: 'Awaiting card match', - smartscanFailed: 'Receipt scanning failed', - someTagLevelsRequired: 'Tag required', - taxAmountChanged: 'Tax amount modified', - taxOutOfPolicy: 'Tax rate no longer valid', - taxRateChanged: 'Tax rate modified', - taxRequired: 'Missing tax rate', - }, - reportOrFieldViolations: { - fieldRequired: 'Report field required', - }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `Pole ${fieldName} jest wymagane`, reportContainsExpensesWithViolations: 'Raport zawiera wydatki z naruszeniami.', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index c85cec85b67f..a426f8d8c9db 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -9720,58 +9720,52 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, customUnitRateOutOfDateRange: ({startDate, endDate}: {startDate: string; endDate: string}) => `A tarifa só é válida de ${startDate} até ${endDate}`, customUnitRateOutOfDateRangeStartOnly: ({startDate}: {startDate: string}) => `A tarifa só é válida a partir de ${startDate}`, customUnitRateOutOfDateRangeEndOnly: ({endDate}: {endDate: string}) => `A tarifa só é válida até ${endDate}`, + shortName: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + fieldRequired: 'Report field required', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, }, - /** - * Parameter-free labels for submitted transaction violations shown in Search table columns. - * Prefer these over `violations.*` when violation data/context is unavailable. - */ - transactionViolations: { - allTagLevelsRequired: 'All tags required', - autoReportedRejectedExpense: 'Expense rejected', - billableExpense: 'Billable no longer valid', - cashExpenseWithNoReceipt: 'Receipt required', - categoryOutOfPolicy: 'Category no longer valid', - companyCardRequired: 'Company card required', - conversionSurcharge: 'Conversion surcharge applied', - customUnitOutOfPolicy: 'Rate not valid for workspace', - customUnitRateOutOfDateRange: 'Rate outside valid dates', - duplicatedTransaction: 'Potential duplicate', - futureDate: 'Future date not allowed', - hold: 'Expense on hold', - inactiveVendor: 'Vendor no longer valid', - increasedDistance: 'Distance exceeds route', - invoiceMarkup: 'Invoice marked up', - itemizedReceiptRequired: 'Itemized receipt required', - maxAge: 'Expense too old', - missingAttendees: 'Attendees required', - missingCategory: 'Missing category', - missingComment: 'Description required', - missingTag: 'Missing tag', - modifiedAmount: 'Amount modified', - modifiedDate: 'Date modified', - noRoute: 'No valid route', - nonExpensiworksExpense: 'Non-Expensiworks expense', - overAutoApprovalLimit: 'Over auto-approval limit', - overCategoryLimit: 'Over category limit', - overLimit: 'Over limit', - overTripLimit: 'Over trip limit', - perDayLimit: 'Over daily limit', - prohibitedExpense: 'Prohibited expense', - receiptGeneratedWithAI: 'Possible AI-generated receipt', - receiptNotSmartScanned: 'Receipt added manually', - receiptRequired: 'Receipt required', - rter: 'Awaiting card match', - smartscanFailed: 'Receipt scanning failed', - someTagLevelsRequired: 'Tag required', - taxAmountChanged: 'Tax amount modified', - taxOutOfPolicy: 'Tax rate no longer valid', - taxRateChanged: 'Tax rate modified', - taxRequired: 'Missing tax rate', - }, - reportOrFieldViolations: { - fieldRequired: 'Report field required', - }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} é obrigatório`, reportContainsExpensesWithViolations: 'O relatório contém despesas com violações.', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 1a6e9f612263..9669fb178537 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -9403,58 +9403,52 @@ ${reportName}`, customUnitRateOutOfDateRange: ({startDate, endDate}: {startDate: string; endDate: string}) => `费率仅在 ${startDate} 至 ${endDate} 期间有效`, customUnitRateOutOfDateRangeStartOnly: ({startDate}: {startDate: string}) => `费率仅自 ${startDate} 起有效`, customUnitRateOutOfDateRangeEndOnly: ({endDate}: {endDate: string}) => `该费率仅在 ${endDate} 之前有效`, + shortName: { + allTagLevelsRequired: 'All tags required', + autoReportedRejectedExpense: 'Expense rejected', + billableExpense: 'Billable no longer valid', + cashExpenseWithNoReceipt: 'Receipt required', + categoryOutOfPolicy: 'Category no longer valid', + companyCardRequired: 'Company card required', + conversionSurcharge: 'Conversion surcharge applied', + customUnitOutOfPolicy: 'Rate not valid for workspace', + customUnitRateOutOfDateRange: 'Rate outside valid dates', + duplicatedTransaction: 'Potential duplicate', + fieldRequired: 'Report field required', + futureDate: 'Future date not allowed', + hold: 'Expense on hold', + inactiveVendor: 'Vendor no longer valid', + increasedDistance: 'Distance exceeds route', + invoiceMarkup: 'Invoice marked up', + itemizedReceiptRequired: 'Itemized receipt required', + maxAge: 'Expense too old', + missingAttendees: 'Attendees required', + missingCategory: 'Missing category', + missingComment: 'Description required', + missingTag: 'Missing tag', + modifiedAmount: 'Amount modified', + modifiedDate: 'Date modified', + noRoute: 'No valid route', + nonExpensiworksExpense: 'Non-Expensiworks expense', + overAutoApprovalLimit: 'Over auto-approval limit', + overCategoryLimit: 'Over category limit', + overLimit: 'Over limit', + overTripLimit: 'Over trip limit', + perDayLimit: 'Over daily limit', + prohibitedExpense: 'Prohibited expense', + receiptGeneratedWithAI: 'Possible AI-generated receipt', + receiptNotSmartScanned: 'Receipt added manually', + receiptRequired: 'Receipt required', + rter: 'Awaiting card match', + smartscanFailed: 'Receipt scanning failed', + someTagLevelsRequired: 'Tag required', + taxAmountChanged: 'Tax amount modified', + taxOutOfPolicy: 'Tax rate no longer valid', + taxRateChanged: 'Tax rate modified', + taxRequired: 'Missing tax rate', + }, }, - /** - * Parameter-free labels for submitted transaction violations shown in Search table columns. - * Prefer these over `violations.*` when violation data/context is unavailable. - */ - transactionViolations: { - allTagLevelsRequired: 'All tags required', - autoReportedRejectedExpense: 'Expense rejected', - billableExpense: 'Billable no longer valid', - cashExpenseWithNoReceipt: 'Receipt required', - categoryOutOfPolicy: 'Category no longer valid', - companyCardRequired: 'Company card required', - conversionSurcharge: 'Conversion surcharge applied', - customUnitOutOfPolicy: 'Rate not valid for workspace', - customUnitRateOutOfDateRange: 'Rate outside valid dates', - duplicatedTransaction: 'Potential duplicate', - futureDate: 'Future date not allowed', - hold: 'Expense on hold', - inactiveVendor: 'Vendor no longer valid', - increasedDistance: 'Distance exceeds route', - invoiceMarkup: 'Invoice marked up', - itemizedReceiptRequired: 'Itemized receipt required', - maxAge: 'Expense too old', - missingAttendees: 'Attendees required', - missingCategory: 'Missing category', - missingComment: 'Description required', - missingTag: 'Missing tag', - modifiedAmount: 'Amount modified', - modifiedDate: 'Date modified', - noRoute: 'No valid route', - nonExpensiworksExpense: 'Non-Expensiworks expense', - overAutoApprovalLimit: 'Over auto-approval limit', - overCategoryLimit: 'Over category limit', - overLimit: 'Over limit', - overTripLimit: 'Over trip limit', - perDayLimit: 'Over daily limit', - prohibitedExpense: 'Prohibited expense', - receiptGeneratedWithAI: 'Possible AI-generated receipt', - receiptNotSmartScanned: 'Receipt added manually', - receiptRequired: 'Receipt required', - rter: 'Awaiting card match', - smartscanFailed: 'Receipt scanning failed', - someTagLevelsRequired: 'Tag required', - taxAmountChanged: 'Tax amount modified', - taxOutOfPolicy: 'Tax rate no longer valid', - taxRateChanged: 'Tax rate modified', - taxRequired: 'Missing tax rate', - }, - reportOrFieldViolations: { - fieldRequired: 'Report field required', - }, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} 为必填项`, reportContainsExpensesWithViolations: '报表包含有违规的报销。', diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 8455cf9325c1..260a8abf2621 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -4935,6 +4935,7 @@ const SUBMITTED_TRANSACTION_VIOLATION_SHORT_NAMES = [ CONST.VIOLATIONS.CUSTOM_UNIT_OUT_OF_POLICY, CONST.VIOLATIONS.CUSTOM_UNIT_RATE_OUT_OF_DATE_RANGE, CONST.VIOLATIONS.DUPLICATED_TRANSACTION, + CONST.VIOLATIONS.FIELD_REQUIRED, CONST.VIOLATIONS.FUTURE_DATE, CONST.VIOLATIONS.HOLD, CONST.VIOLATIONS.INACTIVE_VENDOR, @@ -4985,12 +4986,8 @@ function getSubmittedViolationDisplayName(violationName: string, translate: Loca return violationName; } - if (violationName === CONST.VIOLATIONS.FIELD_REQUIRED) { - return translate('reportOrFieldViolations.fieldRequired'); - } - if (isSubmittedTransactionViolationShortName(violationName)) { - return translate(`transactionViolations.${violationName}`); + return translate(`violations.shortName.${violationName}`); } return violationName; diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 452c384a3146..d3b80b3871b5 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -11475,7 +11475,7 @@ describe('SearchUIUtils', () => { }); expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitAction], transactionIDForViolations, translateLocal)).toBe( - `${translateLocal('transactionViolations.missingCategory')}, ${translateLocal('transactionViolations.missingComment')}`, + `${translateLocal('violations.shortName.missingCategory')}, ${translateLocal('violations.shortName.missingComment')}`, ); }); @@ -11487,7 +11487,7 @@ describe('SearchUIUtils', () => { }); expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitAndCloseAction], transactionIDForViolations, translateLocal)).toBe( - translateLocal('transactionViolations.receiptRequired'), + translateLocal('violations.shortName.receiptRequired'), ); }); @@ -11512,20 +11512,18 @@ describe('SearchUIUtils', () => { ); expect(SearchUIUtils.getSubmittedViolationsForTransaction([firstSubmit, secondSubmit], transactionIDForViolations, translateLocal)).toBe( - `${translateLocal('transactionViolations.missingCategory')}, ${translateLocal('transactionViolations.missingTag')}, ${translateLocal('transactionViolations.receiptRequired')}`, + `${translateLocal('violations.shortName.missingCategory')}, ${translateLocal('violations.shortName.missingTag')}, ${translateLocal('violations.shortName.receiptRequired')}`, ); }); - test('translates fieldRequired via reportOrFieldViolations', () => { + test('translates fieldRequired via violations.shortName', () => { const submitAction = createSubmittedAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED, { transactions: { [transactionIDForViolations]: [{name: CONST.VIOLATIONS.FIELD_REQUIRED}], }, }); - expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitAction], transactionIDForViolations, translateLocal)).toBe( - translateLocal('reportOrFieldViolations.fieldRequired'), - ); + expect(SearchUIUtils.getSubmittedViolationsForTransaction([submitAction], transactionIDForViolations, translateLocal)).toBe(translateLocal('violations.shortName.fieldRequired')); }); test('falls back to the raw identifier when no short-name translation exists', () => { From aed2dfa9af1ce5f04e1d639a6f1513b212bb78ed Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 29 Jul 2026 14:02:36 +0700 Subject: [PATCH 09/11] fix test --- tests/unit/TransactionGroupListItemTest.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/TransactionGroupListItemTest.tsx b/tests/unit/TransactionGroupListItemTest.tsx index 018465698a13..e8aa568ffdb3 100644 --- a/tests/unit/TransactionGroupListItemTest.tsx +++ b/tests/unit/TransactionGroupListItemTest.tsx @@ -39,6 +39,7 @@ jest.mock('@libs/SearchUIUtils', () => ({ getTableMinWidth: jest.fn(() => 0), getSuggestedSearches: jest.fn(() => ({})), getSuggestedSearchesVisibility: jest.fn(() => ({topSpendersPolicyIDs: []})), + getSubmittedViolationsForTransaction: jest.fn(() => ''), })); jest.mock('@react-navigation/native', () => ({ From df261bc04158f0b5d307779f0ab3f2c80c918212 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 30 Jul 2026 16:29:07 +0700 Subject: [PATCH 10/11] update non sortable list --- src/libs/SearchUIUtils.ts | 1 + tests/unit/Search/SearchUIUtilsTest.ts | 122 +++++++++++++------------ 2 files changed, 66 insertions(+), 57 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index d9f1edb27041..4a1a40f6d5d9 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -436,6 +436,7 @@ const nonSortableColumns = new Set([ CONST.SEARCH.TABLE_COLUMNS.ACTION, CONST.SEARCH.TABLE_COLUMNS.IN, CONST.SEARCH.TABLE_COLUMNS.AVATAR, + CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS, ]); function isValidExpenseStatus(status: unknown): status is ValueOf { diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 1b170a7b1e88..fd143b047400 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -9241,63 +9241,6 @@ describe('SearchUIUtils', () => { expect(searchQuery).toContain(`view:${CONST.SEARCH.VIEW.PIE}`); }); - test('Should return Violations by submitter search with correct properties', () => { - const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined); - const violationsBySubmitterSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]; - - expect(violationsBySubmitterSearch).toBeDefined(); - expect(violationsBySubmitterSearch.key).toBe(CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER); - expect(violationsBySubmitterSearch.translationPath).toBe('search.tabs.violationsBySubmitter'); - expect(violationsBySubmitterSearch.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); - expect(violationsBySubmitterSearch.icon).toBe('UserEye'); - }); - - test('Should return Violations by submitter search query with correct parameters', () => { - const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined); - const violationsBySubmitterSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]; - const searchQueryJSON = violationsBySubmitterSearch.searchQueryJSON; - - expect(searchQueryJSON).toBeDefined(); - expect(searchQueryJSON?.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); - expect(searchQueryJSON?.groupBy).toBe(CONST.SEARCH.GROUP_BY.FROM); - expect(searchQueryJSON?.view).toBe(CONST.SEARCH.VIEW.TABLE); - expect(searchQueryJSON?.sortBy).toBe(CONST.SEARCH.TABLE_COLUMNS.GROUP_EXPENSES); - expect(searchQueryJSON?.sortOrder).toBe(CONST.SEARCH.SORT_ORDER.DESC); - - const dateFilter = searchQueryJSON?.flatFilters?.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); - expect(dateFilter).toBeDefined(); - expect(dateFilter?.filters?.some((filter) => filter.value === CONST.SEARCH.DATE_PRESETS.LAST_MONTH)).toBe(true); - - const hasFilter = searchQueryJSON?.flatFilters?.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.HAS); - expect(hasFilter).toBeDefined(); - expect(hasFilter?.filters?.some((filter) => filter.value === CONST.SEARCH.HAS_VALUES.SUBMITTED_VIOLATION)).toBe(true); - - expect(searchQueryJSON?.limit).toBe(CONST.SEARCH.TOP_SEARCH_LIMIT); - }); - - test('Should return Violations by submitter search query string with correct format', () => { - const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined); - const violationsBySubmitterSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]; - const searchQuery = violationsBySubmitterSearch.searchQuery; - - expect(searchQuery).toContain(`type:${CONST.SEARCH.DATA_TYPES.EXPENSE}`); - expect(searchQuery).toContain(`groupBy:${CONST.SEARCH.GROUP_BY.FROM}`); - expect(searchQuery).toContain(`date:${CONST.SEARCH.DATE_PRESETS.LAST_MONTH}`); - expect(searchQuery).toContain(`has:${CONST.SEARCH.HAS_VALUES.SUBMITTED_VIOLATION}`); - expect(searchQuery).toContain(`view:${CONST.SEARCH.VIEW.TABLE}`); - expect(searchQuery).toContain(`limit:${CONST.SEARCH.TOP_SEARCH_LIMIT}`); - expect(searchQuery).toContain(`sortBy:${CONST.SEARCH.TABLE_COLUMNS.GROUP_EXPENSES}`); - expect(searchQuery).toContain(`sortOrder:${CONST.SEARCH.SORT_ORDER.DESC}`); - }); - - test('Should return Violations by submitter search with valid hash', () => { - const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined); - const violationsBySubmitterSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]; - - expect(violationsBySubmitterSearch.hash).toBeGreaterThan(0); - expect(violationsBySubmitterSearch.similarSearchHash).toBeGreaterThan(0); - }); - test('Should show Top Spenders for workflow approver (submitsTo) in paid policy', () => { const workflowApproverEmail = 'workflow-approver@policy.com'; const policyKey = `policy_${policyID}`; @@ -9603,6 +9546,65 @@ describe('SearchUIUtils', () => { }); }); + describe('Test getSuggestedSearches', () => { + test('Should return Violations by submitter search with correct properties', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined); + const violationsBySubmitterSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]; + + expect(violationsBySubmitterSearch).toBeDefined(); + expect(violationsBySubmitterSearch.key).toBe(CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER); + expect(violationsBySubmitterSearch.translationPath).toBe('search.tabs.violationsBySubmitter'); + expect(violationsBySubmitterSearch.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); + expect(violationsBySubmitterSearch.icon).toBe('UserEye'); + }); + + test('Should return Violations by submitter search query with correct parameters', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined); + const violationsBySubmitterSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]; + const searchQueryJSON = violationsBySubmitterSearch.searchQueryJSON; + + expect(searchQueryJSON).toBeDefined(); + expect(searchQueryJSON?.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); + expect(searchQueryJSON?.groupBy).toBe(CONST.SEARCH.GROUP_BY.FROM); + expect(searchQueryJSON?.view).toBe(CONST.SEARCH.VIEW.TABLE); + expect(searchQueryJSON?.sortBy).toBe(CONST.SEARCH.TABLE_COLUMNS.GROUP_EXPENSES); + expect(searchQueryJSON?.sortOrder).toBe(CONST.SEARCH.SORT_ORDER.DESC); + + const dateFilter = searchQueryJSON?.flatFilters?.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); + expect(dateFilter).toBeDefined(); + expect(dateFilter?.filters?.some((filter) => filter.value === CONST.SEARCH.DATE_PRESETS.LAST_MONTH)).toBe(true); + + const hasFilter = searchQueryJSON?.flatFilters?.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.HAS); + expect(hasFilter).toBeDefined(); + expect(hasFilter?.filters?.some((filter) => filter.value === CONST.SEARCH.HAS_VALUES.SUBMITTED_VIOLATION)).toBe(true); + + expect(searchQueryJSON?.limit).toBe(CONST.SEARCH.TOP_SEARCH_LIMIT); + }); + + test('Should return Violations by submitter search query string with correct format', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined); + const violationsBySubmitterSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]; + const searchQuery = violationsBySubmitterSearch.searchQuery; + + expect(searchQuery).toContain(`type:${CONST.SEARCH.DATA_TYPES.EXPENSE}`); + expect(searchQuery).toContain(`groupBy:${CONST.SEARCH.GROUP_BY.FROM}`); + expect(searchQuery).toContain(`date:${CONST.SEARCH.DATE_PRESETS.LAST_MONTH}`); + expect(searchQuery).toContain(`has:${CONST.SEARCH.HAS_VALUES.SUBMITTED_VIOLATION}`); + expect(searchQuery).toContain(`view:${CONST.SEARCH.VIEW.TABLE}`); + expect(searchQuery).toContain(`limit:${CONST.SEARCH.TOP_SEARCH_LIMIT}`); + expect(searchQuery).toContain(`sortBy:${CONST.SEARCH.TABLE_COLUMNS.GROUP_EXPENSES}`); + expect(searchQuery).toContain(`sortOrder:${CONST.SEARCH.SORT_ORDER.DESC}`); + }); + + test('Should return Violations by submitter search with valid hash', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined); + const violationsBySubmitterSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.VIOLATIONS_BY_SUBMITTER]; + + expect(violationsBySubmitterSearch.hash).toBeGreaterThan(0); + expect(violationsBySubmitterSearch.similarSearchHash).toBeGreaterThan(0); + }); + }); + describe('Test getColumnsToShow', () => { test('Should show all default columns when no custom columns are saved & viewing expense reports', () => { expect(SearchUIUtils.getColumnsToShow({currentAccountID: 1, data: [], visibleColumns: [], type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT})).toEqual([ @@ -9782,6 +9784,12 @@ describe('SearchUIUtils', () => { expect(tagGLCodeHeader?.sortColumnName).toBe(CONST.SEARCH.SORT_BY_COLUMNS.TAG_GL_CODE); }); + test('Should exclude Violations from sort options', () => { + expect(SearchUIUtils.getSortByOptions([CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS, CONST.SEARCH.TABLE_COLUMNS.CATEGORY_GL_CODE], translateLocal)).toEqual([ + {text: translateLocal('common.categoryGLCode'), value: CONST.SEARCH.SORT_BY_COLUMNS.CATEGORY_GL_CODE}, + ]); + }); + test('Should show MCC whenever that column is selected, even with no displayable MCC', () => { const baseTransaction = searchResults.data[`transactions_${transactionID}`]; const transactionWithoutMCC = { From 2f31cf65dcadfe91207cb1bf7c3799d838b22d40 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 31 Jul 2026 09:53:25 +0700 Subject: [PATCH 11/11] only display violation column when data is available and remove it from search edit column --- src/CONST/index.ts | 2 +- src/libs/SearchUIUtils.ts | 8 ++ tests/unit/Search/SearchUIUtilsTest.ts | 115 ++++++++++++++++++ .../ColumnAvailabilityTest.ts.snap | 1 - 4 files changed, 124 insertions(+), 2 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 28db1496c3bb..8455f3687eae 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6876,7 +6876,7 @@ const CONST = { TAG: {column: this.TABLE_COLUMNS.TAG, search: true, reportView: true}, VIOLATIONS: { column: this.TABLE_COLUMNS.VIOLATIONS, - search: true, + search: false, reportView: false, }, TAG_GL_CODE: { diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 87fcf55dc365..263fd18955a2 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -6465,6 +6465,14 @@ function getColumnsToShow({ } if (customResult) { + if (columns[CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS] && !customResult.includes(CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS)) { + const totalAmountIndex = customResult.indexOf(CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT); + if (totalAmountIndex === -1) { + customResult.push(CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS); + } else { + customResult.splice(totalAmountIndex, 0, CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS); + } + } return customResult; } diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index a793d76db192..d9b876c81ece 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -9804,6 +9804,121 @@ describe('SearchUIUtils', () => { expect(strictColumns).not.toContain(CONST.SEARCH.TABLE_COLUMNS.TO); }); + test('Should not show Violations on expense search when no submitted-violation data is present', () => { + const baseTransaction = searchResults.data[`transactions_${transactionID}`]; + const tx = { + ...baseTransaction, + transactionID: 'no-submitted-violations', + merchant: 'Test Merchant', + modifiedMerchant: '', + reportID, + }; + + // @ts-expect-error minimal dataset for getColumnsToShow + const data: OnyxTypes.SearchResults['data'] = { + [`report_${reportID}`]: searchResults.data[`report_${reportID}`], + [`transactions_${tx.transactionID}`]: tx, + [`reportActions_${reportID}`]: { + '1': { + reportActionID: '1', + actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED, + created: '2025-01-01 00:00:00', + originalMessage: { + amount: 1000, + currency: CONST.CURRENCY.USD, + }, + }, + }, + personalDetailsList: searchResults.data.personalDetailsList, + }; + + const columns = SearchUIUtils.getColumnsToShow({currentAccountID: submitterAccountID, data, visibleColumns: []}); + expect(columns).not.toContain(CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS); + }); + + test('Should show Violations only when submitted-violation data is present', () => { + const baseTransaction = searchResults.data[`transactions_${transactionID}`]; + const tx = { + ...baseTransaction, + transactionID: 'with-submitted-violations', + merchant: 'Test Merchant', + modifiedMerchant: '', + reportID, + }; + + // @ts-expect-error minimal dataset for getColumnsToShow + const data: OnyxTypes.SearchResults['data'] = { + [`report_${reportID}`]: searchResults.data[`report_${reportID}`], + [`transactions_${tx.transactionID}`]: tx, + [`reportActions_${reportID}`]: { + '1': { + reportActionID: '1', + actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED, + created: '2025-01-01 00:00:00', + originalMessage: { + amount: 1000, + currency: CONST.CURRENCY.USD, + violations: { + transactions: { + [tx.transactionID]: [{name: CONST.VIOLATIONS.MISSING_CATEGORY}], + }, + }, + }, + }, + }, + personalDetailsList: searchResults.data.personalDetailsList, + }; + + const columns = SearchUIUtils.getColumnsToShow({currentAccountID: submitterAccountID, data, visibleColumns: []}); + expect(columns).toContain(CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS); + }); + + test('Should inject Violations into custom column layouts when submitted-violation data is present', () => { + const baseTransaction = searchResults.data[`transactions_${transactionID}`]; + const tx = { + ...baseTransaction, + transactionID: 'custom-columns-submitted-violations', + merchant: 'Test Merchant', + modifiedMerchant: '', + category: 'Advertising', + reportID, + }; + const customVisibleColumns = [ + CONST.SEARCH.TABLE_COLUMNS.RECEIPT, + CONST.SEARCH.TABLE_COLUMNS.DATE, + CONST.SEARCH.TABLE_COLUMNS.MERCHANT, + CONST.SEARCH.TABLE_COLUMNS.CATEGORY, + CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT, + ]; + + // @ts-expect-error minimal dataset for getColumnsToShow + const data: OnyxTypes.SearchResults['data'] = { + [`report_${reportID}`]: searchResults.data[`report_${reportID}`], + [`transactions_${tx.transactionID}`]: tx, + [`reportActions_${reportID}`]: { + '1': { + reportActionID: '1', + actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED, + created: '2025-01-01 00:00:00', + originalMessage: { + amount: 1000, + currency: CONST.CURRENCY.USD, + violations: { + transactions: { + [tx.transactionID]: [{name: CONST.VIOLATIONS.MISSING_CATEGORY}], + }, + }, + }, + }, + }, + personalDetailsList: searchResults.data.personalDetailsList, + }; + + const columns = SearchUIUtils.getColumnsToShow({currentAccountID: submitterAccountID, data, visibleColumns: customVisibleColumns}); + expect(columns).toContain(CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS); + expect(columns.indexOf(CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS)).toBeLessThan(columns.indexOf(CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT)); + }); + test('Should only show Category GL Code when that column is selected', () => { const baseTransaction = searchResults.data[`transactions_${transactionID}`]; const transactionWithCategoryGLCode = { diff --git a/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap b/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap index d415876f37bb..23b2ccc516da 100644 --- a/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap +++ b/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap @@ -51,7 +51,6 @@ exports[`Column availability single source of truth the derived picker lists mat "attendees", "totalPerAttendee", "tag", - "violations", "tagGLCode", "exchangeRate", "originalamount",