From d64c058e7ead3b0601550495df2d0a12a3622a8e Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 13 Mar 2026 16:34:49 +0700 Subject: [PATCH 01/21] feat: Display Per-Person Amount and Attendees in Expense Table --- src/CONST/index.ts | 6 ++ .../SearchTableHeader.tsx | 8 +++ src/components/TransactionItemRow/index.tsx | 64 +++++++++++++++++++ src/languages/de.ts | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + src/libs/SearchUIUtils.ts | 8 +++ src/styles/utils/index.ts | 1 + 15 files changed, 97 insertions(+) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index aa699de640f1..7d1829d9c7e5 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7294,6 +7294,8 @@ const CONST = { POLICY_NAME: this.TABLE_COLUMNS.POLICY_NAME, CARD: this.TABLE_COLUMNS.CARD, CATEGORY: this.TABLE_COLUMNS.CATEGORY, + ATTENDEES: this.TABLE_COLUMNS.ATTENDEES, + TOTAL_PER_ATTENDEE: this.TABLE_COLUMNS.TOTAL_PER_ATTENDEE, TAG: this.TABLE_COLUMNS.TAG, EXCHANGE_RATE: this.TABLE_COLUMNS.EXCHANGE_RATE, ORIGINAL_AMOUNT: this.TABLE_COLUMNS.ORIGINAL_AMOUNT, @@ -7418,6 +7420,8 @@ const CONST = { this.TABLE_COLUMNS.FROM, this.TABLE_COLUMNS.TO, this.TABLE_COLUMNS.CATEGORY, + this.TABLE_COLUMNS.ATTENDEES, + this.TABLE_COLUMNS.TOTAL_PER_ATTENDEE, this.TABLE_COLUMNS.TAG, this.TABLE_COLUMNS.TOTAL_AMOUNT, this.TABLE_COLUMNS.ACTION, @@ -7520,6 +7524,8 @@ const CONST = { BILLABLE: 'billable', TAX_RATE: 'taxrate', TOTAL_AMOUNT: 'amount', + ATTENDEES: 'attendees', + TOTAL_PER_ATTENDEE: 'totalPerAttendee', TOTAL: 'total', TYPE: 'type', ACTION: 'action', diff --git a/src/components/SelectionListWithSections/SearchTableHeader.tsx b/src/components/SelectionListWithSections/SearchTableHeader.tsx index 7d05263b54a0..6bfed64da723 100644 --- a/src/components/SelectionListWithSections/SearchTableHeader.tsx +++ b/src/components/SelectionListWithSections/SearchTableHeader.tsx @@ -83,6 +83,14 @@ const getExpenseHeaders = (groupBy?: SearchGroupBy): SearchColumnConfig[] => [ columnName: CONST.SEARCH.TABLE_COLUMNS.CATEGORY, translationKey: 'common.category', }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.ATTENDEES, + translationKey: 'iou.attendees', + }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.TOTAL_PER_ATTENDEE, + translationKey: 'iou.totalPerAttendee', + }, { columnName: CONST.SEARCH.TABLE_COLUMNS.TAG, translationKey: 'common.tag', diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 13a5de8f1deb..7485e280a096 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -6,6 +6,7 @@ import Icon from '@components/Icon'; import type {TransactionWithOptionalHighlight} from '@components/MoneyRequestReportView/MoneyRequestReportTransactionList'; import {PressableWithFeedback} from '@components/Pressable'; import RadioButton from '@components/RadioButton'; +import ReportActionAvatars from '@components/ReportActionAvatars'; import type {SearchColumnType, TableColumnSize} from '@components/Search/types'; import ActionCell from '@components/SelectionListWithSections/Search/ActionCell'; import DateCell from '@components/SelectionListWithSections/Search/DateCell'; @@ -16,6 +17,7 @@ import AmountCell from '@components/SelectionListWithSections/Search/TotalCell'; import UserInfoCell from '@components/SelectionListWithSections/Search/UserInfoCell'; import WorkspaceCell from '@components/SelectionListWithSections/Search/WorkspaceCell'; import Text from '@components/Text'; +import TextWithTooltip from '@components/TextWithTooltip'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -23,12 +25,16 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {isCategoryMissing} from '@libs/CategoryUtils'; +import {convertToDisplayString} from '@libs/CurrencyUtils'; import getBase62ReportID from '@libs/getBase62ReportID'; +import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils'; import {getReportName} from '@libs/ReportNameUtils'; import {isExpenseReport, isIOUReport, isSettled} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import { + getAmount, + getCurrency, getDescription, getExchangeRate, getMerchant, @@ -266,6 +272,30 @@ function TransactionItemRow({ return transactionItem.cardName; }, [transactionItem.cardID, transactionItem.cardName, transactionItem.isCardFeedDeleted, customCardNames, translate]); + const attendeeAccountIDs = useMemo( + () => + transactionItem.comment?.attendees + ?.map((attendee) => { + if (attendee.accountID) { + return attendee.accountID; + } + return getPersonalDetailByEmail(attendee.email)?.accountID; + }) + .filter((accountID): accountID is number => typeof accountID === 'number') ?? [], + [transactionItem.comment?.attendees], + ); + + const totalPerAttendee = useMemo(() => { + const attendeesCount = transactionItem.comment?.attendees?.length ?? 0; + const totalAmount = getAmount(transactionItem); + + if (!attendeesCount || totalAmount === undefined) { + return undefined; + } + + return totalAmount / attendeesCount; + }, [transactionItem]); + const renderColumn = (column: SearchColumnType): React.ReactNode => { switch (column) { case CONST.SEARCH.TABLE_COLUMNS.TYPE: @@ -495,6 +525,24 @@ function TransactionItemRow({ ); + case CONST.SEARCH.TABLE_COLUMNS.ATTENDEES: + return ( + + {!!attendeeAccountIDs.length && ( + + )} + + ); case CONST.SEARCH.TABLE_COLUMNS.COMMENTS: return ( ); + case CONST.SEARCH.TABLE_COLUMNS.TOTAL_PER_ATTENDEE: + return ( + + {!!totalPerAttendee && ( + + )} + + ); case CONST.SEARCH.TABLE_COLUMNS.ORIGINAL_AMOUNT: return ( = { bookingArchived: 'Diese Buchung ist archiviert', bookingArchivedDescription: 'Diese Buchung ist archiviert, weil das Reisedatum verstrichen ist. Füge bei Bedarf eine Ausgabe für den endgültigen Betrag hinzu.', attendees: 'Teilnehmende', + totalPerAttendee: 'Summe pro Teilnehmendem', whoIsYourAccountant: 'Wer ist Ihre Steuerberaterin bzw. Ihr Steuerberater?', paymentComplete: 'Zahlung abgeschlossen', time: 'Zeit', diff --git a/src/languages/en.ts b/src/languages/en.ts index 2f7e97db1d28..03d2b63e71a6 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1570,6 +1570,7 @@ const translations = { bookingArchived: 'This booking is archived', bookingArchivedDescription: 'This booking is archived because the trip date has passed. Add an expense for the final amount if needed.', attendees: 'Attendees', + totalPerAttendee: 'Total per attendee', whoIsYourAccountant: 'Who is your accountant?', paymentComplete: 'Payment complete', time: 'Time', diff --git a/src/languages/es.ts b/src/languages/es.ts index be67749dfcd5..71450ee94feb 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1411,6 +1411,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Esta reserva está archivada', bookingArchivedDescription: 'Esta reserva está archivada porque la fecha del viaje ha pasado. Agregue un gasto por el monto final si es necesario.', attendees: 'Asistentes', + totalPerAttendee: 'Total por asistente', whoIsYourAccountant: '¿Quién es tu contador?', paymentComplete: 'Pago completo', time: 'Tiempo', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 3a34c0c1947c..92bd4c4660ea 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1544,6 +1544,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Cette réservation est archivée', bookingArchivedDescription: 'Cette réservation est archivée car la date du voyage est passée. Ajoutez une dépense pour le montant final si nécessaire.', attendees: 'Participants', + totalPerAttendee: 'Total par participant', whoIsYourAccountant: 'Qui est votre comptable ?', paymentComplete: 'Paiement terminé', time: 'Heure', diff --git a/src/languages/it.ts b/src/languages/it.ts index e65e7db28213..68e747b3574e 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1537,6 +1537,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Questa prenotazione è archiviata', bookingArchivedDescription: 'Questa prenotazione è archiviata perché la data del viaggio è passata. Aggiungi una spesa per l’importo finale, se necessario.', attendees: 'Partecipanti', + totalPerAttendee: 'Totale per partecipante', whoIsYourAccountant: 'Chi è il tuo commercialista?', paymentComplete: 'Pagamento completato', time: 'Ora', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 9ab90d2d8dd3..c81a2bea4673 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1527,6 +1527,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'この予約はアーカイブされています', bookingArchivedDescription: 'この予約は旅行日が過ぎたためアーカイブされています。必要に応じて、最終金額の経費を追加してください。', attendees: '参加者', + totalPerAttendee: '参加者一人あたりの合計', whoIsYourAccountant: 'あなたの会計士は誰ですか?', paymentComplete: '支払いが完了しました', time: '時間', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 1ade218c60df..7018c8b2237e 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1535,6 +1535,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Deze boeking is gearchiveerd', bookingArchivedDescription: 'Deze boeking is gearchiveerd omdat de reisdatum is verstreken. Voeg indien nodig een uitgave toe voor het eindbedrag.', attendees: 'Deelnemers', + totalPerAttendee: 'Totaal per deelnemer', whoIsYourAccountant: 'Wie is jouw accountant?', paymentComplete: 'Betaling voltooid', time: 'Tijd', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index dea6dcd3a6ea..2d57906f02cd 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1534,6 +1534,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Ta rezerwacja jest zarchiwizowana', bookingArchivedDescription: 'Ta rezerwacja została zarchiwizowana, ponieważ data podróży już minęła. W razie potrzeby dodaj wydatek na ostateczną kwotę.', attendees: 'Uczestnicy', + totalPerAttendee: 'Suma na uczestnika', whoIsYourAccountant: 'Kim jest twój księgowy?', paymentComplete: 'Płatność zakończona', time: 'Czas', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index be11c98aa086..037884c4267b 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1532,6 +1532,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Esta reserva está arquivada', bookingArchivedDescription: 'Esta reserva está arquivada porque a data da viagem já passou. Adicione uma despesa com o valor final, se necessário.', attendees: 'Participantes', + totalPerAttendee: 'Total por participante', whoIsYourAccountant: 'Quem é seu contador?', paymentComplete: 'Pagamento concluído', time: 'Hora', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index f87e2bac4758..122a2b3e72dd 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1502,6 +1502,7 @@ const translations: TranslationDeepObject = { bookingArchived: '此预订已归档', bookingArchivedDescription: '此预订已归档,因为行程日期已过。如有需要,请为最终金额添加一笔报销。', attendees: '参与者', + totalPerAttendee: '每位参与者总计', whoIsYourAccountant: '你的会计是谁?', paymentComplete: '付款完成', time: '时间', diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 31907d214b8e..cc99bcf21fd3 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -3522,6 +3522,10 @@ function getSearchColumnTranslationKey(columnId: SearchCustomColumnIds): Transla return 'common.to'; case CONST.SEARCH.TABLE_COLUMNS.CATEGORY: return 'common.category'; + case CONST.SEARCH.TABLE_COLUMNS.ATTENDEES: + return 'iou.attendees'; + case CONST.SEARCH.TABLE_COLUMNS.TOTAL_PER_ATTENDEE: + return 'iou.totalPerAttendee'; case CONST.SEARCH.TABLE_COLUMNS.RECEIPT: return 'common.receipt'; case CONST.SEARCH.TABLE_COLUMNS.TAG: @@ -4308,6 +4312,8 @@ function getColumnsToShow( [CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: false, [CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: false, [CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: false, + [CONST.SEARCH.TABLE_COLUMNS.ATTENDEES]: true, + [CONST.SEARCH.TABLE_COLUMNS.TOTAL_PER_ATTENDEE]: true, [CONST.SEARCH.TABLE_COLUMNS.TAG]: false, [CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE]: shouldShowReimbursableColumn, [CONST.SEARCH.TABLE_COLUMNS.BILLABLE]: shouldShowBillableColumn, @@ -4329,6 +4335,8 @@ function getColumnsToShow( [CONST.SEARCH.TABLE_COLUMNS.POLICY_NAME]: false, [CONST.SEARCH.TABLE_COLUMNS.CARD]: false, [CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: false, + [CONST.SEARCH.TABLE_COLUMNS.ATTENDEES]: true, + [CONST.SEARCH.TABLE_COLUMNS.TOTAL_PER_ATTENDEE]: true, [CONST.SEARCH.TABLE_COLUMNS.TAG]: false, [CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE]: false, [CONST.SEARCH.TABLE_COLUMNS.BILLABLE]: false, diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 885ad9cc579e..66db2f36562f 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1823,6 +1823,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ case CONST.SEARCH.TABLE_COLUMNS.ORIGINAL_AMOUNT: case CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT: case CONST.SEARCH.TABLE_COLUMNS.GROUP_TOTAL: + case CONST.SEARCH.TABLE_COLUMNS.TOTAL_PER_ATTENDEE: case CONST.SEARCH.TABLE_COLUMNS.TOTAL: columnWidth = {...getWidthStyle(isAmountColumnWide ? variables.w130 : variables.w96), ...(!shouldRemoveTotalColumnFlex && styles.flex1), ...styles.alignItemsEnd}; break; From c7c84575f8fc4a401ab4bd778d0283d6266fc717 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 16 Mar 2026 14:43:00 +0700 Subject: [PATCH 02/21] implement hook to get the attendees --- src/components/TransactionItemRow/index.tsx | 18 +++---------- src/hooks/useAccountIDsByEmails.ts | 29 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 14 deletions(-) create mode 100644 src/hooks/useAccountIDsByEmails.ts diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 7485e280a096..7adcd6c54c76 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -27,7 +27,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {isCategoryMissing} from '@libs/CategoryUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import getBase62ReportID from '@libs/getBase62ReportID'; -import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils'; import {getReportName} from '@libs/ReportNameUtils'; import {isExpenseReport, isIOUReport, isSettled} from '@libs/ReportUtils'; @@ -51,6 +50,7 @@ import { isUnreportedAndHasInvalidDistanceRateTransaction, } from '@libs/TransactionUtils'; import CONST from '@src/CONST'; +import useAccountIDsByEmails from '@src/hooks/useAccountIDsByEmails'; import type {TranslationPaths} from '@src/languages/types'; import type {PersonalDetails, Policy, Report, ReportAction, TransactionViolation} from '@src/types/onyx'; import type {SearchTransactionAction} from '@src/types/onyx/SearchResults'; @@ -272,18 +272,8 @@ function TransactionItemRow({ return transactionItem.cardName; }, [transactionItem.cardID, transactionItem.cardName, transactionItem.isCardFeedDeleted, customCardNames, translate]); - const attendeeAccountIDs = useMemo( - () => - transactionItem.comment?.attendees - ?.map((attendee) => { - if (attendee.accountID) { - return attendee.accountID; - } - return getPersonalDetailByEmail(attendee.email)?.accountID; - }) - .filter((accountID): accountID is number => typeof accountID === 'number') ?? [], - [transactionItem.comment?.attendees], - ); + const attendeeEmails = useMemo(() => transactionItem.comment?.attendees?.map((attendee) => attendee.email) ?? [], [transactionItem.comment?.attendees]); + const attendeeAccountIDs = useAccountIDsByEmails(attendeeEmails); const totalPerAttendee = useMemo(() => { const attendeesCount = transactionItem.comment?.attendees?.length ?? 0; @@ -531,7 +521,7 @@ function TransactionItemRow({ key={column} style={[StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.ATTENDEES)]} > - {!!attendeeAccountIDs.length && ( + {!!attendeeAccountIDs?.length && ( ) => { + return Object.values(personalDetailsList ?? {}) + .filter((personalDetail) => !!personalDetail?.login && emails.includes(personalDetail.login)) + .map((personalDetail) => personalDetail?.accountID ?? CONST.DEFAULT_NUMBER_ID); + }, + [emails], + ); + + const [accountIDs] = useOnyx( + ONYXKEYS.PERSONAL_DETAILS_LIST, + { + selector: personalDetailsSelector, + }, + [personalDetailsSelector], + ); + + return accountIDs; +} + +export default useAccountIDsByEmails; From c2ef12aba541e7b8d5a124de8c2a2a1982db8e81 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 16 Mar 2026 14:46:39 +0700 Subject: [PATCH 03/21] use getAttendees util --- src/components/TransactionItemRow/index.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 7adcd6c54c76..fd2fe3b0ffa3 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -33,6 +33,7 @@ import {isExpenseReport, isIOUReport, isSettled} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import { getAmount, + getAttendees, getCurrency, getDescription, getExchangeRate, @@ -272,11 +273,13 @@ function TransactionItemRow({ return transactionItem.cardName; }, [transactionItem.cardID, transactionItem.cardName, transactionItem.isCardFeedDeleted, customCardNames, translate]); - const attendeeEmails = useMemo(() => transactionItem.comment?.attendees?.map((attendee) => attendee.email) ?? [], [transactionItem.comment?.attendees]); + const transactionAttendees = useMemo(() => getAttendees(transactionItem, undefined), [transactionItem]); + + const attendeeEmails = useMemo(() => transactionAttendees?.map((attendee) => attendee.email) ?? [], [transactionAttendees]); const attendeeAccountIDs = useAccountIDsByEmails(attendeeEmails); const totalPerAttendee = useMemo(() => { - const attendeesCount = transactionItem.comment?.attendees?.length ?? 0; + const attendeesCount = transactionAttendees.length ?? 0; const totalAmount = getAmount(transactionItem); if (!attendeesCount || totalAmount === undefined) { @@ -284,7 +287,7 @@ function TransactionItemRow({ } return totalAmount / attendeesCount; - }, [transactionItem]); + }, [transactionAttendees.length, transactionItem]); const renderColumn = (column: SearchColumnType): React.ReactNode => { switch (column) { From c7ac28c7ca1570dea5c95f5b2a38c53d0f4e070e Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 16 Mar 2026 22:36:32 +0700 Subject: [PATCH 04/21] use avatar from attendee data to display the avatar --- src/components/ReportActionAvatars/index.tsx | 17 ++++++++++- src/components/TransactionItemRow/index.tsx | 30 ++++++++++++++------ src/hooks/useAccountIDsByEmails.ts | 29 ------------------- 3 files changed, 38 insertions(+), 38 deletions(-) delete mode 100644 src/hooks/useAccountIDsByEmails.ts diff --git a/src/components/ReportActionAvatars/index.tsx b/src/components/ReportActionAvatars/index.tsx index 5f49c30b4c40..51375346949f 100644 --- a/src/components/ReportActionAvatars/index.tsx +++ b/src/components/ReportActionAvatars/index.tsx @@ -7,6 +7,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {InvitedEmailsToAccountIDs, Policy, Report, ReportAction} from '@src/types/onyx'; import type {CardFeed} from '@src/types/onyx/CardFeeds'; +import type {Icon as IconType} from '@src/types/onyx/OnyxCommon'; import type {HorizontalStacking} from './ReportActionAvatar'; import ReportActionAvatar from './ReportActionAvatar'; import useReportActionAvatars from './useReportActionAvatars'; @@ -73,6 +74,12 @@ type ReportActionAvatarsProps = { /** chatReportID needed for the avatars logic. When provided, this will be used as a fallback if the snapshot is undefined */ chatReportID?: string; + + /** Custom avatars to display instead of the avatars from the hook */ + customAvatars?: IconType[]; + + /** Custom avatar type to display instead of the avatar type from the hook */ + customAvatarType?: ValueOf; }; /** @@ -106,6 +113,8 @@ function ReportActionAvatars({ invitedEmailsToAccountIDs, shouldUseCustomFallbackAvatar = false, chatReportID, + customAvatars, + customAvatarType, }: ReportActionAvatarsProps) { const accountIDs = passedAccountIDs.filter((accountID) => accountID !== CONST.DEFAULT_NUMBER_ID); @@ -126,7 +135,7 @@ function ReportActionAvatars({ const { avatarType: notPreciseAvatarType, - avatars: icons, + avatars, details: {delegateAccountID}, source, } = useReportActionAvatars({ @@ -143,6 +152,8 @@ function ReportActionAvatars({ chatReportID, }); + const icons = customAvatars ?? avatars; + let avatarType: ValueOf = notPreciseAvatarType; if (avatarType === CONST.REPORT_ACTION_AVATARS.TYPE.MULTIPLE && !icons.length) { @@ -153,6 +164,10 @@ function ReportActionAvatars({ avatarType = shouldStackHorizontally ? CONST.REPORT_ACTION_AVATARS.TYPE.MULTIPLE_HORIZONTAL : CONST.REPORT_ACTION_AVATARS.TYPE.MULTIPLE_DIAGONAL; } + if (customAvatarType) { + avatarType = customAvatarType; + } + const [primaryAvatar, secondaryAvatar] = icons; if (avatarType === CONST.REPORT_ACTION_AVATARS.TYPE.SUBSCRIPT && (!!secondaryAvatar?.name || !!subscriptCardFeed)) { diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index fd2fe3b0ffa3..8c3eb0db515a 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -18,6 +18,8 @@ import UserInfoCell from '@components/SelectionListWithSections/Search/UserInfoC import WorkspaceCell from '@components/SelectionListWithSections/Search/WorkspaceCell'; import Text from '@components/Text'; import TextWithTooltip from '@components/TextWithTooltip'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDefaultAvatars from '@hooks/useDefaultAvatars'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -50,10 +52,11 @@ import { isTimeRequest, isUnreportedAndHasInvalidDistanceRateTransaction, } from '@libs/TransactionUtils'; +import {getDefaultAvatar} from '@libs/UserAvatarUtils'; import CONST from '@src/CONST'; -import useAccountIDsByEmails from '@src/hooks/useAccountIDsByEmails'; import type {TranslationPaths} from '@src/languages/types'; import type {PersonalDetails, Policy, Report, ReportAction, TransactionViolation} from '@src/types/onyx'; +import type {Icon as IconType} from '@src/types/onyx/OnyxCommon'; import type {SearchTransactionAction} from '@src/types/onyx/SearchResults'; import CategoryCell from './DataCells/CategoryCell'; import ChatBubbleCell from './DataCells/ChatBubbleCell'; @@ -208,6 +211,8 @@ function TransactionItemRow({ const hasCategoryOrTag = !isCategoryMissing(transactionItem?.category) || !!transactionItem.tag; const createdAt = getTransactionCreated(transactionItem); const expensicons = useMemoizedLazyExpensifyIcons(['ArrowRight']); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const defaultAvatars = useDefaultAvatars(); const transactionThreadReportID = reportActions ? getIOUActionForTransactionID(reportActions, transactionItem.transactionID)?.childReportID : undefined; const isDateColumnWide = dateColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE; @@ -273,10 +278,18 @@ function TransactionItemRow({ return transactionItem.cardName; }, [transactionItem.cardID, transactionItem.cardName, transactionItem.isCardFeedDeleted, customCardNames, translate]); - const transactionAttendees = useMemo(() => getAttendees(transactionItem, undefined), [transactionItem]); - - const attendeeEmails = useMemo(() => transactionAttendees?.map((attendee) => attendee.email) ?? [], [transactionAttendees]); - const attendeeAccountIDs = useAccountIDsByEmails(attendeeEmails); + const transactionAttendees = useMemo(() => getAttendees(transactionItem, currentUserPersonalDetails), [transactionItem, currentUserPersonalDetails]); + + const attendeeIcons: IconType[] = useMemo( + () => + transactionAttendees.map((attendee) => ({ + id: attendee.accountID ?? CONST.DEFAULT_NUMBER_ID, + name: attendee.displayName ?? attendee.email, + source: (attendee.avatarUrl || getDefaultAvatar({accountID: attendee.accountID, defaultAvatars})) ?? '', + type: CONST.ICON_TYPE_AVATAR, + })), + [defaultAvatars, transactionAttendees], + ); const totalPerAttendee = useMemo(() => { const attendeesCount = transactionAttendees.length ?? 0; @@ -524,11 +537,12 @@ function TransactionItemRow({ key={column} style={[StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.ATTENDEES)]} > - {!!attendeeAccountIDs?.length && ( + {!!attendeeIcons.length && ( ) => { - return Object.values(personalDetailsList ?? {}) - .filter((personalDetail) => !!personalDetail?.login && emails.includes(personalDetail.login)) - .map((personalDetail) => personalDetail?.accountID ?? CONST.DEFAULT_NUMBER_ID); - }, - [emails], - ); - - const [accountIDs] = useOnyx( - ONYXKEYS.PERSONAL_DETAILS_LIST, - { - selector: personalDetailsSelector, - }, - [personalDetailsSelector], - ); - - return accountIDs; -} - -export default useAccountIDsByEmails; From 21c7142ee41a11f03c7ec924cba9697dae807e05 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 16 Mar 2026 22:41:35 +0700 Subject: [PATCH 05/21] change condition to display total per attendee --- src/components/TransactionItemRow/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 8c3eb0db515a..4c0f21ccf2f9 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -590,7 +590,7 @@ function TransactionItemRow({ key={column} style={[StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.TOTAL_PER_ATTENDEE, undefined, isAmountColumnWide)]} > - {!!totalPerAttendee && ( + {!!attendeeIcons.length && ( Date: Tue, 17 Mar 2026 16:21:03 +0700 Subject: [PATCH 06/21] update border color and text style --- .../ReportActionAvatars/ReportActionAvatar.tsx | 14 ++++++++++++-- src/components/TransactionItemRow/index.tsx | 5 +++++ src/styles/utils/index.ts | 5 +++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionAvatars/ReportActionAvatar.tsx b/src/components/ReportActionAvatars/ReportActionAvatar.tsx index aad0061dfbc0..2f5c7db25b9d 100644 --- a/src/components/ReportActionAvatars/ReportActionAvatar.tsx +++ b/src/components/ReportActionAvatars/ReportActionAvatar.tsx @@ -1,6 +1,6 @@ import lodashSortBy from 'lodash/sortBy'; import React, {useMemo} from 'react'; -import type {ColorValue, ImageStyle, StyleProp, ViewStyle} from 'react-native'; +import type {ColorValue, ImageStyle, StyleProp, TextStyle, ViewStyle} from 'react-native'; import {View} from 'react-native'; import type {ValueOf} from 'type-fest'; import type {UpperCaseCharacters} from 'type-fest/source/internal'; @@ -55,6 +55,12 @@ type HorizontalStacking = Partial<{ /** Prop to sort the avatars */ sort: SortingOptions | SortingOptions[]; + + /** Border color for the active avatar */ + pressedBorderColor?: string; + + /** Inner text style */ + textStyle?: TextStyle; }>; type AvatarStyles = { @@ -308,6 +314,8 @@ function ReportActionAvatarMultipleHorizontal({ useProfileNavigationWrapper, fallbackDisplayName, reportID, + pressedBorderColor, + textStyle, }: HorizontalStacking & { size: ValueOf; shouldShowTooltip: boolean; @@ -393,6 +401,7 @@ function ReportActionAvatarMultipleHorizontal({ isInReportAction, shouldUseCardBackground, isActive, + customPressedBorderColor: pressedBorderColor, }), StyleUtils.getAvatarBorderWidth(size), ]} @@ -425,6 +434,7 @@ function ReportActionAvatarMultipleHorizontal({ isPressed, isInReportAction, shouldUseCardBackground, + customPressedBorderColor: pressedBorderColor, }), // Set overlay background color with RGBA value so that the text will not inherit opacity @@ -435,7 +445,7 @@ function ReportActionAvatarMultipleHorizontal({ > {`+${avatars.length - maxAvatarsInRow}`} diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 4c0f21ccf2f9..7e94e1cbba94 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -544,8 +544,13 @@ function TransactionItemRow({ horizontalStacking={{ sort: CONST.REPORT_ACTION_AVATARS.SORT_BY.NAME, useCardBG: true, + isHovered: isHover, + isPressed: isSelected, + pressedBorderColor: theme.activeComponentBG, + textStyle: styles.textMicroBold, }} size={CONST.AVATAR_SIZE.SUBSCRIPT} + isInReportAction /> )} diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 31d3b15c0831..0e63bb3de4a9 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -834,6 +834,7 @@ type AvatarBorderStyleParams = { isInReportAction: boolean; shouldUseCardBackground: boolean; isActive?: boolean; + customPressedBorderColor?: string; }; function getHorizontalStackedAvatarBorderStyle({ @@ -843,6 +844,7 @@ function getHorizontalStackedAvatarBorderStyle({ isInReportAction = false, shouldUseCardBackground = false, isActive = false, + customPressedBorderColor, }: AvatarBorderStyleParams): ViewStyle { let borderColor = shouldUseCardBackground ? theme.cardBG : theme.appBG; @@ -855,6 +857,9 @@ function getHorizontalStackedAvatarBorderStyle({ if (isPressed) { borderColor = isInReportAction ? theme.hoverComponentBG : theme.buttonPressedBG; + if (customPressedBorderColor) { + borderColor = customPressedBorderColor; + } } return {borderColor}; From 2e655918dd48bc85c5d0ae1d5378161aa6ed610b Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 18 Mar 2026 22:05:45 +0700 Subject: [PATCH 07/21] update overlay style and translation --- src/components/ReportActionAvatars/ReportActionAvatar.tsx | 7 ++++++- src/components/TransactionItemRow/index.tsx | 5 ++++- 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 +- 12 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/components/ReportActionAvatars/ReportActionAvatar.tsx b/src/components/ReportActionAvatars/ReportActionAvatar.tsx index 2f5c7db25b9d..02143753c6f2 100644 --- a/src/components/ReportActionAvatars/ReportActionAvatar.tsx +++ b/src/components/ReportActionAvatars/ReportActionAvatar.tsx @@ -60,7 +60,10 @@ type HorizontalStacking = Partial<{ pressedBorderColor?: string; /** Inner text style */ - textStyle?: TextStyle; + textStyle?: StyleProp; + + /** Style for the overlay */ + overlayStyle?: StyleProp; }>; type AvatarStyles = { @@ -316,6 +319,7 @@ function ReportActionAvatarMultipleHorizontal({ reportID, pressedBorderColor, textStyle, + overlayStyle, }: HorizontalStacking & { size: ValueOf; shouldShowTooltip: boolean; @@ -441,6 +445,7 @@ function ReportActionAvatarMultipleHorizontal({ StyleUtils.getBackgroundColorWithOpacityStyle(theme.overlay, variables.overlayOpacity), StyleUtils.getHorizontalStackedOverlayAvatarStyle(oneAvatarSize, oneAvatarBorderWidth), icons.at(3)?.type === CONST.ICON_TYPE_WORKSPACE && StyleUtils.getAvatarBorderRadius(size, icons.at(3)?.type), + overlayStyle, ]} > diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index ec7296eec170..34fc92eaeb66 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -53,6 +53,8 @@ import { isUnreportedAndHasInvalidDistanceRateTransaction, } from '@libs/TransactionUtils'; import {getDefaultAvatar} from '@libs/UserAvatarUtils'; +import colors from '@styles/theme/colors'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type {PersonalDetails, Policy, Report, ReportAction, TransactionViolation} from '@src/types/onyx'; @@ -546,7 +548,8 @@ function TransactionItemRow({ isHovered: isHover, isPressed: isSelected, pressedBorderColor: theme.activeComponentBG, - textStyle: styles.textMicroBold, + textStyle: [styles.textMicroBold, styles.buttonSuccessText], + overlayStyle: StyleUtils.getBackgroundColorWithOpacityStyle(colors.productDark400, variables.overlayOpacity), }} size={CONST.AVATAR_SIZE.SUBSCRIPT} isInReportAction diff --git a/src/languages/de.ts b/src/languages/de.ts index 263ae860e836..6cc07ea33a8a 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1501,7 +1501,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Diese Buchung ist archiviert', bookingArchivedDescription: 'Diese Buchung ist archiviert, weil das Reisedatum verstrichen ist. Füge bei Bedarf eine Ausgabe für den endgültigen Betrag hinzu.', attendees: 'Teilnehmende', - totalPerAttendee: 'Summe pro Teilnehmendem', + totalPerAttendee: 'Betrag pro Teilnehmendem', whoIsYourAccountant: 'Wer ist Ihre Steuerberaterin bzw. Ihr Steuerberater?', paymentComplete: 'Zahlung abgeschlossen', time: 'Zeit', diff --git a/src/languages/en.ts b/src/languages/en.ts index 7733eea0d65c..92520463f6ef 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1536,7 +1536,7 @@ const translations = { bookingArchived: 'This booking is archived', bookingArchivedDescription: 'This booking is archived because the trip date has passed. Add an expense for the final amount if needed.', attendees: 'Attendees', - totalPerAttendee: 'Total per attendee', + totalPerAttendee: 'Amount per attendee', whoIsYourAccountant: 'Who is your accountant?', paymentComplete: 'Payment complete', time: 'Time', diff --git a/src/languages/es.ts b/src/languages/es.ts index 05eb847520be..51a798185da1 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1411,7 +1411,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Esta reserva está archivada', bookingArchivedDescription: 'Esta reserva está archivada porque la fecha del viaje ha pasado. Agregue un gasto por el monto final si es necesario.', attendees: 'Asistentes', - totalPerAttendee: 'Total por asistente', + totalPerAttendee: 'Monto por asistente', whoIsYourAccountant: '¿Quién es tu contador?', paymentComplete: 'Pago completo', time: 'Tiempo', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 1471fadb44f2..c7517168e4bd 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1505,7 +1505,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Cette réservation est archivée', bookingArchivedDescription: 'Cette réservation est archivée car la date du voyage est passée. Ajoutez une dépense pour le montant final si nécessaire.', attendees: 'Participants', - totalPerAttendee: 'Total par participant', + totalPerAttendee: 'Montant par participant', whoIsYourAccountant: 'Qui est votre comptable ?', paymentComplete: 'Paiement terminé', time: 'Heure', diff --git a/src/languages/it.ts b/src/languages/it.ts index 0e06b2da5761..ea0a8bf5dd41 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1500,7 +1500,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Questa prenotazione è archiviata', bookingArchivedDescription: 'Questa prenotazione è archiviata perché la data del viaggio è passata. Aggiungi una spesa per l’importo finale, se necessario.', attendees: 'Partecipanti', - totalPerAttendee: 'Totale per partecipante', + totalPerAttendee: 'Importo per partecipante', whoIsYourAccountant: 'Chi è il tuo commercialista?', paymentComplete: 'Pagamento completato', time: 'Ora', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index a4bc67ad26b2..036e98371a35 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1490,7 +1490,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'この予約はアーカイブされています', bookingArchivedDescription: 'この予約は旅行日が過ぎたためアーカイブされています。必要に応じて、最終金額の経費を追加してください。', attendees: '参加者', - totalPerAttendee: '参加者一人あたりの合計', + totalPerAttendee: '参加者一人あたりの金額', whoIsYourAccountant: 'あなたの会計士は誰ですか?', paymentComplete: '支払いが完了しました', time: '時間', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index ff2cebe6e2d5..92c13197822d 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1497,7 +1497,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Deze boeking is gearchiveerd', bookingArchivedDescription: 'Deze boeking is gearchiveerd omdat de reisdatum is verstreken. Voeg indien nodig een uitgave toe voor het eindbedrag.', attendees: 'Deelnemers', - totalPerAttendee: 'Totaal per deelnemer', + totalPerAttendee: 'Bedrag per deelnemer', whoIsYourAccountant: 'Wie is jouw accountant?', paymentComplete: 'Betaling voltooid', time: 'Tijd', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index d967c4eb31db..53658531b146 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1496,7 +1496,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Ta rezerwacja jest zarchiwizowana', bookingArchivedDescription: 'Ta rezerwacja została zarchiwizowana, ponieważ data podróży już minęła. W razie potrzeby dodaj wydatek na ostateczną kwotę.', attendees: 'Uczestnicy', - totalPerAttendee: 'Suma na uczestnika', + totalPerAttendee: 'Kwota na uczestnika', whoIsYourAccountant: 'Kim jest twój księgowy?', paymentComplete: 'Płatność zakończona', time: 'Czas', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 317331994577..33ed998429c4 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1494,7 +1494,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Esta reserva está arquivada', bookingArchivedDescription: 'Esta reserva está arquivada porque a data da viagem já passou. Adicione uma despesa com o valor final, se necessário.', attendees: 'Participantes', - totalPerAttendee: 'Total por participante', + totalPerAttendee: 'Valor por participante', whoIsYourAccountant: 'Quem é seu contador?', paymentComplete: 'Pagamento concluído', time: 'Hora', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 2a2ac5c6c256..4e8d1d6df310 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1465,7 +1465,7 @@ const translations: TranslationDeepObject = { bookingArchived: '此预订已归档', bookingArchivedDescription: '此预订已归档,因为行程日期已过。如有需要,请为最终金额添加一笔报销。', attendees: '参与者', - totalPerAttendee: '每位参与者总计', + totalPerAttendee: '每位参与者金额', whoIsYourAccountant: '你的会计是谁?', paymentComplete: '付款完成', time: '时间', From 6b8f45c9cb8769574a00546700b35944040492f4 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 18 Mar 2026 23:02:12 +0700 Subject: [PATCH 08/21] update fixed width for attendee column --- src/styles/utils/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 5cea3024d5f2..eb8623022482 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1878,6 +1878,9 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ case CONST.SEARCH.TABLE_COLUMNS.EXPORTED_TO: columnWidth = {...getWidthStyle(variables.w72), ...styles.alignItemsCenter}; break; + case CONST.SEARCH.TABLE_COLUMNS.ATTENDEES: + columnWidth = {...getWidthStyle(variables.w72)}; + break; case CONST.SEARCH.TABLE_COLUMNS.GROUP_FEED: case CONST.SEARCH.TABLE_COLUMNS.GROUP_BANK_ACCOUNT: case CONST.SEARCH.TABLE_COLUMNS.GROUP_WITHDRAWAL_ID: From ff0857d177e2dda6891e8a3313bff88e2ce5edc8 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 20 Mar 2026 14:22:32 +0700 Subject: [PATCH 09/21] hide attendee column when the feature is not enalbed --- .../MoneyRequestReportTransactionList.tsx | 19 +-- src/components/Search/index.tsx | 13 +- .../Search/TransactionGroupListExpanded.tsx | 8 +- src/libs/SearchUIUtils.ts | 46 +++++-- tests/unit/Search/SearchUIUtilsTest.ts | 129 ++++++++++++++++-- 5 files changed, 178 insertions(+), 37 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index 1239c32f21e8..698fa4e8cfa4 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -276,17 +276,18 @@ function MoneyRequestReportTransactionList({ const isExpenseReportViewFromIOUReport = isIOUReport(report); const shouldShowBillableColumn = isBillableEnabledOnPolicy(policy); const columnsToShow = useMemo(() => { - return getColumnsToShow( - currentUserDetails?.accountID, - transactions, - (reportDetailsColumns ?? []) as SearchCustomColumnIds[], - true, - undefined, - undefined, + return getColumnsToShow({ + currentAccountID: currentUserDetails?.accountID, + data: transactions, + visibleColumns: (reportDetailsColumns ?? []) as SearchCustomColumnIds[], + isExpenseReportView: true, + type: undefined, + groupBy: undefined, isExpenseReportViewFromIOUReport, shouldShowBillableColumn, - hasNonReimbursableTransactions(transactions), - ); + shouldShowReimbursableColumn: hasNonReimbursableTransactions(transactions), + policy, + }); }, [transactions, currentUserDetails?.accountID, isExpenseReportViewFromIOUReport, shouldShowBillableColumn, reportDetailsColumns]); const {windowWidth, windowHeight} = useWindowDimensions(); diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 0a16220696a3..103f8620499a 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -1135,7 +1135,18 @@ function Search({ if (!searchResults?.data) { return []; } - return getColumnsToShow(accountID, searchResults?.data, visibleColumns, false, searchDataType, validGroupBy, false, false, false, shouldUseStrictDefaultExpenseColumns); + return getColumnsToShow({ + currentAccountID: accountID, + data: searchResults?.data, + visibleColumns, + isExpenseReportView: false, + type: searchDataType, + groupBy: validGroupBy, + isExpenseReportViewFromIOUReport: false, + shouldShowBillableColumn: false, + shouldShowReimbursableColumn: false, + shouldUseStrictDefaultExpenseColumns, + }); }, [accountID, searchResults?.data, searchDataType, visibleColumns, validGroupBy, shouldUseStrictDefaultExpenseColumns]); const opacity = useSharedValue(1); diff --git a/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx b/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx index 7cb5be7d361d..55792bc54f7c 100644 --- a/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx @@ -77,7 +77,13 @@ function TransactionGroupListExpanded({ if (!transactionsSnapshot?.data) { currentColumns = []; } else { - currentColumns = getColumnsToShow(accountID, transactionsSnapshot?.data, visibleColumns, false, transactionsSnapshot?.search.type); + currentColumns = getColumnsToShow({ + currentAccountID: accountID, + data: transactionsSnapshot?.data, + visibleColumns, + isExpenseReportView: false, + type: transactionsSnapshot?.search.type, + }); } } diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index b142da5b21b0..092653cbd74e 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -157,6 +157,7 @@ import { isPending, isScanning, isViolationDismissed, + shouldShowAttendees, } from './TransactionUtils'; import {isInvalidMerchantValue} from './ValidationUtils'; import ViolationsUtils from './Violations/ViolationsUtils'; @@ -4236,18 +4237,31 @@ function getActionOptions(translate: LocaleContextProps['translate']) { * @param isExpenseReportView: true when we are inside an expense report view, false if we're in the Reports page. * @returns An ordered array of visible column IDs */ -function getColumnsToShow( - currentAccountID: number | undefined, - data: OnyxTypes.SearchResults['data'] | OnyxTypes.Transaction[], - visibleColumns: SearchCustomColumnIds[] = [], +function getColumnsToShow({ + currentAccountID, + data, + visibleColumns = [], isExpenseReportView = false, - type?: SearchDataTypes, - groupBy?: SearchGroupBy, + type, + groupBy, isExpenseReportViewFromIOUReport = false, shouldShowBillableColumn = false, shouldShowReimbursableColumn = false, shouldUseStrictDefaultExpenseColumns = false, -): SearchColumnType[] { + policy, +}: { + currentAccountID: number | undefined; + data: OnyxTypes.SearchResults['data'] | OnyxTypes.Transaction[]; + visibleColumns?: SearchCustomColumnIds[]; + isExpenseReportView?: boolean; + type?: SearchDataTypes; + groupBy?: SearchGroupBy; + isExpenseReportViewFromIOUReport?: boolean; + shouldShowBillableColumn?: boolean; + shouldShowReimbursableColumn?: boolean; + shouldUseStrictDefaultExpenseColumns?: boolean; + policy?: OnyxTypes.Policy; +}): SearchColumnType[] { if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT) { const defaultReportColumns: SearchColumnType[] = [ CONST.SEARCH.TABLE_COLUMNS.AVATAR, @@ -4355,8 +4369,8 @@ function getColumnsToShow( [CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: false, [CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: false, [CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: false, - [CONST.SEARCH.TABLE_COLUMNS.ATTENDEES]: true, - [CONST.SEARCH.TABLE_COLUMNS.TOTAL_PER_ATTENDEE]: true, + [CONST.SEARCH.TABLE_COLUMNS.ATTENDEES]: false, + [CONST.SEARCH.TABLE_COLUMNS.TOTAL_PER_ATTENDEE]: false, [CONST.SEARCH.TABLE_COLUMNS.TAG]: false, [CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE]: shouldShowReimbursableColumn, [CONST.SEARCH.TABLE_COLUMNS.BILLABLE]: shouldShowBillableColumn, @@ -4379,8 +4393,8 @@ function getColumnsToShow( [CONST.SEARCH.TABLE_COLUMNS.POLICY_NAME]: false, [CONST.SEARCH.TABLE_COLUMNS.CARD]: false, [CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: false, - [CONST.SEARCH.TABLE_COLUMNS.ATTENDEES]: true, - [CONST.SEARCH.TABLE_COLUMNS.TOTAL_PER_ATTENDEE]: true, + [CONST.SEARCH.TABLE_COLUMNS.ATTENDEES]: false, + [CONST.SEARCH.TABLE_COLUMNS.TOTAL_PER_ATTENDEE]: false, [CONST.SEARCH.TABLE_COLUMNS.TAG]: false, [CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE]: false, [CONST.SEARCH.TABLE_COLUMNS.BILLABLE]: false, @@ -4489,8 +4503,18 @@ function getColumnsToShow( for (const item of data) { updateColumns(item); } + if (shouldShowAttendees(CONST.IOU.TYPE.SUBMIT, policy)) { + columns[CONST.SEARCH.TABLE_COLUMNS.ATTENDEES] = true; + columns[CONST.SEARCH.TABLE_COLUMNS.TOTAL_PER_ATTENDEE] = true; + } } else { for (const key of Object.keys(data)) { + if (isPolicyEntry(key)) { + if (shouldShowAttendees(CONST.IOU.TYPE.SUBMIT, data[key])) { + columns[CONST.SEARCH.TABLE_COLUMNS.ATTENDEES] = true; + columns[CONST.SEARCH.TABLE_COLUMNS.TOTAL_PER_ATTENDEE] = true; + } + } if (!isTransactionEntry(key)) { continue; } diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 944d1c60a9fe..22f3d30c23e8 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -6808,7 +6808,15 @@ describe('SearchUIUtils', () => { describe('Test getColumnsToShow', () => { test('Should show all default columns when no custom columns are saved & viewing expense reports', () => { - expect(SearchUIUtils.getColumnsToShow(1, [], [], false, CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT)).toEqual([ + expect( + SearchUIUtils.getColumnsToShow({ + currentAccountID: 1, + data: [], + visibleColumns: [], + isExpenseReportView: false, + type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, + }), + ).toEqual([ CONST.SEARCH.TABLE_COLUMNS.AVATAR, CONST.SEARCH.TABLE_COLUMNS.DATE, CONST.SEARCH.TABLE_COLUMNS.STATUS, @@ -6823,7 +6831,15 @@ describe('SearchUIUtils', () => { test('Should show specific columns when custom columns are saved & viewing expense reports', () => { const visibleColumns = [CONST.SEARCH.TABLE_COLUMNS.DATE, CONST.SEARCH.TABLE_COLUMNS.STATUS, CONST.SEARCH.TABLE_COLUMNS.TITLE, CONST.SEARCH.TABLE_COLUMNS.TOTAL]; - expect(SearchUIUtils.getColumnsToShow(1, [], visibleColumns, false, CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT)).toEqual([ + expect( + SearchUIUtils.getColumnsToShow({ + currentAccountID: 1, + data: [], + visibleColumns, + isExpenseReportView: false, + type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, + }), + ).toEqual([ // Avatar should always be visible CONST.SEARCH.TABLE_COLUMNS.AVATAR, CONST.SEARCH.TABLE_COLUMNS.DATE, @@ -6854,12 +6870,34 @@ describe('SearchUIUtils', () => { personalDetailsList: searchResults.data.personalDetailsList, }; - const nonStrictColumns = SearchUIUtils.getColumnsToShow(submitterAccountID, data, [], false, undefined, undefined, false, false, false, false); + const nonStrictColumns = SearchUIUtils.getColumnsToShow({ + currentAccountID: submitterAccountID, + data, + visibleColumns: [], + isExpenseReportView: false, + type: undefined, + groupBy: undefined, + isExpenseReportViewFromIOUReport: false, + shouldShowBillableColumn: false, + shouldShowReimbursableColumn: false, + shouldUseStrictDefaultExpenseColumns: false, + }); expect(nonStrictColumns).toContain(CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION); expect(nonStrictColumns).toContain(CONST.SEARCH.TABLE_COLUMNS.TAG); expect(nonStrictColumns).toContain(CONST.SEARCH.TABLE_COLUMNS.TO); - const strictColumns = SearchUIUtils.getColumnsToShow(submitterAccountID, data, [], false, undefined, undefined, false, false, false, true); + const strictColumns = SearchUIUtils.getColumnsToShow({ + currentAccountID: submitterAccountID, + data, + visibleColumns: [], + isExpenseReportView: false, + type: undefined, + groupBy: undefined, + isExpenseReportViewFromIOUReport: false, + shouldShowBillableColumn: false, + shouldShowReimbursableColumn: false, + shouldUseStrictDefaultExpenseColumns: true, + }); expect(strictColumns).toContain(CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION); expect(strictColumns).toContain(CONST.SEARCH.TABLE_COLUMNS.TAG); expect(strictColumns).not.toContain(CONST.SEARCH.TABLE_COLUMNS.TO); @@ -6948,7 +6986,12 @@ describe('SearchUIUtils', () => { }; // Test 1: No optional fields should be shown when all transactions are empty - let columns = SearchUIUtils.getColumnsToShow(submitterAccountID, [emptyTransaction, emptyTransaction], [], false); + let columns = SearchUIUtils.getColumnsToShow({ + currentAccountID: submitterAccountID, + data: [emptyTransaction, emptyTransaction], + visibleColumns: [], + isExpenseReportView: false, + }); expect(columns).not.toContain(CONST.SEARCH.TABLE_COLUMNS.MERCHANT); expect(columns).not.toContain(CONST.SEARCH.TABLE_COLUMNS.CATEGORY); expect(columns).not.toContain(CONST.SEARCH.TABLE_COLUMNS.TAG); @@ -6957,22 +7000,42 @@ describe('SearchUIUtils', () => { expect(columns).not.toContain(CONST.SEARCH.TABLE_COLUMNS.TO); // Test 2: Merchant column should show when at least one transaction has merchant - columns = SearchUIUtils.getColumnsToShow(submitterAccountID, [emptyTransaction, merchantTransaction], [], false); + columns = SearchUIUtils.getColumnsToShow({ + currentAccountID: submitterAccountID, + data: [emptyTransaction, merchantTransaction], + visibleColumns: [], + isExpenseReportView: false, + }); expect(columns).toContain(CONST.SEARCH.TABLE_COLUMNS.MERCHANT); expect(columns).not.toContain(CONST.SEARCH.TABLE_COLUMNS.CATEGORY); // Test 3: Category column should show when at least one transaction has category - columns = SearchUIUtils.getColumnsToShow(submitterAccountID, [emptyTransaction, categoryTransaction], [], false); + columns = SearchUIUtils.getColumnsToShow({ + currentAccountID: submitterAccountID, + data: [emptyTransaction, categoryTransaction], + visibleColumns: [], + isExpenseReportView: false, + }); expect(columns).toContain(CONST.SEARCH.TABLE_COLUMNS.CATEGORY); expect(columns).not.toContain(CONST.SEARCH.TABLE_COLUMNS.MERCHANT); // Test 4: Tag column should show when at least one transaction has tag - columns = SearchUIUtils.getColumnsToShow(submitterAccountID, [emptyTransaction, tagTransaction], [], false); + columns = SearchUIUtils.getColumnsToShow({ + currentAccountID: submitterAccountID, + data: [emptyTransaction, tagTransaction], + visibleColumns: [], + isExpenseReportView: false, + }); expect(columns).toContain(CONST.SEARCH.TABLE_COLUMNS.TAG); expect(columns).not.toContain(CONST.SEARCH.TABLE_COLUMNS.CATEGORY); // Test 5: Description column should show when at least one transaction has description - columns = SearchUIUtils.getColumnsToShow(submitterAccountID, [emptyTransaction, descriptionTransaction], [], false); + columns = SearchUIUtils.getColumnsToShow({ + currentAccountID: submitterAccountID, + data: [emptyTransaction, descriptionTransaction], + visibleColumns: [], + isExpenseReportView: false, + }); expect(columns).toContain(CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION); expect(columns).not.toContain(CONST.SEARCH.TABLE_COLUMNS.MERCHANT); @@ -6985,12 +7048,22 @@ describe('SearchUIUtils', () => { [`reportActions_${reportID2}`]: {[differentUsersTransactionIOUAction.reportActionID]: differentUsersTransactionIOUAction}, personalDetailsList: searchResults.data.personalDetailsList, }; - columns = SearchUIUtils.getColumnsToShow(submitterAccountID, data, [], false); + columns = SearchUIUtils.getColumnsToShow({ + currentAccountID: submitterAccountID, + data, + visibleColumns: [], + isExpenseReportView: false, + }); expect(columns).toContain(CONST.SEARCH.TABLE_COLUMNS.FROM); expect(columns).toContain(CONST.SEARCH.TABLE_COLUMNS.TO); // Test 7: Multiple columns should show when transactions have different fields - columns = SearchUIUtils.getColumnsToShow(submitterAccountID, [merchantTransaction, categoryTransaction, tagTransaction], [], false); + columns = SearchUIUtils.getColumnsToShow({ + currentAccountID: submitterAccountID, + data: [merchantTransaction, categoryTransaction, tagTransaction], + visibleColumns: [], + isExpenseReportView: false, + }); expect(columns).toContain(CONST.SEARCH.TABLE_COLUMNS.MERCHANT); expect(columns).toContain(CONST.SEARCH.TABLE_COLUMNS.CATEGORY); expect(columns).toContain(CONST.SEARCH.TABLE_COLUMNS.TAG); @@ -7013,7 +7086,12 @@ describe('SearchUIUtils', () => { }; // In expense report view, From/To columns should not be shown - const columns = SearchUIUtils.getColumnsToShow(submitterAccountID, [testTransaction], [], true); + const columns = SearchUIUtils.getColumnsToShow({ + currentAccountID: submitterAccountID, + data: [testTransaction], + visibleColumns: [], + isExpenseReportView: true, + }); // These columns should be shown based on data expect(columns).toContain(CONST.SEARCH.TABLE_COLUMNS.MERCHANT); @@ -7050,7 +7128,15 @@ describe('SearchUIUtils', () => { billable: false, }; - const columns = SearchUIUtils.getColumnsToShow(submitterAccountID, [reimbursableTransaction, nonReimbursableTransaction], [], true, undefined, undefined, false, true, true); + const columns = SearchUIUtils.getColumnsToShow({ + currentAccountID: submitterAccountID, + data: [reimbursableTransaction, nonReimbursableTransaction], + visibleColumns: [], + isExpenseReportView: true, + isExpenseReportViewFromIOUReport: false, + shouldShowBillableColumn: true, + shouldShowReimbursableColumn: true, + }); expect(columns).toContain(CONST.SEARCH.TABLE_COLUMNS.BILLABLE); expect(columns).toContain(CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE); @@ -7085,7 +7171,15 @@ describe('SearchUIUtils', () => { billable: true, }; - const columns = SearchUIUtils.getColumnsToShow(submitterAccountID, [reimbursableTransaction1, reimbursableTransaction2], [], true, undefined, undefined, false, true, false); + const columns = SearchUIUtils.getColumnsToShow({ + currentAccountID: submitterAccountID, + data: [reimbursableTransaction1, reimbursableTransaction2], + visibleColumns: [], + isExpenseReportView: true, + isExpenseReportViewFromIOUReport: false, + shouldShowBillableColumn: true, + shouldShowReimbursableColumn: false, + }); expect(columns).toContain(CONST.SEARCH.TABLE_COLUMNS.BILLABLE); expect(columns).not.toContain(CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE); @@ -7105,7 +7199,12 @@ describe('SearchUIUtils', () => { managerID: adminAccountID, }; - const columns = SearchUIUtils.getColumnsToShow(submitterAccountID, [testTransaction], [], false); + const columns = SearchUIUtils.getColumnsToShow({ + currentAccountID: submitterAccountID, + data: [testTransaction], + visibleColumns: [], + isExpenseReportView: false, + }); // Should show merchant column because modifiedMerchant has value expect(columns).toContain(CONST.SEARCH.TABLE_COLUMNS.MERCHANT); From 3a4438d2cce759b64ba100e9acb801b39af5deb1 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 20 Mar 2026 14:34:51 +0700 Subject: [PATCH 10/21] fix attendee avatar --- src/components/TransactionItemRow/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 34fc92eaeb66..b935e87b7e41 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -287,7 +287,7 @@ function TransactionItemRow({ transactionAttendees.map((attendee) => ({ id: attendee.accountID ?? CONST.DEFAULT_NUMBER_ID, name: attendee.displayName ?? attendee.email, - source: (attendee.avatarUrl || getDefaultAvatar({accountID: attendee.accountID, defaultAvatars})) ?? '', + source: (attendee.avatarUrl || getDefaultAvatar({accountID: attendee.accountID, accountEmail: attendee.email, defaultAvatars})) ?? '', type: CONST.ICON_TYPE_AVATAR, })), [defaultAvatars, transactionAttendees], From 4e9cea807c0d3964083f2bf936e4ce4ee0ac4b29 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 23 Mar 2026 21:02:08 +0700 Subject: [PATCH 11/21] update avatar size --- src/components/TransactionItemRow/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index b935e87b7e41..97606296dbeb 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -551,7 +551,7 @@ function TransactionItemRow({ textStyle: [styles.textMicroBold, styles.buttonSuccessText], overlayStyle: StyleUtils.getBackgroundColorWithOpacityStyle(colors.productDark400, variables.overlayOpacity), }} - size={CONST.AVATAR_SIZE.SUBSCRIPT} + size={CONST.AVATAR_SIZE.SMALLER} isInReportAction /> )} From c960ff0e74d25332c8a3d7e20da02087fad382e4 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 23 Mar 2026 21:06:30 +0700 Subject: [PATCH 12/21] update shouldShowAttendees conditon for each transaction --- src/components/TransactionItemRow/index.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 97606296dbeb..b5416c32a4e9 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -51,6 +51,7 @@ import { isScanning, isTimeRequest, isUnreportedAndHasInvalidDistanceRateTransaction, + shouldShowAttendees as shouldShowAttendeesUtils, } from '@libs/TransactionUtils'; import {getDefaultAvatar} from '@libs/UserAvatarUtils'; import colors from '@styles/theme/colors'; @@ -293,6 +294,8 @@ function TransactionItemRow({ [defaultAvatars, transactionAttendees], ); + const shouldShowAttendees = shouldShowAttendeesUtils(CONST.IOU.TYPE.SUBMIT, policy) && attendeeIcons.length > 0; + const totalPerAttendee = useMemo(() => { const attendeesCount = transactionAttendees.length ?? 0; const totalAmount = getAmount(transactionItem); @@ -538,7 +541,7 @@ function TransactionItemRow({ key={column} style={[StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.ATTENDEES)]} > - {!!attendeeIcons.length && ( + {shouldShowAttendees && ( - {!!attendeeIcons.length && ( + {shouldShowAttendees && ( Date: Tue, 24 Mar 2026 14:26:04 +0700 Subject: [PATCH 13/21] implement AttendeesCell component --- .../ReportActionAvatar.tsx | 19 +-- src/components/ReportActionAvatars/index.tsx | 17 +-- .../Search/AttendeesCell.tsx | 139 ++++++++++++++++++ src/components/TransactionItemRow/index.tsx | 34 +++-- src/styles/utils/index.ts | 4 +- 5 files changed, 164 insertions(+), 49 deletions(-) create mode 100644 src/components/SelectionListWithSections/Search/AttendeesCell.tsx diff --git a/src/components/ReportActionAvatars/ReportActionAvatar.tsx b/src/components/ReportActionAvatars/ReportActionAvatar.tsx index 02143753c6f2..aad0061dfbc0 100644 --- a/src/components/ReportActionAvatars/ReportActionAvatar.tsx +++ b/src/components/ReportActionAvatars/ReportActionAvatar.tsx @@ -1,6 +1,6 @@ import lodashSortBy from 'lodash/sortBy'; import React, {useMemo} from 'react'; -import type {ColorValue, ImageStyle, StyleProp, TextStyle, ViewStyle} from 'react-native'; +import type {ColorValue, ImageStyle, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import type {ValueOf} from 'type-fest'; import type {UpperCaseCharacters} from 'type-fest/source/internal'; @@ -55,15 +55,6 @@ type HorizontalStacking = Partial<{ /** Prop to sort the avatars */ sort: SortingOptions | SortingOptions[]; - - /** Border color for the active avatar */ - pressedBorderColor?: string; - - /** Inner text style */ - textStyle?: StyleProp; - - /** Style for the overlay */ - overlayStyle?: StyleProp; }>; type AvatarStyles = { @@ -317,9 +308,6 @@ function ReportActionAvatarMultipleHorizontal({ useProfileNavigationWrapper, fallbackDisplayName, reportID, - pressedBorderColor, - textStyle, - overlayStyle, }: HorizontalStacking & { size: ValueOf; shouldShowTooltip: boolean; @@ -405,7 +393,6 @@ function ReportActionAvatarMultipleHorizontal({ isInReportAction, shouldUseCardBackground, isActive, - customPressedBorderColor: pressedBorderColor, }), StyleUtils.getAvatarBorderWidth(size), ]} @@ -438,19 +425,17 @@ function ReportActionAvatarMultipleHorizontal({ isPressed, isInReportAction, shouldUseCardBackground, - customPressedBorderColor: pressedBorderColor, }), // Set overlay background color with RGBA value so that the text will not inherit opacity StyleUtils.getBackgroundColorWithOpacityStyle(theme.overlay, variables.overlayOpacity), StyleUtils.getHorizontalStackedOverlayAvatarStyle(oneAvatarSize, oneAvatarBorderWidth), icons.at(3)?.type === CONST.ICON_TYPE_WORKSPACE && StyleUtils.getAvatarBorderRadius(size, icons.at(3)?.type), - overlayStyle, ]} > {`+${avatars.length - maxAvatarsInRow}`} diff --git a/src/components/ReportActionAvatars/index.tsx b/src/components/ReportActionAvatars/index.tsx index 51375346949f..5f49c30b4c40 100644 --- a/src/components/ReportActionAvatars/index.tsx +++ b/src/components/ReportActionAvatars/index.tsx @@ -7,7 +7,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {InvitedEmailsToAccountIDs, Policy, Report, ReportAction} from '@src/types/onyx'; import type {CardFeed} from '@src/types/onyx/CardFeeds'; -import type {Icon as IconType} from '@src/types/onyx/OnyxCommon'; import type {HorizontalStacking} from './ReportActionAvatar'; import ReportActionAvatar from './ReportActionAvatar'; import useReportActionAvatars from './useReportActionAvatars'; @@ -74,12 +73,6 @@ type ReportActionAvatarsProps = { /** chatReportID needed for the avatars logic. When provided, this will be used as a fallback if the snapshot is undefined */ chatReportID?: string; - - /** Custom avatars to display instead of the avatars from the hook */ - customAvatars?: IconType[]; - - /** Custom avatar type to display instead of the avatar type from the hook */ - customAvatarType?: ValueOf; }; /** @@ -113,8 +106,6 @@ function ReportActionAvatars({ invitedEmailsToAccountIDs, shouldUseCustomFallbackAvatar = false, chatReportID, - customAvatars, - customAvatarType, }: ReportActionAvatarsProps) { const accountIDs = passedAccountIDs.filter((accountID) => accountID !== CONST.DEFAULT_NUMBER_ID); @@ -135,7 +126,7 @@ function ReportActionAvatars({ const { avatarType: notPreciseAvatarType, - avatars, + avatars: icons, details: {delegateAccountID}, source, } = useReportActionAvatars({ @@ -152,8 +143,6 @@ function ReportActionAvatars({ chatReportID, }); - const icons = customAvatars ?? avatars; - let avatarType: ValueOf = notPreciseAvatarType; if (avatarType === CONST.REPORT_ACTION_AVATARS.TYPE.MULTIPLE && !icons.length) { @@ -164,10 +153,6 @@ function ReportActionAvatars({ avatarType = shouldStackHorizontally ? CONST.REPORT_ACTION_AVATARS.TYPE.MULTIPLE_HORIZONTAL : CONST.REPORT_ACTION_AVATARS.TYPE.MULTIPLE_DIAGONAL; } - if (customAvatarType) { - avatarType = customAvatarType; - } - const [primaryAvatar, secondaryAvatar] = icons; if (avatarType === CONST.REPORT_ACTION_AVATARS.TYPE.SUBSCRIPT && (!!secondaryAvatar?.name || !!subscriptCardFeed)) { diff --git a/src/components/SelectionListWithSections/Search/AttendeesCell.tsx b/src/components/SelectionListWithSections/Search/AttendeesCell.tsx new file mode 100644 index 000000000000..ab898a9123eb --- /dev/null +++ b/src/components/SelectionListWithSections/Search/AttendeesCell.tsx @@ -0,0 +1,139 @@ +import React, {useMemo} from 'react'; +import {View} from 'react-native'; +import Avatar from '@components/Avatar'; +import Text from '@components/Text'; +import Tooltip from '@components/Tooltip'; +import UserDetailsTooltip from '@components/UserDetailsTooltip'; +import useDefaultAvatars from '@hooks/useDefaultAvatars'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getUserDetailTooltipText, sortIconsByName} from '@libs/ReportUtils'; +import {getDefaultAvatar} from '@libs/UserAvatarUtils'; +import colors from '@styles/theme/colors'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Attendee} from '@src/types/onyx/IOU'; +import type {Icon as IconType} from '@src/types/onyx/OnyxCommon'; + +type AttendeesCellProps = { + attendees: Attendee[]; + isHovered: boolean; + isPressed: boolean; +}; + +function AttendeesCell({attendees, isHovered, isPressed}: AttendeesCellProps) { + const defaultAvatars = useDefaultAvatars(); + const attendeeIcons: IconType[] = useMemo( + () => + attendees.map((attendee) => ({ + id: attendee.accountID ?? CONST.DEFAULT_NUMBER_ID, + name: attendee.displayName ?? attendee.email, + source: (attendee.avatarUrl || getDefaultAvatar({accountID: attendee.accountID, accountEmail: attendee.email, defaultAvatars})) ?? '', + type: CONST.ICON_TYPE_AVATAR, + })), + [defaultAvatars, attendees], + ); + + const theme = useTheme(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {localeCompare, formatPhoneNumber} = useLocalize(); + + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); + + const size = CONST.AVATAR_SIZE.SMALLER; + const maxAvatarsInRow = CONST.AVATAR_ROW_SIZE.DEFAULT; + const oneAvatarSize = StyleUtils.getAvatarStyle(size); + const oneAvatarBorderWidth = StyleUtils.getAvatarBorderWidth(size).borderWidth ?? 0; + const overlapSize = oneAvatarSize.width / 3 + 2 * oneAvatarBorderWidth; + const height = oneAvatarSize.height; + const avatarContainerStyles = StyleUtils.combineStyles([styles.alignItemsCenter, styles.flexRow, StyleUtils.getHeight(height), styles.overflowHidden]); + + const icons = sortIconsByName(attendeeIcons, personalDetails, localeCompare); + const tooltipTexts = useMemo(() => icons.map((icon) => getUserDetailTooltipText(Number(icon.id), formatPhoneNumber, icon.name)), [icons, formatPhoneNumber]); + + return ( + + {[...icons].splice(0, maxAvatarsInRow).map((icon, index) => ( + + + + + + ))} + {icons.length > maxAvatarsInRow && ( + + + + {`+${icons.length - maxAvatarsInRow}`} + + + + )} + + ); +} + +export default AttendeesCell; diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index b5416c32a4e9..a5e2a3325d6c 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -9,6 +9,7 @@ import RadioButton from '@components/RadioButton'; import ReportActionAvatars from '@components/ReportActionAvatars'; import type {SearchColumnType, TableColumnSize} from '@components/Search/types'; import ActionCell from '@components/SelectionListWithSections/Search/ActionCell'; +import AttendeesCell from '@components/SelectionListWithSections/Search/AttendeesCell'; import DateCell from '@components/SelectionListWithSections/Search/DateCell'; import ExportedIconCell from '@components/SelectionListWithSections/Search/ExportedIconCell'; import StatusCell from '@components/SelectionListWithSections/Search/StatusCell'; @@ -542,20 +543,25 @@ function TransactionItemRow({ style={[StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.ATTENDEES)]} > {shouldShowAttendees && ( - + )} diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index a6e18dfa34f9..47e7db267db5 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -868,9 +868,9 @@ function getHorizontalStackedAvatarBorderStyle({ /** * Get computed avatar styles based on position and border size */ -function getHorizontalStackedAvatarStyle(index: number, overlapSize: number): ViewStyle { +function getHorizontalStackedAvatarStyle(index: number, overlapSize: number, firstAvatarMarginLeft = 0): ViewStyle { return { - marginLeft: index > 0 ? -overlapSize : 0, + marginLeft: index > 0 ? -overlapSize : firstAvatarMarginLeft, zIndex: index + 2, }; } From 83c9223320cf3b90669c895542b4bc485b15c45e Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 24 Mar 2026 14:29:01 +0700 Subject: [PATCH 14/21] fix lint --- src/components/TransactionItemRow/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index f4b658313aea..5b176c617923 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -54,8 +54,6 @@ import { shouldShowAttendees as shouldShowAttendeesUtils, } from '@libs/TransactionUtils'; import {getDefaultAvatar} from '@libs/UserAvatarUtils'; -import colors from '@styles/theme/colors'; -import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type {PersonalDetails, Policy, Report, ReportAction, TransactionViolation} from '@src/types/onyx'; From 3df12e03f692119e15ca8712bb9447ad93eba009 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 24 Mar 2026 20:54:33 +0700 Subject: [PATCH 15/21] fix key for attendee cell --- src/components/Search/SearchList/ListItem/AttendeesCell.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Search/SearchList/ListItem/AttendeesCell.tsx b/src/components/Search/SearchList/ListItem/AttendeesCell.tsx index ab898a9123eb..55f197ac7b56 100644 --- a/src/components/Search/SearchList/ListItem/AttendeesCell.tsx +++ b/src/components/Search/SearchList/ListItem/AttendeesCell.tsx @@ -63,7 +63,8 @@ function AttendeesCell({attendees, isHovered, isPressed}: AttendeesCellProps) { > {[...icons].splice(0, maxAvatarsInRow).map((icon, index) => ( Date: Tue, 24 Mar 2026 21:21:29 +0700 Subject: [PATCH 16/21] update translation --- src/components/TransactionItemRow/index.tsx | 17 +---------------- 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 +- 11 files changed, 11 insertions(+), 26 deletions(-) diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 5b176c617923..9fa120915253 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -19,7 +19,6 @@ import type {SearchColumnType, TableColumnSize} from '@components/Search/types'; import Text from '@components/Text'; import TextWithTooltip from '@components/TextWithTooltip'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useDefaultAvatars from '@hooks/useDefaultAvatars'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -53,11 +52,9 @@ import { isUnreportedAndHasInvalidDistanceRateTransaction, shouldShowAttendees as shouldShowAttendeesUtils, } from '@libs/TransactionUtils'; -import {getDefaultAvatar} from '@libs/UserAvatarUtils'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type {PersonalDetails, Policy, Report, ReportAction, TransactionViolation} from '@src/types/onyx'; -import type {Icon as IconType} from '@src/types/onyx/OnyxCommon'; import type {SearchTransactionAction} from '@src/types/onyx/SearchResults'; import CategoryCell from './DataCells/CategoryCell'; import ChatBubbleCell from './DataCells/ChatBubbleCell'; @@ -213,7 +210,6 @@ function TransactionItemRow({ const createdAt = getTransactionCreated(transactionItem); const expensicons = useMemoizedLazyExpensifyIcons(['ArrowRight']); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const defaultAvatars = useDefaultAvatars(); const transactionThreadReportID = reportActions ? getIOUActionForTransactionID(reportActions, transactionItem.transactionID)?.childReportID : undefined; const isDateColumnWide = dateColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE; @@ -281,18 +277,7 @@ function TransactionItemRow({ const transactionAttendees = useMemo(() => getAttendees(transactionItem, currentUserPersonalDetails), [transactionItem, currentUserPersonalDetails]); - const attendeeIcons: IconType[] = useMemo( - () => - transactionAttendees.map((attendee) => ({ - id: attendee.accountID ?? CONST.DEFAULT_NUMBER_ID, - name: attendee.displayName ?? attendee.email, - source: (attendee.avatarUrl || getDefaultAvatar({accountID: attendee.accountID, accountEmail: attendee.email, defaultAvatars})) ?? '', - type: CONST.ICON_TYPE_AVATAR, - })), - [defaultAvatars, transactionAttendees], - ); - - const shouldShowAttendees = shouldShowAttendeesUtils(CONST.IOU.TYPE.SUBMIT, policy) && attendeeIcons.length > 0; + const shouldShowAttendees = shouldShowAttendeesUtils(CONST.IOU.TYPE.SUBMIT, policy) && transactionAttendees.length > 0; const totalPerAttendee = useMemo(() => { const attendeesCount = transactionAttendees.length ?? 0; diff --git a/src/languages/de.ts b/src/languages/de.ts index a2b86911a6b3..029af8283be3 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1513,7 +1513,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Diese Buchung ist archiviert', bookingArchivedDescription: 'Diese Buchung ist archiviert, weil das Reisedatum verstrichen ist. Füge bei Bedarf eine Ausgabe für den endgültigen Betrag hinzu.', attendees: 'Teilnehmende', - totalPerAttendee: 'Betrag pro Teilnehmendem', + totalPerAttendee: 'Pro Teilnehmendem', whoIsYourAccountant: 'Wer ist Ihre Steuerberaterin bzw. Ihr Steuerberater?', paymentComplete: 'Zahlung abgeschlossen', time: 'Zeit', diff --git a/src/languages/en.ts b/src/languages/en.ts index 7a9910ac1a06..83d27bb0026c 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1562,7 +1562,7 @@ const translations = { bookingArchived: 'This booking is archived', bookingArchivedDescription: 'This booking is archived because the trip date has passed. Add an expense for the final amount if needed.', attendees: 'Attendees', - totalPerAttendee: 'Amount per attendee', + totalPerAttendee: 'Per attendee', whoIsYourAccountant: 'Who is your accountant?', paymentComplete: 'Payment complete', time: 'Time', diff --git a/src/languages/es.ts b/src/languages/es.ts index 7d6b13c287b9..65d0277e0a8c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1434,7 +1434,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Esta reserva está archivada', bookingArchivedDescription: 'Esta reserva está archivada porque la fecha del viaje ha pasado. Agregue un gasto por el monto final si es necesario.', attendees: 'Asistentes', - totalPerAttendee: 'Monto por asistente', + totalPerAttendee: 'Por asistente', whoIsYourAccountant: '¿Quién es tu contador?', paymentComplete: 'Pago completo', time: 'Tiempo', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 9fb9344e1ff9..8808ca5a6a1d 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1517,7 +1517,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Cette réservation est archivée', bookingArchivedDescription: 'Cette réservation est archivée car la date du voyage est passée. Ajoutez une dépense pour le montant final si nécessaire.', attendees: 'Participants', - totalPerAttendee: 'Montant par participant', + totalPerAttendee: 'Par participant', whoIsYourAccountant: 'Qui est votre comptable ?', paymentComplete: 'Paiement terminé', time: 'Heure', diff --git a/src/languages/it.ts b/src/languages/it.ts index 715db1e1409f..aedb0c6b5239 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1512,7 +1512,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Questa prenotazione è archiviata', bookingArchivedDescription: 'Questa prenotazione è archiviata perché la data del viaggio è passata. Aggiungi una spesa per l’importo finale, se necessario.', attendees: 'Partecipanti', - totalPerAttendee: 'Importo per partecipante', + totalPerAttendee: 'Per partecipante', whoIsYourAccountant: 'Chi è il tuo commercialista?', paymentComplete: 'Pagamento completato', time: 'Ora', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 0cd6d21625aa..f7c26061a0b4 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1493,7 +1493,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'この予約はアーカイブされています', bookingArchivedDescription: 'この予約は旅行日が過ぎたためアーカイブされています。必要に応じて、最終金額の経費を追加してください。', attendees: '参加者', - totalPerAttendee: '参加者一人あたりの金額', + totalPerAttendee: '参加者ごと', whoIsYourAccountant: 'あなたの会計士は誰ですか?', paymentComplete: '支払いが完了しました', time: '時間', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 95f28a4156cd..1c7583745416 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1509,7 +1509,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Deze boeking is gearchiveerd', bookingArchivedDescription: 'Deze boeking is gearchiveerd omdat de reisdatum is verstreken. Voeg indien nodig een uitgave toe voor het eindbedrag.', attendees: 'Deelnemers', - totalPerAttendee: 'Bedrag per deelnemer', + totalPerAttendee: 'Per deelnemer', whoIsYourAccountant: 'Wie is jouw accountant?', paymentComplete: 'Betaling voltooid', time: 'Tijd', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index b11d4014c83a..5e3de5a88e28 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1508,7 +1508,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Ta rezerwacja jest zarchiwizowana', bookingArchivedDescription: 'Ta rezerwacja została zarchiwizowana, ponieważ data podróży już minęła. W razie potrzeby dodaj wydatek na ostateczną kwotę.', attendees: 'Uczestnicy', - totalPerAttendee: 'Kwota na uczestnika', + totalPerAttendee: 'Na uczestnika', whoIsYourAccountant: 'Kim jest twój księgowy?', paymentComplete: 'Płatność zakończona', time: 'Czas', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 28db44d9087d..cea1c4e89cbe 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1506,7 +1506,7 @@ const translations: TranslationDeepObject = { bookingArchived: 'Esta reserva está arquivada', bookingArchivedDescription: 'Esta reserva está arquivada porque a data da viagem já passou. Adicione uma despesa com o valor final, se necessário.', attendees: 'Participantes', - totalPerAttendee: 'Valor por participante', + totalPerAttendee: 'Por participante', whoIsYourAccountant: 'Quem é seu contador?', paymentComplete: 'Pagamento concluído', time: 'Hora', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 4e82097c7385..3f16e11ecff1 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1465,7 +1465,7 @@ const translations: TranslationDeepObject = { bookingArchived: '此预订已归档', bookingArchivedDescription: '此预订已归档,因为行程日期已过。如有需要,请为最终金额添加一笔报销。', attendees: '参与者', - totalPerAttendee: '每位参与者金额', + totalPerAttendee: '每位参与者', whoIsYourAccountant: '你的会计是谁?', paymentComplete: '付款完成', time: '时间', From 8966c13356674aa28293a0189da4a5c06b15b034 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 26 Mar 2026 22:36:58 +0700 Subject: [PATCH 17/21] fix crash bug --- src/libs/SearchUIUtils.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index fe80eabc214e..6a71eb201a87 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -440,6 +440,8 @@ type TransactionKey = `${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}`; type ReportActionKey = `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}${string}`; +type PolicyKey = `${typeof ONYXKEYS.COLLECTION.POLICY}${string}`; + type ViolationKey = `${typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${string}`; type SearchGroupKey = `${typeof CONST.SEARCH.GROUP_PREFIX}${string}`; @@ -1191,6 +1193,13 @@ function isTransactionEntry(key: string): key is TransactionKey { return key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION); } +/** + * @private + */ +function isPolicyEntry(key: string): key is PolicyKey { + return key.startsWith(ONYXKEYS.COLLECTION.POLICY); +} + /** * Determines whether to display the merchant field based on the transactions in the search results. */ From e3a8f7e60b6c8e44515f1a3207a977d53a72ab04 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 27 Mar 2026 14:52:01 +0700 Subject: [PATCH 18/21] fix ts --- src/components/Search/FilterDropdowns/SortByPopup.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/Search/FilterDropdowns/SortByPopup.tsx b/src/components/Search/FilterDropdowns/SortByPopup.tsx index 3d1b3ecc796e..e12a866ad921 100644 --- a/src/components/Search/FilterDropdowns/SortByPopup.tsx +++ b/src/components/Search/FilterDropdowns/SortByPopup.tsx @@ -35,7 +35,15 @@ function SortByPopup({searchResults, queryJSON, groupBy, onSort, closeOverlay}: const {clearSelectedTransactions} = useSearchActionsContext(); const [visibleColumns] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {selector: columnsSelector}); const searchDataType = shouldUseLiveData ? CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT : searchResults?.search?.type; - const currentColumns = !searchResults?.data ? [] : getColumnsToShow(accountID, searchResults?.data, visibleColumns, false, searchDataType, groupBy?.value); + const currentColumns = !searchResults?.data + ? [] + : getColumnsToShow({ + currentAccountID: accountID, + data: searchResults?.data, + visibleColumns, + type: searchDataType, + groupBy: groupBy?.value, + }); const sortableColumns = getSortByOptions(currentColumns, translate); const sortBy = {text: translate(getSearchColumnTranslationKey(queryJSON.sortBy)), value: queryJSON.sortBy}; From 7ad290633e96073c1fad56e2230c0cd5af454fa6 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 31 Mar 2026 14:59:45 +0700 Subject: [PATCH 19/21] fix attendee avatar --- src/libs/TransactionUtils/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 98e6f2b3ad7e..de1c92d24b23 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1178,7 +1178,7 @@ function getReportOwnerAsAttendee(transaction: OnyxInputOrEntry, cu accountID: creatorAccountID, text: creatorDisplayName, searchText: creatorDisplayName, - avatarUrl: creatorDetails?.avatarThumbnail ?? '', + avatarUrl: (creatorDetails?.avatarThumbnail ?? creatorDetails?.avatar ?? '') as string, selected: true, }; } From f3ae72458c2b9945c0f6df007ddc57d8ed8219a7 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 1 Apr 2026 23:11:35 +0700 Subject: [PATCH 20/21] remove useMemo --- .../SearchList/ListItem/AttendeesCell.tsx | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/components/Search/SearchList/ListItem/AttendeesCell.tsx b/src/components/Search/SearchList/ListItem/AttendeesCell.tsx index 55f197ac7b56..ea45c07ee33d 100644 --- a/src/components/Search/SearchList/ListItem/AttendeesCell.tsx +++ b/src/components/Search/SearchList/ListItem/AttendeesCell.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react'; +import React from 'react'; import {View} from 'react-native'; import Avatar from '@components/Avatar'; import Text from '@components/Text'; @@ -27,16 +27,12 @@ type AttendeesCellProps = { function AttendeesCell({attendees, isHovered, isPressed}: AttendeesCellProps) { const defaultAvatars = useDefaultAvatars(); - const attendeeIcons: IconType[] = useMemo( - () => - attendees.map((attendee) => ({ - id: attendee.accountID ?? CONST.DEFAULT_NUMBER_ID, - name: attendee.displayName ?? attendee.email, - source: (attendee.avatarUrl || getDefaultAvatar({accountID: attendee.accountID, accountEmail: attendee.email, defaultAvatars})) ?? '', - type: CONST.ICON_TYPE_AVATAR, - })), - [defaultAvatars, attendees], - ); + const attendeeIcons: IconType[] = attendees.map((attendee) => ({ + id: attendee.accountID ?? CONST.DEFAULT_NUMBER_ID, + name: attendee.displayName ?? attendee.email, + source: (attendee.avatarUrl || getDefaultAvatar({accountID: attendee.accountID, accountEmail: attendee.email, defaultAvatars})) ?? '', + type: CONST.ICON_TYPE_AVATAR, + })); const theme = useTheme(); const styles = useThemeStyles(); @@ -54,7 +50,7 @@ function AttendeesCell({attendees, isHovered, isPressed}: AttendeesCellProps) { const avatarContainerStyles = StyleUtils.combineStyles([styles.alignItemsCenter, styles.flexRow, StyleUtils.getHeight(height), styles.overflowHidden]); const icons = sortIconsByName(attendeeIcons, personalDetails, localeCompare); - const tooltipTexts = useMemo(() => icons.map((icon) => getUserDetailTooltipText(Number(icon.id), formatPhoneNumber, icon.name)), [icons, formatPhoneNumber]); + const tooltipTexts = icons.map((icon) => getUserDetailTooltipText(Number(icon.id), formatPhoneNumber, icon.name)); return ( Date: Wed, 1 Apr 2026 23:17:47 +0700 Subject: [PATCH 21/21] show scanning per attendee --- src/components/TransactionItemRow/index.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 9977cb3df012..8bff270e24ce 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -17,7 +17,6 @@ import UserInfoCell from '@components/Search/SearchList/ListItem/UserInfoCell'; import WorkspaceCell from '@components/Search/SearchList/ListItem/WorkspaceCell'; import type {SearchColumnType, TableColumnSize} from '@components/Search/types'; import Text from '@components/Text'; -import TextWithTooltip from '@components/TextWithTooltip'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -26,7 +25,6 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {isCategoryMissing} from '@libs/CategoryUtils'; -import {convertToDisplayString} from '@libs/CurrencyUtils'; import getBase62ReportID from '@libs/getBase62ReportID'; import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils'; import {getReportName} from '@libs/ReportNameUtils'; @@ -577,11 +575,10 @@ function TransactionItemRow({ style={[StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.TOTAL_PER_ATTENDEE, undefined, isAmountColumnWide)]} > {shouldShowAttendees && ( - )}