From 4a0e174e9a6fbfcddbe423603a6fa3b901c541dc Mon Sep 17 00:00:00 2001 From: Mukher Date: Wed, 10 Jun 2026 15:46:11 +0500 Subject: [PATCH 01/16] added missing translations --- src/languages/de.ts | 35 ++++++++++++++++++++--------------- src/languages/es.ts | 35 ++++++++++++++++++++--------------- src/languages/fr.ts | 35 ++++++++++++++++++++--------------- src/languages/it.ts | 35 ++++++++++++++++++++--------------- src/languages/ja.ts | 35 ++++++++++++++++++++--------------- src/languages/nl.ts | 35 ++++++++++++++++++++--------------- src/languages/pl.ts | 35 ++++++++++++++++++++--------------- src/languages/pt-BR.ts | 35 ++++++++++++++++++++--------------- src/languages/zh-hans.ts | 35 ++++++++++++++++++++--------------- 9 files changed, 180 insertions(+), 135 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index d49ab9960341..71d970c82c95 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -4134,28 +4134,31 @@ ${amount} für ${merchant} – ${date}`, verificationFailed: 'Die Verifizierung ist fehlgeschlagen, daher benötigen wir zusätzliche Dokumente, um dich und dein Unternehmen zu überprüfen', taxIDVerification: 'Steuer-ID-Verifizierung', taxIDVerificationDescription: dedent(` - Bitte lade eine der folgenden Dateien hoch: - • IRS TIN/EIN-Zuweisungsschreiben - • IRS TIN/EIN-Antragsbestätigung (enthält normalerweise „Congratulations! The EIN has been successfully assigned“) - • IRS-Steuerbefreiungsschreiben mit Firmenname und EIN`), + Bitte lade eine der folgenden Dateien hoch: + • IRS TIN/EIN-Zuweisungsschreiben + • IRS TIN/EIN-Antragsbestätigung (enthält normalerweise „Congratulations! The EIN has been successfully assigned“) + • IRS-Steuerbefreiungsschreiben mit Firmenname und EIN + `), nameChangeDocument: 'Dokument zur Namensänderung', nameChangeDocumentDescription: 'Wenn sich der Name deines Unternehmens seit der Beantragung der TIN/EIN geändert hat, benötigen wir dieses Dokument zur Verifizierung der angegebenen Steuer-ID', companyAddressVerification: 'Verifizierung der Unternehmensadresse', companyAddressVerificationDescription: dedent(` - Bitte lade eine der folgenden Dateien hoch: - • Aktuelle Strom-, Wasser- oder Gasrechnung mit Firmenname und Adresse - • Kontoauszug mit Firmenname und Adresse - • Aktueller Miet- oder Leasingvertrag inkl. Unterschriftsseite mit Firmenname und aktueller Adresse - • Versicherungsnachweis mit Firmenname und Adresse - • TIN-Zuweisungsdokument mit Firmenname und Adresse`), + Bitte lade eine der folgenden Dateien hoch: + • Aktuelle Strom-, Wasser- oder Gasrechnung mit Firmenname und Adresse + • Kontoauszug mit Firmenname und Adresse + • Aktueller Miet- oder Leasingvertrag inkl. Unterschriftsseite mit Firmenname und aktueller Adresse + • Versicherungsnachweis mit Firmenname und Adresse + • TIN-Zuweisungsdokument mit Firmenname und Adresse + `), userAddressVerification: 'Adressverifizierung', userAddressVerificationDescription: dedent(` - Bitte lade eine der folgenden Dateien hoch: - • Wählerregistrierungskarte - • Führerschein - • Kontoauszug - • Versorgungsrechnung`), + Bitte lade eine der folgenden Dateien hoch: + • Wählerregistrierungskarte + • Führerschein + • Kontoauszug + • Versorgungsrechnung + `), userDOBVerification: 'Geburtsdatumsverifizierung', userDOBVerificationDescription: 'Bitte lade einen in den USA ausgestellten Ausweis hoch', finishViaChat: 'Über Chat abschließen', @@ -8274,6 +8277,8 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc [CONST.SEARCH.ACTION_FILTERS.PAY]: 'Bezahlen', [CONST.SEARCH.ACTION_FILTERS.EXPORT]: 'Export', }, + firstApprover: 'Erste:r Genehmiger:in', + firstApproved: 'Zuerst genehmigt', }, display: { label: 'Anzeige', diff --git a/src/languages/es.ts b/src/languages/es.ts index 4ed11c0837eb..94fa5202ee87 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4017,27 +4017,30 @@ ${amount} para ${merchant} - ${date}`, verificationFailed: 'La verificación falló, por lo que necesitaremos documentos adicionales para verificarte a ti y a tu empresa', taxIDVerification: 'Verificación del ID fiscal', taxIDVerificationDescription: dedent(` - Por favor, sube uno de los siguientes archivos: - • Carta de asignación de TIN/EIN del IRS - • Confirmación de solicitud de TIN/EIN del IRS (normalmente indica "Congratulations! The EIN has been successfully assigned") - • Carta de exención fiscal del IRS que incluya el nombre de la empresa y el EIN`), + Por favor, sube uno de los siguientes archivos: + • Carta de asignación de TIN/EIN del IRS + • Confirmación de solicitud de TIN/EIN del IRS (normalmente indica "Congratulations! The EIN has been successfully assigned") + • Carta de exención fiscal del IRS que incluya el nombre de la empresa y el EIN + `), nameChangeDocument: 'Documento de cambio de nombre', nameChangeDocumentDescription: 'Si el nombre de tu empresa cambió desde que solicitaste el TIN/EIN, necesitamos este documento para verificar el número de ID fiscal proporcionado', companyAddressVerification: 'Verificación de la dirección de la empresa', companyAddressVerificationDescription: dedent(` - Por favor, sube uno de los siguientes archivos: - • Factura reciente de servicios públicos con nombre y dirección de la empresa - • Estado de cuenta bancario con nombre y dirección de la empresa - • Contrato de arrendamiento vigente con página de firmas que muestre el nombre y la dirección actual de la empresa - • Estado de seguro con nombre y dirección de la empresa - • Documento de asignación de TIN con nombre y dirección de la empresa`), + Por favor, sube uno de los siguientes archivos: + • Factura reciente de servicios públicos con nombre y dirección de la empresa + • Estado de cuenta bancario con nombre y dirección de la empresa + • Contrato de arrendamiento vigente con página de firmas que muestre el nombre y la dirección actual de la empresa + • Estado de seguro con nombre y dirección de la empresa + • Documento de asignación de TIN con nombre y dirección de la empresa + `), userAddressVerification: 'Verificación de dirección', userAddressVerificationDescription: dedent(` - Por favor, sube uno de los siguientes archivos: - • Tarjeta de registro de votante - • Licencia de conducir - • Estado de cuenta bancario - • Factura de servicios públicos`), + Por favor, sube uno de los siguientes archivos: + • Tarjeta de registro de votante + • Licencia de conducir + • Estado de cuenta bancario + • Factura de servicios públicos + `), userDOBVerification: 'Verificación de fecha de nacimiento', userDOBVerificationDescription: 'Por favor, sube una identificación emitida en EE. UU.', finishViaChat: 'Finalizar por chat', @@ -8100,6 +8103,8 @@ ${amount} para ${merchant} - ${date}`, [CONST.SEARCH.ACTION_FILTERS.EXPORT]: 'Exportar', }, reportField: (name, value) => `${name} es ${value}`, + firstApprover: 'Primer aprobador', + firstApproved: 'Primera aprobación', }, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'De', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 81ed59d53e2f..408d4d5d7f79 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -4147,28 +4147,31 @@ ${amount} pour ${merchant} - ${date}`, verificationFailed: 'La vérification a échoué, nous aurons donc besoin de documents supplémentaires pour te vérifier ainsi que ton entreprise', taxIDVerification: 'Vérification de l’identifiant fiscal', taxIDVerificationDescription: dedent(` - Veuillez téléverser l’un des fichiers suivants : - • Lettre d’attribution TIN/EIN de l’IRS - • Confirmation de demande TIN/EIN de l’IRS (indique généralement « Congratulations! The EIN has been successfully assigned ») - • Lettre d’exonération fiscale de l’IRS indiquant le nom de l’entreprise et l’EIN`), + Veuillez téléverser l’un des fichiers suivants : + • Lettre d’attribution TIN/EIN de l’IRS + • Confirmation de demande TIN/EIN de l’IRS (indique généralement « Congratulations! The EIN has been successfully assigned ») + • Lettre d’exonération fiscale de l’IRS indiquant le nom de l’entreprise et l’EIN + `), nameChangeDocument: 'Document de changement de nom', nameChangeDocumentDescription: 'Si le nom de ton entreprise a changé depuis la demande du TIN/EIN, ce document est nécessaire pour vérifier le numéro d’identification fiscale fourni', companyAddressVerification: 'Vérification de l’adresse de l’entreprise', companyAddressVerificationDescription: dedent(` - Veuillez téléverser l’un des fichiers suivants : - • Facture récente de services publics indiquant le nom et l’adresse de l’entreprise - • Relevé bancaire indiquant le nom et l’adresse de l’entreprise - • Contrat de location en cours incluant la page de signature avec le nom et l’adresse actuelle de l’entreprise - • Attestation d’assurance indiquant le nom et l’adresse de l’entreprise - • Document d’attribution TIN indiquant le nom et l’adresse de l’entreprise`), + Veuillez téléverser l’un des fichiers suivants : + • Facture récente de services publics indiquant le nom et l’adresse de l’entreprise + • Relevé bancaire indiquant le nom et l’adresse de l’entreprise + • Contrat de location en cours incluant la page de signature avec le nom et l’adresse actuelle de l’entreprise + • Attestation d’assurance indiquant le nom et l’adresse de l’entreprise + • Document d’attribution TIN indiquant le nom et l’adresse de l’entreprise + `), userAddressVerification: 'Vérification de l’adresse', userAddressVerificationDescription: dedent(` - Veuillez téléverser l’un des fichiers suivants : - • Carte d’inscription électorale - • Permis de conduire - • Relevé bancaire - • Facture de services publics`), + Veuillez téléverser l’un des fichiers suivants : + • Carte d’inscription électorale + • Permis de conduire + • Relevé bancaire + • Facture de services publics + `), userDOBVerification: 'Vérification de la date de naissance', userDOBVerificationDescription: 'Veuillez téléverser une pièce d’identité délivrée aux États-Unis', finishViaChat: 'Finaliser via le chat', @@ -8309,6 +8312,8 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e [CONST.SEARCH.ACTION_FILTERS.PAY]: 'Payer', [CONST.SEARCH.ACTION_FILTERS.EXPORT]: 'Exporter', }, + firstApprover: 'Premier approbateur', + firstApproved: 'Approuvé en premier', }, display: { label: 'Affichage', diff --git a/src/languages/it.ts b/src/languages/it.ts index f73777568947..6a5d8e995fc3 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -4122,28 +4122,31 @@ ${amount} per ${merchant} - ${date}`, verificationFailed: 'La verifica non è riuscita, quindi avremo bisogno di documenti aggiuntivi per verificare te e la tua azienda', taxIDVerification: 'Verifica dell’ID fiscale', taxIDVerificationDescription: dedent(` - Carica uno dei seguenti file: - • Lettera di assegnazione TIN/EIN dell’IRS - • Conferma della richiesta TIN/EIN dell’IRS (di solito indica "Congratulations! The EIN has been successfully assigned") - • Lettera di esenzione fiscale dell’IRS con nome dell’azienda ed EIN`), + Carica uno dei seguenti file: + • Lettera di assegnazione TIN/EIN dell’IRS + • Conferma della richiesta TIN/EIN dell’IRS (di solito indica "Congratulations! The EIN has been successfully assigned") + • Lettera di esenzione fiscale dell’IRS con nome dell’azienda ed EIN + `), nameChangeDocument: 'Documento di cambio nome', nameChangeDocumentDescription: 'Se il nome della tua azienda è cambiato dopo la richiesta del TIN/EIN, abbiamo bisogno di questo documento per verificare il numero di ID fiscale fornito', companyAddressVerification: 'Verifica dell’indirizzo aziendale', companyAddressVerificationDescription: dedent(` - Carica uno dei seguenti file: - • Bolletta recente con nome e indirizzo dell’azienda - • Estratto conto bancario con nome e indirizzo dell’azienda - • Contratto di locazione attuale con pagina firme che mostri nome e indirizzo attuale dell’azienda - • Documento assicurativo con nome e indirizzo dell’azienda - • Documento di assegnazione TIN con nome e indirizzo dell’azienda`), + Carica uno dei seguenti file: + • Bolletta recente con nome e indirizzo dell’azienda + • Estratto conto bancario con nome e indirizzo dell’azienda + • Contratto di locazione attuale con pagina firme che mostri nome e indirizzo attuale dell’azienda + • Documento assicurativo con nome e indirizzo dell’azienda + • Documento di assegnazione TIN con nome e indirizzo dell’azienda + `), userAddressVerification: 'Verifica dell’indirizzo', userAddressVerificationDescription: dedent(` - Carica uno dei seguenti file: - • Tessera elettorale - • Patente di guida - • Estratto conto bancario - • Bolletta`), + Carica uno dei seguenti file: + • Tessera elettorale + • Patente di guida + • Estratto conto bancario + • Bolletta + `), userDOBVerification: 'Verifica della data di nascita', userDOBVerificationDescription: 'Carica un documento di identità rilasciato negli Stati Uniti', finishViaChat: 'Completa via chat', @@ -8267,6 +8270,8 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, [CONST.SEARCH.ACTION_FILTERS.PAY]: 'Paga', [CONST.SEARCH.ACTION_FILTERS.EXPORT]: 'Esporta', }, + firstApprover: 'Primo approvatore', + firstApproved: 'Approvato per primo', }, display: { label: 'Visualizza', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 0de98845eab1..9df09f36d7c5 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -4094,27 +4094,30 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' verificationFailed: '確認に失敗したため、追加の書類で本人および事業の確認が必要です', taxIDVerification: '納税者番号の確認', taxIDVerificationDescription: dedent(` - 以下のいずれかの書類をアップロードしてください: - • IRS TIN/EIN 割当通知書 - • IRS TIN/EIN 申請確認書(通常「Congratulations! The EIN has been successfully assigned」と記載) - • 会社名と EIN が記載された IRS の免税通知書`), + 以下のいずれかの書類をアップロードしてください: + • IRS TIN/EIN 割当通知書 + • IRS TIN/EIN 申請確認書(通常「Congratulations! The EIN has been successfully assigned」と記載) + • 会社名と EIN が記載された IRS の免税通知書 + `), nameChangeDocument: '名称変更書類', nameChangeDocumentDescription: 'TIN/EIN 申請後に会社名が変更された場合、提供された納税者番号を確認するためにこの書類が必要です', companyAddressVerification: '会社住所の確認', companyAddressVerificationDescription: dedent(` - 以下のいずれかの書類をアップロードしてください: - • 会社名と住所が記載された最近の公共料金請求書 - • 会社名と住所が記載された銀行取引明細書 - • 署名ページを含む現行の賃貸契約書(会社名と現住所が記載されたもの) - • 会社名と住所が記載された保険証書 - • 会社名と住所が記載された TIN 割当書類`), + 以下のいずれかの書類をアップロードしてください: + • 会社名と住所が記載された最近の公共料金請求書 + • 会社名と住所が記載された銀行取引明細書 + • 署名ページを含む現行の賃貸契約書(会社名と現住所が記載されたもの) + • 会社名と住所が記載された保険証書 + • 会社名と住所が記載された TIN 割当書類 + `), userAddressVerification: '住所確認', userAddressVerificationDescription: dedent(` - 以下のいずれかの書類をアップロードしてください: - • 有権者登録カード - • 運転免許証 - • 銀行取引明細書 - • 公共料金請求書`), + 以下のいずれかの書類をアップロードしてください: + • 有権者登録カード + • 運転免許証 + • 銀行取引明細書 + • 公共料金請求書 + `), userDOBVerification: '生年月日の確認', userDOBVerificationDescription: '米国発行の身分証明書をアップロードしてください', finishViaChat: 'チャットで完了', @@ -8167,6 +8170,8 @@ ${reportName} [CONST.SEARCH.ACTION_FILTERS.PAY]: '支払う', [CONST.SEARCH.ACTION_FILTERS.EXPORT]: 'エクスポート', }, + firstApprover: '最初の承認者', + firstApproved: '最初に承認済み', }, display: { label: '表示', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 0aba8236fe62..3176a3d3ff27 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -4118,27 +4118,30 @@ ${amount} voor ${merchant} - ${date}`, verificationFailed: 'De verificatie is mislukt, daarom hebben we extra documenten nodig om jou en je bedrijf te verifiëren', taxIDVerification: 'Belastingnummerverificatie', taxIDVerificationDescription: dedent(` - Upload een van de volgende bestanden: - • IRS TIN/EIN-toewijzingsbrief - • IRS TIN/EIN-aanvraagbevestiging (bevat meestal "Congratulations! The EIN has been successfully assigned") - • IRS-belastingvrijstellingsbrief met bedrijfsnaam en EIN`), + Upload een van de volgende bestanden: + • IRS TIN/EIN-toewijzingsbrief + • IRS TIN/EIN-aanvraagbevestiging (bevat meestal "Congratulations! The EIN has been successfully assigned") + • IRS-belastingvrijstellingsbrief met bedrijfsnaam en EIN + `), nameChangeDocument: 'Document naamswijziging', nameChangeDocumentDescription: 'Als de naam van je bedrijf is gewijzigd sinds de TIN/EIN-aanvraag, hebben we dit document nodig om het opgegeven belastingnummer te verifiëren', companyAddressVerification: 'Verificatie van bedrijfsadres', companyAddressVerificationDescription: dedent(` - Upload een van de volgende bestanden: - • Recente energierekening met bedrijfsnaam en adres - • Bankafschrift met bedrijfsnaam en adres - • Huidige huur- of leaseovereenkomst inclusief ondertekeningspagina met bedrijfsnaam en huidig adres - • Verzekeringsverklaring met bedrijfsnaam en adres - • TIN-toewijzingsdocument met bedrijfsnaam en adres`), + Upload een van de volgende bestanden: + • Recente energierekening met bedrijfsnaam en adres + • Bankafschrift met bedrijfsnaam en adres + • Huidige huur- of leaseovereenkomst inclusief ondertekeningspagina met bedrijfsnaam en huidig adres + • Verzekeringsverklaring met bedrijfsnaam en adres + • TIN-toewijzingsdocument met bedrijfsnaam en adres + `), userAddressVerification: 'Adresverificatie', userAddressVerificationDescription: dedent(` - Upload een van de volgende bestanden: - • Kiezersregistratiekaart - • Rijbewijs - • Bankafschrift - • Energierekening`), + Upload een van de volgende bestanden: + • Kiezersregistratiekaart + • Rijbewijs + • Bankafschrift + • Energierekening + `), userDOBVerification: 'Verificatie van geboortedatum', userDOBVerificationDescription: 'Upload een in de VS uitgegeven identiteitsbewijs', finishViaChat: 'Afronden via chat', @@ -8230,6 +8233,8 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, [CONST.SEARCH.ACTION_FILTERS.PAY]: 'Betalen', [CONST.SEARCH.ACTION_FILTERS.EXPORT]: 'Exporteren', }, + firstApprover: 'Eerste gemachtigde', + firstApproved: 'Eerst goedgekeurd', }, display: { label: 'Weergave', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 51fbc5014fc3..4ce512e9d9e5 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -4110,27 +4110,30 @@ ${amount} dla ${merchant} - ${date}`, verificationFailed: 'Weryfikacja nie powiodła się, dlatego potrzebujemy dodatkowych dokumentów do potwierdzenia Twojej tożsamości i firmy', taxIDVerification: 'Weryfikacja numeru podatkowego', taxIDVerificationDescription: dedent(` - Prześlij jeden z poniższych plików: - • List przydziału TIN/EIN z IRS - • Potwierdzenie wniosku TIN/EIN z IRS (zwykle zawiera „Congratulations! The EIN has been successfully assigned”) - • Pismo o zwolnieniu podatkowym z IRS zawierające nazwę firmy i EIN`), + Prześlij jeden z poniższych plików: + • List przydziału TIN/EIN z IRS + • Potwierdzenie wniosku TIN/EIN z IRS (zwykle zawiera „Congratulations! The EIN has been successfully assigned”) + • Pismo o zwolnieniu podatkowym z IRS zawierające nazwę firmy i EIN + `), nameChangeDocument: 'Dokument zmiany nazwy', nameChangeDocumentDescription: 'Jeśli nazwa firmy zmieniła się od momentu złożenia wniosku o TIN/EIN, dokument ten jest wymagany do weryfikacji podanego numeru podatkowego', companyAddressVerification: 'Weryfikacja adresu firmy', companyAddressVerificationDescription: dedent(` - Prześlij jeden z poniższych plików: - • Aktualny rachunek za media z nazwą i adresem firmy - • Wyciąg bankowy z nazwą i adresem firmy - • Aktualna umowa najmu z podpisaną stroną zawierającą nazwę i adres firmy - • Dokument ubezpieczeniowy z nazwą i adresem firmy - • Dokument przydziału TIN z nazwą i adresem firmy`), + Prześlij jeden z poniższych plików: + • Aktualny rachunek za media z nazwą i adresem firmy + • Wyciąg bankowy z nazwą i adresem firmy + • Aktualna umowa najmu z podpisaną stroną zawierającą nazwę i adres firmy + • Dokument ubezpieczeniowy z nazwą i adresem firmy + • Dokument przydziału TIN z nazwą i adresem firmy + `), userAddressVerification: 'Weryfikacja adresu', userAddressVerificationDescription: dedent(` - Prześlij jeden z poniższych plików: - • Karta rejestracji wyborcy - • Prawo jazdy - • Wyciąg bankowy - • Rachunek za media`), + Prześlij jeden z poniższych plików: + • Karta rejestracji wyborcy + • Prawo jazdy + • Wyciąg bankowy + • Rachunek za media + `), userDOBVerification: 'Weryfikacja daty urodzenia', userDOBVerificationDescription: 'Prześlij dokument tożsamości wydany w USA', finishViaChat: 'Zakończ przez czat', @@ -8223,6 +8226,8 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, [CONST.SEARCH.ACTION_FILTERS.PAY]: 'Zapłać', [CONST.SEARCH.ACTION_FILTERS.EXPORT]: 'Eksportuj', }, + firstApprover: 'Pierwszy zatwierdzający', + firstApproved: 'Najpierw zatwierdzono', }, display: { label: 'Wyświetl', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index f8942f7398f1..3e7bede50949 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -4110,27 +4110,30 @@ ${amount} para ${merchant} - ${date}`, verificationFailed: 'A verificação falhou, então precisaremos de documentos adicionais para verificar você e sua empresa', taxIDVerification: 'Verificação de ID fiscal', taxIDVerificationDescription: dedent(` - Envie um dos seguintes arquivos: - • Carta de atribuição de TIN/EIN do IRS - • Confirmação de solicitação de TIN/EIN do IRS (normalmente contém "Congratulations! The EIN has been successfully assigned") - • Carta de isenção fiscal do IRS com o nome da empresa e o EIN`), + Envie um dos seguintes arquivos: + • Carta de atribuição de TIN/EIN do IRS + • Confirmação de solicitação de TIN/EIN do IRS (normalmente contém "Congratulations! The EIN has been successfully assigned") + • Carta de isenção fiscal do IRS com o nome da empresa e o EIN + `), nameChangeDocument: 'Documento de alteração de nome', nameChangeDocumentDescription: 'Se o nome da sua empresa mudou desde a solicitação do TIN/EIN, precisamos deste documento para verificar o número de identificação fiscal informado', companyAddressVerification: 'Verificação de endereço da empresa', companyAddressVerificationDescription: dedent(` - Envie um dos seguintes arquivos: - • Conta recente de serviços públicos com nome e endereço da empresa - • Extrato bancário com nome e endereço da empresa - • Contrato de locação atual incluindo a página de assinatura com nome e endereço atual da empresa - • Apólice ou declaração de seguro com nome e endereço da empresa - • Documento de atribuição de TIN com nome e endereço da empresa`), + Envie um dos seguintes arquivos: + • Conta recente de serviços públicos com nome e endereço da empresa + • Extrato bancário com nome e endereço da empresa + • Contrato de locação atual incluindo a página de assinatura com nome e endereço atual da empresa + • Apólice ou declaração de seguro com nome e endereço da empresa + • Documento de atribuição de TIN com nome e endereço da empresa + `), userAddressVerification: 'Verificação de endereço', userAddressVerificationDescription: dedent(` - Envie um dos seguintes arquivos: - • Título de eleitor - • Carteira de motorista - • Extrato bancário - • Conta de serviços públicos`), + Envie um dos seguintes arquivos: + • Título de eleitor + • Carteira de motorista + • Extrato bancário + • Conta de serviços públicos + `), userDOBVerification: 'Verificação de data de nascimento', userDOBVerificationDescription: 'Envie um documento de identidade emitido nos EUA', finishViaChat: 'Finalizar pelo chat', @@ -8225,6 +8228,8 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, [CONST.SEARCH.ACTION_FILTERS.PAY]: 'Pagar', [CONST.SEARCH.ACTION_FILTERS.EXPORT]: 'Exportar', }, + firstApprover: 'Primeira pessoa aprovadora', + firstApproved: 'Primeira aprovada', }, display: { label: 'Exibir', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 2a75e959f486..90abf0dd7901 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -4022,27 +4022,30 @@ ${amount},商户:${merchant} - 日期:${date}`, verificationFailed: '验证失败,因此我们需要额外的文件来验证你及你的企业', taxIDVerification: '税务识别号验证', taxIDVerificationDescription: dedent(` - 请上传以下任一文件: - • IRS TIN/EIN 分配函 - • IRS TIN/EIN 申请确认函(通常包含“Congratulations! The EIN has been successfully assigned”) - • 显示公司名称和 EIN 的 IRS 免税函`), + 请上传以下任一文件: + • IRS TIN/EIN 分配函 + • IRS TIN/EIN 申请确认函(通常包含“Congratulations! The EIN has been successfully assigned”) + • 显示公司名称和 EIN 的 IRS 免税函 + `), nameChangeDocument: '名称变更文件', nameChangeDocumentDescription: '如果你的公司名称在申请 TIN/EIN 后发生更改,我们需要此文件来验证你提供的税务识别号', companyAddressVerification: '公司地址验证', companyAddressVerificationDescription: dedent(` - 请上传以下任一文件: - • 显示公司名称和地址的近期水电账单 - • 显示公司名称和地址的银行对账单 - • 包含签字页的有效租赁协议,显示公司名称和当前地址 - • 显示公司名称和地址的保险声明 - • 显示公司名称和地址的 TIN 分配文件`), + 请上传以下任一文件: + • 显示公司名称和地址的近期水电账单 + • 显示公司名称和地址的银行对账单 + • 包含签字页的有效租赁协议,显示公司名称和当前地址 + • 显示公司名称和地址的保险声明 + • 显示公司名称和地址的 TIN 分配文件 + `), userAddressVerification: '地址验证', userAddressVerificationDescription: dedent(` - 请上传以下任一文件: - • 选民登记卡 - • 驾驶证 - • 银行对账单 - • 水电账单`), + 请上传以下任一文件: + • 选民登记卡 + • 驾驶证 + • 银行对账单 + • 水电账单 + `), userDOBVerification: '出生日期验证', userDOBVerificationDescription: '请上传美国签发的身份证件', finishViaChat: '通过聊天完成', @@ -8007,6 +8010,8 @@ ${reportName} [CONST.SEARCH.ACTION_FILTERS.PAY]: '支付', [CONST.SEARCH.ACTION_FILTERS.EXPORT]: '导出', }, + firstApprover: '首位审批人', + firstApproved: '首次批准', }, display: { label: '显示', From 0fc96ed7f6105bd0fb236869bb0f50d956c92b28 Mon Sep 17 00:00:00 2001 From: Mukher Date: Wed, 10 Jun 2026 16:21:08 +0500 Subject: [PATCH 02/16] added tests --- src/components/Search/SearchTableHeader.tsx | 3 ++ src/components/Search/SortableTableHeader.tsx | 3 ++ src/components/Search/index.tsx | 5 ++- src/libs/SearchUIUtils.ts | 42 +++++++++++++++++++ tests/unit/Search/SearchUIUtilsTest.ts | 6 +++ 5 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/components/Search/SearchTableHeader.tsx b/src/components/Search/SearchTableHeader.tsx index 5840aba57e06..cfbbd99ab2f7 100644 --- a/src/components/Search/SearchTableHeader.tsx +++ b/src/components/Search/SearchTableHeader.tsx @@ -471,6 +471,7 @@ type SearchTableHeaderProps = { shouldShowYear: boolean; shouldShowYearSubmitted?: boolean; shouldShowYearApproved?: boolean; + shouldShowYearFirstApproved?: boolean; shouldShowYearPosted?: boolean; shouldShowYearExported?: boolean; shouldShowYearWithdrawn?: boolean; @@ -496,6 +497,7 @@ function SearchTableHeader({ shouldShowYear, shouldShowYearSubmitted, shouldShowYearApproved, + shouldShowYearFirstApproved, shouldShowYearPosted, shouldShowYearExported, shouldShowYearWithdrawn, @@ -569,6 +571,7 @@ function SearchTableHeader({ dateColumnSize={shouldShowYear ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL} submittedColumnSize={shouldShowYearSubmitted ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL} approvedColumnSize={shouldShowYearApproved ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL} + firstApprovedColumnSize={shouldShowYearFirstApproved ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL} postedColumnSize={shouldShowYearPosted ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL} exportedColumnSize={shouldShowYearExported ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL} withdrawnColumnSize={shouldShowYearWithdrawn ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL} diff --git a/src/components/Search/SortableTableHeader.tsx b/src/components/Search/SortableTableHeader.tsx index d08a2f48a38b..bc9d2b6b6904 100644 --- a/src/components/Search/SortableTableHeader.tsx +++ b/src/components/Search/SortableTableHeader.tsx @@ -28,6 +28,7 @@ type SearchTableHeaderProps = { dateColumnSize: TableColumnSize; submittedColumnSize?: TableColumnSize; approvedColumnSize?: TableColumnSize; + firstApprovedColumnSize?: TableColumnSize; postedColumnSize?: TableColumnSize; exportedColumnSize?: TableColumnSize; withdrawnColumnSize?: TableColumnSize; @@ -48,6 +49,7 @@ function SortableTableHeader({ dateColumnSize, submittedColumnSize, approvedColumnSize, + firstApprovedColumnSize, postedColumnSize, exportedColumnSize, withdrawnColumnSize, @@ -91,6 +93,7 @@ function SortableTableHeader({ isDateColumnWide: dateColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE, isSubmittedColumnWide: submittedColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE, isApprovedColumnWide: approvedColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE, + isFirstApprovedColumnWide: firstApprovedColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE, isPostedColumnWide: postedColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE, isExportedColumnWide: exportedColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE, isTaxAmountColumnWide: taxAmountColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE, diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 2068734773bc..fe1f720e5012 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -1719,6 +1719,7 @@ function Search({ shouldShowYearCreated: false, shouldShowYearSubmitted: false, shouldShowYearApproved: false, + shouldShowYearFirstApproved: false, shouldShowYearPosted: false, shouldShowYearExported: false, shouldShowYearWithdrawn: false, @@ -1820,7 +1821,8 @@ function Search({ ); } - const {shouldShowYearCreated, shouldShowYearSubmitted, shouldShowYearApproved, shouldShowYearPosted, shouldShowYearExported, shouldShowYearWithdrawn} = yearIndicators; + const {shouldShowYearCreated, shouldShowYearSubmitted, shouldShowYearApproved, shouldShowYearFirstApproved, shouldShowYearPosted, shouldShowYearExported, shouldShowYearWithdrawn} = + yearIndicators; const {shouldShowAmountInWideColumn, shouldShowTaxAmountInWideColumn} = amountIndicators; const shouldShowTableHeader = isLargeScreenWidth && !isChat; const tableHeaderVisible = canSelectMultiple || shouldShowTableHeader; @@ -1898,6 +1900,7 @@ function Search({ shouldShowYear={shouldShowYearCreated} shouldShowYearSubmitted={shouldShowYearSubmitted} shouldShowYearApproved={shouldShowYearApproved} + shouldShowYearFirstApproved={shouldShowYearFirstApproved} shouldShowYearPosted={shouldShowYearPosted} shouldShowYearExported={shouldShowYearExported} shouldShowYearWithdrawn={shouldShowYearWithdrawn} diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index f8e02cf7dc68..acb8be3d2eeb 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -406,6 +406,7 @@ const nonSortableColumns = new Set([ CONST.SEARCH.TABLE_COLUMNS.ACTION, CONST.SEARCH.TABLE_COLUMNS.IN, CONST.SEARCH.TABLE_COLUMNS.AVATAR, + CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER, ]); function isValidExpenseStatus(status: unknown): status is ValueOf { @@ -1417,6 +1418,7 @@ type ShouldShowYearResult = { shouldShowYearCreated: boolean; shouldShowYearSubmitted: boolean; shouldShowYearApproved: boolean; + shouldShowYearFirstApproved: boolean; shouldShowYearPosted: boolean; shouldShowYearExported: boolean; shouldShowYearWithdrawn: boolean; @@ -1456,6 +1458,38 @@ function buildLastExportedActionByReportIDMap(data: OnyxTypes.SearchResults['dat return lastExportedActionByReportID; } +/** + * @private + * Builds a map of the earliest approval action (APPROVED/FORWARDED) by report ID — its actor is the report's first approver. + */ +function buildFirstApprovedActionByReportIDMap(data: OnyxTypes.SearchResults['data']): Map { + const firstApprovedActionByReportID = new Map(); + for (const key of Object.keys(data)) { + if (!isReportActionEntry(key)) { + continue; + } + const reportID = key.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ''); + + let firstApprovalAction: OnyxTypes.ReportAction | undefined; + let earliestTime = Infinity; + for (const action of Object.values(data[key])) { + if (action.actionName !== CONST.REPORT.ACTIONS.TYPE.APPROVED && action.actionName !== CONST.REPORT.ACTIONS.TYPE.FORWARDED) { + continue; + } + const currentTime = new Date(action.created).getTime(); + if (currentTime < earliestTime) { + earliestTime = currentTime; + firstApprovalAction = action; + } + } + + if (firstApprovalAction) { + firstApprovedActionByReportID.set(reportID, firstApprovalAction); + } + } + return firstApprovedActionByReportID; +} + /** * Checks if the date of transactions or reports indicate the need to display the year because they are from a past year. * @param data - The search results data (array or object) @@ -1472,6 +1506,7 @@ function shouldShowYear( shouldShowYearCreated: false, shouldShowYearSubmitted: false, shouldShowYearApproved: false, + shouldShowYearFirstApproved: false, shouldShowYearPosted: false, shouldShowYearExported: false, shouldShowYearWithdrawn: false, @@ -1526,6 +1561,7 @@ function shouldShowYear( } const lastExportedActionByReportID = precomputedLastExportedMap ?? buildLastExportedActionByReportIDMap(data); + const firstApprovedActionByReportID = buildFirstApprovedActionByReportIDMap(data); for (const key of Object.keys(data)) { if (!checkOnlyReports && isTransactionEntry(key)) { @@ -1577,6 +1613,11 @@ function shouldShowYear( if (exportedAction?.created && DateUtils.doesDateBelongToAPastYear(exportedAction.created)) { result.shouldShowYearExported = true; } + + const firstApprovedAction = firstApprovedActionByReportID.get(item.reportID); + if (firstApprovedAction?.created && DateUtils.doesDateBelongToAPastYear(firstApprovedAction.created)) { + result.shouldShowYearFirstApproved = true; + } } else if (!result.shouldShowYearWithdrawn && isGroupEntry(key)) { const group = data[key]; if ('debitPosted' in group && group.debitPosted && DateUtils.doesDateBelongToAPastYear(group.debitPosted)) { @@ -1589,6 +1630,7 @@ function shouldShowYear( result.shouldShowYearCreated && result.shouldShowYearSubmitted && result.shouldShowYearApproved && + result.shouldShowYearFirstApproved && result.shouldShowYearPosted && result.shouldShowYearExported && result.shouldShowYearWithdrawn diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index fb522ef13d7f..a1d97f2cc108 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -8466,6 +8466,12 @@ describe('SearchUIUtils', () => { expect(categoryGLCodeHeader?.sortColumnName).toBe(CONST.SEARCH.SORT_BY_COLUMNS.CATEGORY_GL_CODE); }); + test('Should exclude First approver from sort options but keep First approved', () => { + const texts = SearchUIUtils.getSortByOptions([CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER, CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED], translateLocal).map((option) => option.text); + expect(texts).not.toContain(translateLocal('search.filters.firstApprover')); + expect(texts).toContain(translateLocal('search.filters.firstApproved')); + }); + test('Should only show MCC when that column is selected and at least one transaction has a displayable MCC', () => { const baseTransaction = searchResults.data[`transactions_${transactionID}`]; const transactionWithoutMCC = { From 647ad0210eb51513a3ec54bb528a3515a4825d87 Mon Sep 17 00:00:00 2001 From: Mukher Date: Wed, 10 Jun 2026 17:02:14 +0500 Subject: [PATCH 03/16] added tests --- tests/unit/ButtonStyleUtilsTest.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/unit/ButtonStyleUtilsTest.ts b/tests/unit/ButtonStyleUtilsTest.ts index 45bf143a3917..cfe4666ad3a6 100644 --- a/tests/unit/ButtonStyleUtilsTest.ts +++ b/tests/unit/ButtonStyleUtilsTest.ts @@ -2,6 +2,7 @@ import CONST from '@src/CONST'; import type {ThemeStyles} from '@src/styles'; import type {ThemeColors} from '@src/styles/theme/types'; import createStyleUtils from '@src/styles/utils'; +import variables from '@src/styles/variables'; const mockTheme = {} as ThemeColors; @@ -22,7 +23,7 @@ const mockStyles = { buttonOpacityDisabled: {opacity: 0.5}, } as unknown as ThemeStyles; -const {getButtonSizeStyle, getButtonPaddingStyle, getButtonStyleWithIcon, getButtonVariantStyles} = createStyleUtils(mockTheme, mockStyles); +const {getButtonSizeStyle, getButtonPaddingStyle, getButtonStyleWithIcon, getButtonVariantStyles, getReportTableColumnStyles} = createStyleUtils(mockTheme, mockStyles); describe('getButtonSizeStyle', () => { it.each([ @@ -88,3 +89,17 @@ describe('getButtonVariantStyles', () => { }); }); }); + +describe('getReportTableColumnStyles - First approved column width', () => { + it('uses the wide width when isFirstApprovedColumnWide is true (e.g. a past-year date)', () => { + expect(getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED, {isFirstApprovedColumnWide: true})).toEqual({width: variables.w92}); + }); + + it('uses the normal width when isFirstApprovedColumnWide is false', () => { + expect(getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED, {isFirstApprovedColumnWide: false})).toEqual({width: variables.w72}); + }); + + it('defaults to the normal width when the wide flag is omitted', () => { + expect(getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED)).toEqual({width: variables.w72}); + }); +}); From eea14df26b208712e11b4ba032bf4f86399772a1 Mon Sep 17 00:00:00 2001 From: Mukher Date: Wed, 10 Jun 2026 18:17:25 +0500 Subject: [PATCH 04/16] hide empty columns from table --- src/libs/SearchUIUtils.ts | 27 ++++++++++++++++++++++++++ tests/unit/Search/SearchUIUtilsTest.ts | 19 ++++++++++++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index acb8be3d2eeb..87dec9da738d 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1490,6 +1490,27 @@ function buildFirstApprovedActionByReportIDMap(data: OnyxTypes.SearchResults['da return firstApprovedActionByReportID; } +/** + * @private + * Whether any report in the results has an approval action — used to hide the empty First approver/approved columns. + */ +function hasFirstApproverData(data: OnyxTypes.SearchResults['data'] | OnyxTypes.Transaction[]): boolean { + if (Array.isArray(data)) { + return false; + } + for (const key of Object.keys(data)) { + if (!isReportActionEntry(key)) { + continue; + } + for (const action of Object.values(data[key])) { + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED || action.actionName === CONST.REPORT.ACTIONS.TYPE.FORWARDED) { + return true; + } + } + } + return false; +} + /** * Checks if the date of transactions or reports indicate the need to display the year because they are from a past year. * @param data - The search results data (array or object) @@ -5534,6 +5555,12 @@ function getColumnsToShow({ result.push(col); } + // Hide First approver/approved when no report has an approval action — an empty column is just noise. + const firstApproverColumns = new Set([CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER, CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED]); + if (result.some((col) => firstApproverColumns.has(col)) && !hasFirstApproverData(data)) { + return result.filter((col) => !firstApproverColumns.has(col)); + } + return result; } diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index a1d97f2cc108..5eab3b008b23 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -8346,10 +8346,25 @@ describe('SearchUIUtils', () => { ]); }); - test('Should include First approver/First approved columns when selected for expense reports', () => { + test('Should hide First approver/First approved columns when no report has an approval action', () => { const visibleColumns = [CONST.SEARCH.TABLE_COLUMNS.DATE, CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER, CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED, CONST.SEARCH.TABLE_COLUMNS.TOTAL]; - const result = SearchUIUtils.getColumnsToShow({currentAccountID: 1, data: [], visibleColumns, type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT}); + const result = SearchUIUtils.getColumnsToShow({currentAccountID: adminAccountID, data: searchResults.data, visibleColumns, type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT}); + expect(result).not.toContain(CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER); + expect(result).not.toContain(CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED); + expect(result).toContain(CONST.SEARCH.TABLE_COLUMNS.TOTAL); + }); + + test('Should keep First approver/First approved columns when a report has an approval action', () => { + const visibleColumns = [CONST.SEARCH.TABLE_COLUMNS.DATE, CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER, CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED, CONST.SEARCH.TABLE_COLUMNS.TOTAL]; + const data = { + ...searchResults.data, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report1.reportID}`]: { + [reportAction1.reportActionID]: {...reportAction1, actionName: CONST.REPORT.ACTIONS.TYPE.APPROVED}, + }, + }; + + const result = SearchUIUtils.getColumnsToShow({currentAccountID: adminAccountID, data, visibleColumns, type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT}); expect(result).toContain(CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER); expect(result).toContain(CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED); }); From 66750bffcd505eab19f1b135dd2283b47c9dba89 Mon Sep 17 00:00:00 2001 From: Mukher Date: Mon, 15 Jun 2026 10:33:46 +0500 Subject: [PATCH 05/16] scoped first-approver detection to the current approval cycle in both places that derive it: --- src/libs/SearchUIUtils.ts | 46 ++++++++++++++++++-------- tests/unit/Search/SearchUIUtilsTest.ts | 37 +++++++++++++++++++++ 2 files changed, 70 insertions(+), 13 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 87dec9da738d..055bab13f758 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1470,14 +1470,23 @@ function buildFirstApprovedActionByReportIDMap(data: OnyxTypes.SearchResults['da } const reportID = key.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ''); - let firstApprovalAction: OnyxTypes.ReportAction | undefined; - let earliestTime = Infinity; + // A SUBMITTED action starts a new approval cycle, so approvals before the latest one are stale. + let latestSubmittedTime = -Infinity; + const approvalActions: OnyxTypes.ReportAction[] = []; for (const action of Object.values(data[key])) { - if (action.actionName !== CONST.REPORT.ACTIONS.TYPE.APPROVED && action.actionName !== CONST.REPORT.ACTIONS.TYPE.FORWARDED) { - continue; + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) { + latestSubmittedTime = Math.max(latestSubmittedTime, new Date(action.created).getTime()); + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED || action.actionName === CONST.REPORT.ACTIONS.TYPE.FORWARDED) { + approvalActions.push(action); } + } + + // The first approver is the earliest approval in the current cycle (after the latest submit). + let firstApprovalAction: OnyxTypes.ReportAction | undefined; + let earliestTime = Infinity; + for (const action of approvalActions) { const currentTime = new Date(action.created).getTime(); - if (currentTime < earliestTime) { + if (currentTime >= latestSubmittedTime && currentTime < earliestTime) { earliestTime = currentTime; firstApprovalAction = action; } @@ -1860,9 +1869,9 @@ function processReportActionEntry(ctx: PreprocessingContext, key: string, action let latestExportTime = -Infinity; let latestExportAction: OnyxTypes.ReportAction | undefined; - // Earliest approval action; its actor is the report's first approver. - let firstApprovalTime = Infinity; - let firstApprovalAction: OnyxTypes.ReportAction | undefined; + // A SUBMITTED action starts a new approval cycle, so approvals before the latest one are stale. + let latestSubmittedTime = -Infinity; + const approvalActions: OnyxTypes.ReportAction[] = []; for (const action of Object.values(actions)) { if (action.actionName === CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_CSV || action.actionName === CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION) { @@ -1873,12 +1882,12 @@ function processReportActionEntry(ctx: PreprocessingContext, key: string, action } } + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) { + latestSubmittedTime = Math.max(latestSubmittedTime, new Date(action.created).getTime()); + } + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED || action.actionName === CONST.REPORT.ACTIONS.TYPE.FORWARDED) { - const currentTime = new Date(action.created).getTime(); - if (currentTime < firstApprovalTime) { - firstApprovalTime = currentTime; - firstApprovalAction = action; - } + approvalActions.push(action); } if (isMoneyRequestAction(action)) { @@ -1900,6 +1909,17 @@ function processReportActionEntry(ctx: PreprocessingContext, key: string, action ctx.lastExportedActionByReportID.set(reportID, latestExportAction); } + // The first approver is the earliest approval in the current cycle (after the latest submit). + let firstApprovalTime = Infinity; + let firstApprovalAction: OnyxTypes.ReportAction | undefined; + for (const action of approvalActions) { + const currentTime = new Date(action.created).getTime(); + if (currentTime >= latestSubmittedTime && currentTime < firstApprovalTime) { + firstApprovalTime = currentTime; + firstApprovalAction = action; + } + } + if (firstApprovalAction) { ctx.firstApprovedActionByReportID.set(reportID, firstApprovalAction); if (!ctx.shouldShowYearFirstApprovedReport && DateUtils.doesDateBelongToAPastYear(firstApprovalAction.created)) { diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 5eab3b008b23..8b7152ac23a5 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -5963,6 +5963,43 @@ describe('SearchUIUtils', () => { expect(item?.firstApproverAccountID).toBeUndefined(); expect(item?.formattedFirstApprover).toBe(''); }); + + it('should ignore a prior approval cycle and use the approval after the latest submit', () => { + const staleApprovedAt = '2024-12-10 08:00:00'; + const resubmittedAt = '2024-12-15 10:00:00'; + const currentApprovedAt = '2024-12-22 09:30:00'; + const data = makeReportFilterTestData( + {type: CONST.REPORT.TYPE.EXPENSE}, + {}, + { + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${rptFilterReportID}`]: { + // Prior cycle: approved, then unapproved/resubmitted, so this approval is stale. + 'approved-stale': { + reportActionID: 'approved-stale', + actionName: CONST.REPORT.ACTIONS.TYPE.APPROVED, + actorAccountID: adminAccountID, + created: staleApprovedAt, + }, + 'submitted-1': { + reportActionID: 'submitted-1', + actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED, + actorAccountID: adminAccountID, + created: resubmittedAt, + }, + 'approved-current': { + reportActionID: 'approved-current', + actionName: CONST.REPORT.ACTIONS.TYPE.APPROVED, + actorAccountID: approverAccountID, + created: currentApprovedAt, + }, + }, + }, + ); + const [sections] = callGetReportSections(data); + const item = sections.find((s) => s.keyForList === rptFilterReportID); + expect(item?.firstApproved).toBe(currentApprovedAt); + expect(item?.firstApproverAccountID).toBe(approverAccountID); + }); }); }); From 4d6bec607c8f82f76c4c07d8c9842097cff770a4 Mon Sep 17 00:00:00 2001 From: Mukher Date: Mon, 15 Jun 2026 11:07:42 +0500 Subject: [PATCH 06/16] removed FIRST_APPROVED sortable feature --- src/components/Search/SearchTableHeader.tsx | 1 + src/libs/SearchUIUtils.ts | 4 +++- tests/unit/Search/SearchUIUtilsTest.ts | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchTableHeader.tsx b/src/components/Search/SearchTableHeader.tsx index cfbbd99ab2f7..385bbfb1bb88 100644 --- a/src/components/Search/SearchTableHeader.tsx +++ b/src/components/Search/SearchTableHeader.tsx @@ -244,6 +244,7 @@ const getExpenseReportHeaders = (profileIcon?: IconAsset): SearchColumnConfig[] { columnName: CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED, translationKey: 'search.filters.firstApproved', + isColumnSortable: false, }, { columnName: CONST.SEARCH.TABLE_COLUMNS.EXPORTED, diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 055bab13f758..a1d342964a04 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -305,7 +305,6 @@ const expenseReportColumnNamesToSortingProperty: ExpenseReportSorting = { [CONST.SEARCH.TABLE_COLUMNS.DATE]: 'created' as const, [CONST.SEARCH.TABLE_COLUMNS.SUBMITTED]: 'submitted' as const, [CONST.SEARCH.TABLE_COLUMNS.APPROVED]: 'approved' as const, - [CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED]: 'firstApproved' as const, [CONST.SEARCH.TABLE_COLUMNS.EXPORTED]: 'exported' as const, [CONST.SEARCH.TABLE_COLUMNS.STATUS]: 'formattedStatus' as const, [CONST.SEARCH.TABLE_COLUMNS.TITLE]: 'reportName' as const, @@ -406,7 +405,10 @@ const nonSortableColumns = new Set([ CONST.SEARCH.TABLE_COLUMNS.ACTION, CONST.SEARCH.TABLE_COLUMNS.IN, CONST.SEARCH.TABLE_COLUMNS.AVATAR, + // First approver/First approved are derived from report actions, not BE-indexed report fields, + // so the Search backend rejects them as sortBy values (INVALID_SEARCH_QUERY). CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER, + CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED, ]); function isValidExpenseStatus(status: unknown): status is ValueOf { diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 8b7152ac23a5..d70ee328eef2 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -8518,10 +8518,10 @@ describe('SearchUIUtils', () => { expect(categoryGLCodeHeader?.sortColumnName).toBe(CONST.SEARCH.SORT_BY_COLUMNS.CATEGORY_GL_CODE); }); - test('Should exclude First approver from sort options but keep First approved', () => { + test('Should exclude First approver and First approved from sort options (BE does not support sorting these derived columns)', () => { const texts = SearchUIUtils.getSortByOptions([CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER, CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED], translateLocal).map((option) => option.text); expect(texts).not.toContain(translateLocal('search.filters.firstApprover')); - expect(texts).toContain(translateLocal('search.filters.firstApproved')); + expect(texts).not.toContain(translateLocal('search.filters.firstApproved')); }); test('Should only show MCC when that column is selected and at least one transaction has a displayable MCC', () => { From b668b448bedb7cfdb2ed0c6f5e093a584136e5ec Mon Sep 17 00:00:00 2001 From: Mukher Date: Mon, 15 Jun 2026 11:19:43 +0500 Subject: [PATCH 07/16] Only the avatar was used from that object (accountID and display name are already separate fields), --- .../ExpenseReportListItemRowWide.tsx | 2 +- src/components/Search/SearchList/ListItem/types.ts | 5 +++-- src/libs/SearchUIUtils.ts | 2 +- tests/unit/Search/SearchUIUtilsTest.ts | 8 ++++---- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx index ef0af803a62b..42f991d0ced4 100644 --- a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx +++ b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx @@ -89,7 +89,7 @@ function ExpenseReportListItemRowWide({ {!!item.firstApproverAccountID && ( diff --git a/src/components/Search/SearchList/ListItem/types.ts b/src/components/Search/SearchList/ListItem/types.ts index 9aaf9a95a225..da1ad2ba9091 100644 --- a/src/components/Search/SearchList/ListItem/types.ts +++ b/src/components/Search/SearchList/ListItem/types.ts @@ -3,6 +3,7 @@ import type {ValueOf} from 'type-fest'; import type {SearchColumnType, SearchGroupBy, SearchQueryJSON} from '@components/Search/types'; import type {ListItemProps} from '@components/SelectionList/ListItem/types'; import type {ListItem} from '@components/SelectionList/types'; +import type {AvatarSource} from '@libs/UserAvatarUtils'; import type CONST from '@src/CONST'; import type { BillingGraceEndPeriod, @@ -237,8 +238,8 @@ type TransactionReportGroupListItemType = TransactionGroupListItemType & {groupe /** The date of the report's first approval (created date of the earliest APPROVED/FORWARDED report action) */ firstApproved?: string; - /** The personal details of the first approver */ - firstApprover?: PersonalDetails; + /** The avatar of the first approver */ + firstApproverAvatar?: AvatarSource; /** Account ID of the first approver (actor on the earliest APPROVED/FORWARDED report action) */ firstApproverAccountID?: number; diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index a1d342964a04..400b5d88196f 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -2983,7 +2983,7 @@ function getReportSections({ to: (toDetails ?? emptyPersonalDetails) as OnyxTypes.PersonalDetails, exported: lastExportedActionByReportID.get(reportItem.reportID)?.created ?? '', firstApproved, - firstApprover: (firstApproverDetails ?? emptyPersonalDetails) as OnyxTypes.PersonalDetails, + firstApproverAvatar: firstApproverDetails?.avatar, firstApproverAccountID, formattedFirstApprover, formattedFrom, diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index d70ee328eef2..e1802b07f57b 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -1145,7 +1145,7 @@ const transactionReportGroupListItems = [ shouldShowYearExported: false, shouldShowYearFirstApproved: false, firstApproved: '', - firstApprover: emptyPersonalDetails, + firstApproverAvatar: '', firstApproverAccountID: undefined, formattedFirstApprover: '', stateNum: 0, @@ -1270,7 +1270,7 @@ const transactionReportGroupListItems = [ shouldShowYearExported: false, shouldShowYearFirstApproved: false, firstApproved: '', - firstApprover: emptyPersonalDetails, + firstApproverAvatar: '', firstApproverAccountID: undefined, formattedFirstApprover: '', stateNum: 1, @@ -1401,7 +1401,7 @@ const transactionReportGroupListItems = [ shouldShowYearExported: false, shouldShowYearFirstApproved: false, firstApproved: '', - firstApprover: emptyPersonalDetails, + firstApproverAvatar: '', firstApproverAccountID: undefined, formattedFirstApprover: '', stateNum: 1, @@ -1613,7 +1613,7 @@ const transactionReportGroupListItems = [ shouldShowYearExported: false, shouldShowYearFirstApproved: false, firstApproved: '', - firstApprover: emptyPersonalDetails, + firstApproverAvatar: '', firstApproverAccountID: undefined, formattedFirstApprover: '', stateNum: 0, From e9c62bd5587cd09729d554767d6726bf47888cc3 Mon Sep 17 00:00:00 2001 From: Mukher Date: Mon, 15 Jun 2026 11:30:03 +0500 Subject: [PATCH 08/16] DRY shouldShowYear for approve related columns --- src/libs/SearchUIUtils.ts | 75 ++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 45 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 400b5d88196f..80017af1da72 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1460,6 +1460,34 @@ function buildLastExportedActionByReportIDMap(data: OnyxTypes.SearchResults['dat return lastExportedActionByReportID; } +/** + * @private + * Returns the report's first approval action of the CURRENT cycle — the earliest APPROVED/FORWARDED after the + * latest SUBMITTED (a SUBMITTED starts a new cycle, so older approvals are stale). Its actor is the first approver. + */ +function getFirstApprovalActionInCurrentCycle(actions: Record): OnyxTypes.ReportAction | undefined { + let latestSubmittedTime = -Infinity; + const approvalActions: OnyxTypes.ReportAction[] = []; + for (const action of Object.values(actions)) { + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) { + latestSubmittedTime = Math.max(latestSubmittedTime, new Date(action.created).getTime()); + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED || action.actionName === CONST.REPORT.ACTIONS.TYPE.FORWARDED) { + approvalActions.push(action); + } + } + + let firstApprovalAction: OnyxTypes.ReportAction | undefined; + let earliestTime = Infinity; + for (const action of approvalActions) { + const currentTime = new Date(action.created).getTime(); + if (currentTime >= latestSubmittedTime && currentTime < earliestTime) { + earliestTime = currentTime; + firstApprovalAction = action; + } + } + return firstApprovalAction; +} + /** * @private * Builds a map of the earliest approval action (APPROVED/FORWARDED) by report ID — its actor is the report's first approver. @@ -1471,28 +1499,7 @@ function buildFirstApprovedActionByReportIDMap(data: OnyxTypes.SearchResults['da continue; } const reportID = key.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ''); - - // A SUBMITTED action starts a new approval cycle, so approvals before the latest one are stale. - let latestSubmittedTime = -Infinity; - const approvalActions: OnyxTypes.ReportAction[] = []; - for (const action of Object.values(data[key])) { - if (action.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) { - latestSubmittedTime = Math.max(latestSubmittedTime, new Date(action.created).getTime()); - } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED || action.actionName === CONST.REPORT.ACTIONS.TYPE.FORWARDED) { - approvalActions.push(action); - } - } - - // The first approver is the earliest approval in the current cycle (after the latest submit). - let firstApprovalAction: OnyxTypes.ReportAction | undefined; - let earliestTime = Infinity; - for (const action of approvalActions) { - const currentTime = new Date(action.created).getTime(); - if (currentTime >= latestSubmittedTime && currentTime < earliestTime) { - earliestTime = currentTime; - firstApprovalAction = action; - } - } + const firstApprovalAction = getFirstApprovalActionInCurrentCycle(data[key]); if (firstApprovalAction) { firstApprovedActionByReportID.set(reportID, firstApprovalAction); @@ -1871,10 +1878,6 @@ function processReportActionEntry(ctx: PreprocessingContext, key: string, action let latestExportTime = -Infinity; let latestExportAction: OnyxTypes.ReportAction | undefined; - // A SUBMITTED action starts a new approval cycle, so approvals before the latest one are stale. - let latestSubmittedTime = -Infinity; - const approvalActions: OnyxTypes.ReportAction[] = []; - for (const action of Object.values(actions)) { if (action.actionName === CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_CSV || action.actionName === CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION) { const currentTime = new Date(action.created).getTime(); @@ -1884,14 +1887,6 @@ function processReportActionEntry(ctx: PreprocessingContext, key: string, action } } - if (action.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) { - latestSubmittedTime = Math.max(latestSubmittedTime, new Date(action.created).getTime()); - } - - if (action.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED || action.actionName === CONST.REPORT.ACTIONS.TYPE.FORWARDED) { - approvalActions.push(action); - } - if (isMoneyRequestAction(action)) { const originalMessage = getOriginalMessage(action); const transactionID = originalMessage?.IOUTransactionID; @@ -1911,17 +1906,7 @@ function processReportActionEntry(ctx: PreprocessingContext, key: string, action ctx.lastExportedActionByReportID.set(reportID, latestExportAction); } - // The first approver is the earliest approval in the current cycle (after the latest submit). - let firstApprovalTime = Infinity; - let firstApprovalAction: OnyxTypes.ReportAction | undefined; - for (const action of approvalActions) { - const currentTime = new Date(action.created).getTime(); - if (currentTime >= latestSubmittedTime && currentTime < firstApprovalTime) { - firstApprovalTime = currentTime; - firstApprovalAction = action; - } - } - + const firstApprovalAction = getFirstApprovalActionInCurrentCycle(actions); if (firstApprovalAction) { ctx.firstApprovedActionByReportID.set(reportID, firstApprovalAction); if (!ctx.shouldShowYearFirstApprovedReport && DateUtils.doesDateBelongToAPastYear(firstApprovalAction.created)) { From c2551b177d269f389091e851a293e7215cdb193b Mon Sep 17 00:00:00 2001 From: Mukher Date: Mon, 15 Jun 2026 11:40:43 +0500 Subject: [PATCH 09/16] firstApproverDetails now defaults to undefined. --- src/libs/SearchUIUtils.ts | 2 +- tests/unit/Search/SearchUIUtilsTest.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 80017af1da72..3885c2600e5e 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -2921,7 +2921,7 @@ function getReportSections({ // First approver/approved come from the earliest APPROVED/FORWARDED report action; blank when the report has no approval. const firstApprovedAction = firstApprovedActionByReportID.get(reportItem.reportID); const firstApproverAccountID = firstApprovedAction?.actorAccountID; - const firstApproverDetails = firstApproverAccountID ? (mergedPersonalDetails?.[firstApproverAccountID] ?? emptyPersonalDetails) : emptyPersonalDetails; + const firstApproverDetails = firstApproverAccountID ? mergedPersonalDetails?.[firstApproverAccountID] : undefined; const firstApproved = firstApprovedAction?.created ?? ''; const formattedFrom = formatPhoneNumber(getDisplayNameOrDefault(fromDetails)); diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index e1802b07f57b..29bd1afe8ad4 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -1145,7 +1145,7 @@ const transactionReportGroupListItems = [ shouldShowYearExported: false, shouldShowYearFirstApproved: false, firstApproved: '', - firstApproverAvatar: '', + firstApproverAvatar: undefined, firstApproverAccountID: undefined, formattedFirstApprover: '', stateNum: 0, @@ -1270,7 +1270,7 @@ const transactionReportGroupListItems = [ shouldShowYearExported: false, shouldShowYearFirstApproved: false, firstApproved: '', - firstApproverAvatar: '', + firstApproverAvatar: undefined, firstApproverAccountID: undefined, formattedFirstApprover: '', stateNum: 1, @@ -1401,7 +1401,7 @@ const transactionReportGroupListItems = [ shouldShowYearExported: false, shouldShowYearFirstApproved: false, firstApproved: '', - firstApproverAvatar: '', + firstApproverAvatar: undefined, firstApproverAccountID: undefined, formattedFirstApprover: '', stateNum: 1, @@ -1613,7 +1613,7 @@ const transactionReportGroupListItems = [ shouldShowYearExported: false, shouldShowYearFirstApproved: false, firstApproved: '', - firstApproverAvatar: '', + firstApproverAvatar: undefined, firstApproverAccountID: undefined, formattedFirstApprover: '', stateNum: 0, From 83de3965b1c7814229692dc37c56b573deb19d58 Mon Sep 17 00:00:00 2001 From: Mukher Date: Mon, 15 Jun 2026 11:50:26 +0500 Subject: [PATCH 10/16] fixed prettier --- src/components/Search/SearchList/ListItem/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Search/SearchList/ListItem/types.ts b/src/components/Search/SearchList/ListItem/types.ts index d1a2db444974..8f727b0cb5f8 100644 --- a/src/components/Search/SearchList/ListItem/types.ts +++ b/src/components/Search/SearchList/ListItem/types.ts @@ -3,9 +3,9 @@ import type {ValueOf} from 'type-fest'; import type {SearchColumnType, SearchGroupBy, SearchQueryJSON} from '@components/Search/types'; import type {ListItemProps} from '@components/SelectionList/ListItem/types'; import type {ListItem} from '@components/SelectionList/types'; -import type {AvatarSource} from '@libs/UserAvatarUtils'; import type {TransactionPreviewData} from '@libs/actions/Search'; import type {ModifiedMouseEvent} from '@libs/Navigation/helpers/openInternalRouteInNewTab'; +import type {AvatarSource} from '@libs/UserAvatarUtils'; import type CONST from '@src/CONST'; import type { BankAccountList, From aafd7823f2a5ded4b631f320cf01d80d4bb604df Mon Sep 17 00:00:00 2001 From: Mukher Date: Mon, 15 Jun 2026 12:32:39 +0500 Subject: [PATCH 11/16] removed unnecessary shouldShowYearFirstApproved --- src/libs/SearchUIUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 4ca9d756f0ae..d572df3c3928 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1673,7 +1673,6 @@ function shouldShowYear( result.shouldShowYearCreated && result.shouldShowYearSubmitted && result.shouldShowYearApproved && - result.shouldShowYearFirstApproved && result.shouldShowYearPosted && result.shouldShowYearExported && result.shouldShowYearWithdrawn From 6235691d3d436d0f19ddee31f30afa166b431355 Mon Sep 17 00:00:00 2001 From: Mukher Date: Mon, 15 Jun 2026 15:28:38 +0500 Subject: [PATCH 12/16] removed shouldShowYearFirstApproved. made first approved have the static width --- .../ExpenseReportListItemRowWide.tsx | 2 +- .../Search/SearchList/ListItem/types.ts | 6 ---- src/components/Search/SearchTableHeader.tsx | 3 -- src/components/Search/SortableTableHeader.tsx | 3 -- src/components/Search/index.tsx | 5 +-- src/libs/SearchUIUtils.ts | 35 ------------------- src/styles/utils/index.ts | 13 ++++--- tests/unit/ButtonStyleUtilsTest.ts | 12 ++----- tests/unit/Search/SearchUIUtilsTest.ts | 4 --- 9 files changed, 10 insertions(+), 73 deletions(-) diff --git a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx index 35ba571d57b9..82e71e08911d 100644 --- a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx +++ b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx @@ -99,7 +99,7 @@ function ExpenseReportListItemRowWide({ ), [CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED]: ( - + { - const firstApprovedActionByReportID = new Map(); - for (const key of Object.keys(data)) { - if (!isReportActionEntry(key)) { - continue; - } - const reportID = key.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ''); - const firstApprovalAction = getFirstApprovalActionInCurrentCycle(data[key]); - - if (firstApprovalAction) { - firstApprovedActionByReportID.set(reportID, firstApprovalAction); - } - } - return firstApprovedActionByReportID; -} - /** * @private * Whether any report in the results has an approval action — used to hide the empty First approver/approved columns. @@ -1549,7 +1528,6 @@ function shouldShowYear( shouldShowYearCreated: false, shouldShowYearSubmitted: false, shouldShowYearApproved: false, - shouldShowYearFirstApproved: false, shouldShowYearPosted: false, shouldShowYearExported: false, shouldShowYearWithdrawn: false, @@ -1604,7 +1582,6 @@ function shouldShowYear( } const lastExportedActionByReportID = precomputedLastExportedMap ?? buildLastExportedActionByReportIDMap(data); - const firstApprovedActionByReportID = buildFirstApprovedActionByReportIDMap(data); for (const key of Object.keys(data)) { if (!checkOnlyReports && isTransactionEntry(key)) { @@ -1656,11 +1633,6 @@ function shouldShowYear( if (exportedAction?.created && DateUtils.doesDateBelongToAPastYear(exportedAction.created)) { result.shouldShowYearExported = true; } - - const firstApprovedAction = firstApprovedActionByReportID.get(item.reportID); - if (firstApprovedAction?.created && DateUtils.doesDateBelongToAPastYear(firstApprovedAction.created)) { - result.shouldShowYearFirstApproved = true; - } } else if (!result.shouldShowYearWithdrawn && isGroupEntry(key)) { const group = data[key]; if ('debitPosted' in group && group.debitPosted && DateUtils.doesDateBelongToAPastYear(group.debitPosted)) { @@ -1834,7 +1806,6 @@ type PreprocessingContext = { shouldShowYearCreatedReport: boolean; shouldShowYearSubmittedReport: boolean; shouldShowYearApprovedReport: boolean; - shouldShowYearFirstApprovedReport: boolean; shouldShowYearExportedReport: boolean; shouldShowAmountInWideColumn: boolean; shouldShowTaxAmountInWideColumn: boolean; @@ -1862,7 +1833,6 @@ function createPreprocessingContext(): PreprocessingContext { shouldShowYearCreatedReport: false, shouldShowYearSubmittedReport: false, shouldShowYearApprovedReport: false, - shouldShowYearFirstApprovedReport: false, shouldShowYearExportedReport: false, shouldShowAmountInWideColumn: false, shouldShowTaxAmountInWideColumn: false, @@ -1912,9 +1882,6 @@ function processReportActionEntry(ctx: PreprocessingContext, key: string, action const firstApprovalAction = getFirstApprovalActionInCurrentCycle(actions); if (firstApprovalAction) { ctx.firstApprovedActionByReportID.set(reportID, firstApprovalAction); - if (!ctx.shouldShowYearFirstApprovedReport && DateUtils.doesDateBelongToAPastYear(firstApprovalAction.created)) { - ctx.shouldShowYearFirstApprovedReport = true; - } } } @@ -2865,7 +2832,6 @@ function getReportSections({ shouldShowYearCreatedReport, shouldShowYearSubmittedReport, shouldShowYearApprovedReport, - shouldShowYearFirstApprovedReport, shouldShowYearExportedReport, shouldShowAmountInWideColumn, shouldShowTaxAmountInWideColumn, @@ -2983,7 +2949,6 @@ function getReportSections({ shouldShowYear: shouldShowYearCreatedReport, shouldShowYearSubmitted: shouldShowYearSubmittedReport, shouldShowYearApproved: shouldShowYearApprovedReport, - shouldShowYearFirstApproved: shouldShowYearFirstApprovedReport, shouldShowYearExported: shouldShowYearExportedReport, hasVisibleViolations: hasVisibleViolationsForReport, isRejectedReport, diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 41f1330f525c..2ec0f7a950b8 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -58,7 +58,6 @@ type GetReportTableColumnStylesParams = { isTaxAmountColumnWide?: boolean; isSubmittedColumnWide?: boolean; isApprovedColumnWide?: boolean; - isFirstApprovedColumnWide?: boolean; isPostedColumnWide?: boolean; isExportedColumnWide?: boolean; shouldRemoveTotalColumnFlex?: boolean; @@ -1871,7 +1870,6 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ const { isSubmittedColumnWide, isApprovedColumnWide, - isFirstApprovedColumnWide, isPostedColumnWide, isExportedColumnWide, isDateColumnWide, @@ -1900,19 +1898,20 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ columnWidth = {...getWidthStyle(variables.w130), ...styles.alignItemsCenter}; break; case CONST.SEARCH.TABLE_COLUMNS.SUBMITTED: - columnWidth = {...getWidthStyle(isSubmittedColumnWide ? variables.w92 : variables.w72)}; + columnWidth = {...getWidthStyle(isSubmittedColumnWide ? variables.w102 : variables.w62)}; break; case CONST.SEARCH.TABLE_COLUMNS.APPROVED: - columnWidth = {...getWidthStyle(isApprovedColumnWide ? variables.w92 : variables.w72)}; + columnWidth = {...getWidthStyle(isApprovedColumnWide ? variables.w102 : variables.w62)}; break; case CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED: - columnWidth = {...getWidthStyle(isFirstApprovedColumnWide ? variables.w92 : variables.w72)}; + // Fixed width: wide enough for both the long "First approved" header and a past-year date, so no year-based widening is needed. + columnWidth = {...getWidthStyle(variables.w102)}; break; case CONST.SEARCH.TABLE_COLUMNS.POSTED: - columnWidth = {...getWidthStyle(isPostedColumnWide ? variables.w92 : variables.w72)}; + columnWidth = {...getWidthStyle(isPostedColumnWide ? variables.w102 : variables.w62)}; break; case CONST.SEARCH.TABLE_COLUMNS.EXPORTED: - columnWidth = {...getWidthStyle(isExportedColumnWide ? variables.w92 : variables.w72)}; + columnWidth = {...getWidthStyle(isExportedColumnWide ? variables.w102 : variables.w62)}; break; case CONST.SEARCH.TABLE_COLUMNS.DATE: columnWidth = {...getWidthStyle(isDateColumnWide ? variables.w102 : variables.w62)}; diff --git a/tests/unit/ButtonStyleUtilsTest.ts b/tests/unit/ButtonStyleUtilsTest.ts index cfe4666ad3a6..8a29cbed0990 100644 --- a/tests/unit/ButtonStyleUtilsTest.ts +++ b/tests/unit/ButtonStyleUtilsTest.ts @@ -91,15 +91,7 @@ describe('getButtonVariantStyles', () => { }); describe('getReportTableColumnStyles - First approved column width', () => { - it('uses the wide width when isFirstApprovedColumnWide is true (e.g. a past-year date)', () => { - expect(getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED, {isFirstApprovedColumnWide: true})).toEqual({width: variables.w92}); - }); - - it('uses the normal width when isFirstApprovedColumnWide is false', () => { - expect(getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED, {isFirstApprovedColumnWide: false})).toEqual({width: variables.w72}); - }); - - it('defaults to the normal width when the wide flag is omitted', () => { - expect(getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED)).toEqual({width: variables.w72}); + it('uses a fixed wide width (fits the long header and a past-year date, so no year-based widening)', () => { + expect(getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED)).toEqual({width: variables.w102}); }); }); diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 4dfd223777bd..8c695ea6058f 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -1143,7 +1143,6 @@ const transactionReportGroupListItems = [ shouldShowYearSubmitted: true, shouldShowYearApproved: false, shouldShowYearExported: false, - shouldShowYearFirstApproved: false, firstApproved: '', firstApproverAvatar: undefined, firstApproverAccountID: undefined, @@ -1268,7 +1267,6 @@ const transactionReportGroupListItems = [ shouldShowYearSubmitted: true, shouldShowYearApproved: false, shouldShowYearExported: false, - shouldShowYearFirstApproved: false, firstApproved: '', firstApproverAvatar: undefined, firstApproverAccountID: undefined, @@ -1399,7 +1397,6 @@ const transactionReportGroupListItems = [ shouldShowYearSubmitted: true, shouldShowYearApproved: false, shouldShowYearExported: false, - shouldShowYearFirstApproved: false, firstApproved: '', firstApproverAvatar: undefined, firstApproverAccountID: undefined, @@ -1611,7 +1608,6 @@ const transactionReportGroupListItems = [ shouldShowYearSubmitted: true, shouldShowYearApproved: false, shouldShowYearExported: false, - shouldShowYearFirstApproved: false, firstApproved: '', firstApproverAvatar: undefined, firstApproverAccountID: undefined, From f6c662961b9c330cd11ff03f2bc88cb5279b3576 Mon Sep 17 00:00:00 2001 From: Mukher Date: Mon, 15 Jun 2026 15:44:13 +0500 Subject: [PATCH 13/16] added first approver and first approved to sorting list of sorting cols --- src/components/Search/SearchTableHeader.tsx | 2 -- src/libs/SearchUIUtils.ts | 6 ++---- tests/unit/Search/SearchUIUtilsTest.ts | 6 +++--- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/components/Search/SearchTableHeader.tsx b/src/components/Search/SearchTableHeader.tsx index 9ae8d5406af7..1e66d0bb04f7 100644 --- a/src/components/Search/SearchTableHeader.tsx +++ b/src/components/Search/SearchTableHeader.tsx @@ -239,12 +239,10 @@ const getExpenseReportHeaders = (profileIcon?: IconAsset): SearchColumnConfig[] { columnName: CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER, translationKey: 'search.filters.firstApprover', - isColumnSortable: false, }, { columnName: CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED, translationKey: 'search.filters.firstApproved', - isColumnSortable: false, }, { columnName: CONST.SEARCH.TABLE_COLUMNS.EXPORTED, diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index b669a18b5630..8528c9b5538b 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -308,6 +308,8 @@ const expenseReportColumnNamesToSortingProperty: ExpenseReportSorting = { [CONST.SEARCH.TABLE_COLUMNS.DATE]: 'created' as const, [CONST.SEARCH.TABLE_COLUMNS.SUBMITTED]: 'submitted' as const, [CONST.SEARCH.TABLE_COLUMNS.APPROVED]: 'approved' as const, + [CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER]: 'formattedFirstApprover' as const, + [CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED]: 'firstApproved' as const, [CONST.SEARCH.TABLE_COLUMNS.EXPORTED]: 'exported' as const, [CONST.SEARCH.TABLE_COLUMNS.STATUS]: 'formattedStatus' as const, [CONST.SEARCH.TABLE_COLUMNS.TITLE]: 'reportName' as const, @@ -408,10 +410,6 @@ const nonSortableColumns = new Set([ CONST.SEARCH.TABLE_COLUMNS.ACTION, CONST.SEARCH.TABLE_COLUMNS.IN, CONST.SEARCH.TABLE_COLUMNS.AVATAR, - // First approver/First approved are derived from report actions, not BE-indexed report fields, - // so the Search backend rejects them as sortBy values (INVALID_SEARCH_QUERY). - CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER, - CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED, ]); function isValidExpenseStatus(status: unknown): status is ValueOf { diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 8c695ea6058f..1831e15629c8 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -8616,10 +8616,10 @@ describe('SearchUIUtils', () => { expect(categoryGLCodeHeader?.sortColumnName).toBe(CONST.SEARCH.SORT_BY_COLUMNS.CATEGORY_GL_CODE); }); - test('Should exclude First approver and First approved from sort options (BE does not support sorting these derived columns)', () => { + test('Should include First approver and First approved in sort options', () => { const texts = SearchUIUtils.getSortByOptions([CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER, CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED], translateLocal).map((option) => option.text); - expect(texts).not.toContain(translateLocal('search.filters.firstApprover')); - expect(texts).not.toContain(translateLocal('search.filters.firstApproved')); + expect(texts).toContain(translateLocal('search.filters.firstApprover')); + expect(texts).toContain(translateLocal('search.filters.firstApproved')); }); test('Should only show MCC when that column is selected and at least one transaction has a displayable MCC', () => { From 5c494a6b5b69c1d1d9d25a7b14e5ca687050491a Mon Sep 17 00:00:00 2001 From: Mukher Date: Thu, 25 Jun 2026 15:24:11 +0500 Subject: [PATCH 14/16] fixed tests --- tests/unit/Search/ColumnAvailabilityTest.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/Search/ColumnAvailabilityTest.ts b/tests/unit/Search/ColumnAvailabilityTest.ts index 5927f9cf896f..a9e142d60353 100644 --- a/tests/unit/Search/ColumnAvailabilityTest.ts +++ b/tests/unit/Search/ColumnAvailabilityTest.ts @@ -30,6 +30,8 @@ describe('Column availability single source of truth', () => { // Expense-report (report list) view. TABLE_COLUMNS.REIMBURSABLE_TOTAL, TABLE_COLUMNS.NON_REIMBURSABLE_TOTAL, + TABLE_COLUMNS.FIRST_APPROVER, + TABLE_COLUMNS.FIRST_APPROVED, // Grouped search views. TABLE_COLUMNS.EXPENSES, TABLE_COLUMNS.FEED, From 654eebad3665f50a9e71f6631946f6f81e29aa62 Mon Sep 17 00:00:00 2001 From: Mukher Date: Fri, 26 Jun 2026 16:41:34 +0500 Subject: [PATCH 15/16] lowercase new sort keys --- src/CONST/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 1ddf6c84a680..da993866423a 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6914,8 +6914,8 @@ const CONST = { DATE: 'date', SUBMITTED: 'submitted', APPROVED: 'approved', - FIRST_APPROVER: 'firstApprover', - FIRST_APPROVED: 'firstApproved', + FIRST_APPROVER: 'firstapprover', + FIRST_APPROVED: 'firstapproved', POSTED: 'posted', EXPORTED: 'exported', MERCHANT: 'merchant', From 606bbdddf9ab15c3578556155b0f1c400925dd2f Mon Sep 17 00:00:00 2001 From: Mukher Date: Fri, 26 Jun 2026 18:14:36 +0500 Subject: [PATCH 16/16] this removes the multi-cycle guard --- src/libs/SearchUIUtils.ts | 48 ++++++++------------------ tests/unit/Search/SearchUIUtilsTest.ts | 37 -------------------- 2 files changed, 14 insertions(+), 71 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 06c68f37cae1..16fc72b21c3a 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1455,42 +1455,11 @@ function buildLastExportedActionByReportIDMap(data: OnyxTypes.SearchResults['dat return lastExportedActionByReportID; } -/** - * @private - * Returns the report's first approval action of the CURRENT cycle — the earliest APPROVED/FORWARDED after the - * latest SUBMITTED (a SUBMITTED starts a new cycle, so older approvals are stale). Its actor is the first approver. - */ -function getFirstApprovalActionInCurrentCycle(actions: Record): OnyxTypes.ReportAction | undefined { - let latestSubmittedTime = -Infinity; - const approvalActions: OnyxTypes.ReportAction[] = []; - for (const action of Object.values(actions)) { - if (action.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) { - latestSubmittedTime = Math.max(latestSubmittedTime, new Date(action.created).getTime()); - } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED || action.actionName === CONST.REPORT.ACTIONS.TYPE.FORWARDED) { - approvalActions.push(action); - } - } - - let firstApprovalAction: OnyxTypes.ReportAction | undefined; - let earliestTime = Infinity; - for (const action of approvalActions) { - const currentTime = new Date(action.created).getTime(); - if (currentTime >= latestSubmittedTime && currentTime < earliestTime) { - earliestTime = currentTime; - firstApprovalAction = action; - } - } - return firstApprovalAction; -} - /** * @private * Whether any report in the results has an approval action — used to hide the empty First approver/approved columns. */ -function hasFirstApproverData(data: OnyxTypes.SearchResults['data'] | OnyxTypes.Transaction[]): boolean { - if (Array.isArray(data)) { - return false; - } +function hasFirstApproverData(data: OnyxTypes.SearchResults['data']): boolean { for (const key of Object.keys(data)) { if (!isReportActionEntry(key)) { continue; @@ -1843,6 +1812,10 @@ function processReportActionEntry(ctx: PreprocessingContext, key: string, action let latestExportTime = -Infinity; let latestExportAction: OnyxTypes.ReportAction | undefined; + // The first approver is the actor on the earliest APPROVED/FORWARDED action. + let firstApprovalTime = Infinity; + let firstApprovalAction: OnyxTypes.ReportAction | undefined; + for (const action of Object.values(actions)) { if (action.actionName === CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_CSV || action.actionName === CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION) { const currentTime = new Date(action.created).getTime(); @@ -1852,6 +1825,14 @@ function processReportActionEntry(ctx: PreprocessingContext, key: string, action } } + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED || action.actionName === CONST.REPORT.ACTIONS.TYPE.FORWARDED) { + const currentTime = new Date(action.created).getTime(); + if (currentTime < firstApprovalTime) { + firstApprovalTime = currentTime; + firstApprovalAction = action; + } + } + if (isMoneyRequestAction(action)) { const originalMessage = getOriginalMessage(action); const transactionID = originalMessage?.IOUTransactionID; @@ -1871,7 +1852,6 @@ function processReportActionEntry(ctx: PreprocessingContext, key: string, action ctx.lastExportedActionByReportID.set(reportID, latestExportAction); } - const firstApprovalAction = getFirstApprovalActionInCurrentCycle(actions); if (firstApprovalAction) { ctx.firstApprovedActionByReportID.set(reportID, firstApprovalAction); } @@ -5661,7 +5641,7 @@ function getColumnsToShow({ // Hide First approver/approved when no report has an approval action — an empty column is just noise. const firstApproverColumns = new Set([CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER, CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED]); - if (result.some((col) => firstApproverColumns.has(col)) && !hasFirstApproverData(data)) { + if (result.some((col) => firstApproverColumns.has(col)) && (Array.isArray(data) || !hasFirstApproverData(data))) { return result.filter((col) => !firstApproverColumns.has(col)); } diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 27e0c3bdb340..e9f3a138a743 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -6052,43 +6052,6 @@ describe('SearchUIUtils', () => { expect(item?.firstApproverAccountID).toBeUndefined(); expect(item?.formattedFirstApprover).toBe(''); }); - - it('should ignore a prior approval cycle and use the approval after the latest submit', () => { - const staleApprovedAt = '2024-12-10 08:00:00'; - const resubmittedAt = '2024-12-15 10:00:00'; - const currentApprovedAt = '2024-12-22 09:30:00'; - const data = makeReportFilterTestData( - {type: CONST.REPORT.TYPE.EXPENSE}, - {}, - { - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${rptFilterReportID}`]: { - // Prior cycle: approved, then unapproved/resubmitted, so this approval is stale. - 'approved-stale': { - reportActionID: 'approved-stale', - actionName: CONST.REPORT.ACTIONS.TYPE.APPROVED, - actorAccountID: adminAccountID, - created: staleApprovedAt, - }, - 'submitted-1': { - reportActionID: 'submitted-1', - actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED, - actorAccountID: adminAccountID, - created: resubmittedAt, - }, - 'approved-current': { - reportActionID: 'approved-current', - actionName: CONST.REPORT.ACTIONS.TYPE.APPROVED, - actorAccountID: approverAccountID, - created: currentApprovedAt, - }, - }, - }, - ); - const [sections] = callGetReportSections(data); - const item = sections.find((s) => s.keyForList === rptFilterReportID); - expect(item?.firstApproved).toBe(currentApprovedAt); - expect(item?.firstApproverAccountID).toBe(approverAccountID); - }); }); });