diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 8e1b003553bb8..7ae8c55a3ea1c 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6893,6 +6893,7 @@ const CONST = { MERCHANT: 'merchant', TAG: 'tag', MONTH: 'month', + WEEK: 'week', }, get TYPE_CUSTOM_COLUMNS() { return { @@ -6989,6 +6990,11 @@ const CONST = { EXPENSES: this.TABLE_COLUMNS.GROUP_EXPENSES, TOTAL: this.TABLE_COLUMNS.GROUP_TOTAL, }, + WEEK: { + WEEK: this.TABLE_COLUMNS.GROUP_WEEK, + EXPENSES: this.TABLE_COLUMNS.GROUP_EXPENSES, + TOTAL: this.TABLE_COLUMNS.GROUP_TOTAL, + }, }; }, get TYPE_DEFAULT_COLUMNS() { @@ -7034,6 +7040,7 @@ const CONST = { MERCHANT: [this.TABLE_COLUMNS.GROUP_MERCHANT, this.TABLE_COLUMNS.GROUP_EXPENSES, this.TABLE_COLUMNS.GROUP_TOTAL], TAG: [this.TABLE_COLUMNS.GROUP_TAG, this.TABLE_COLUMNS.GROUP_EXPENSES, this.TABLE_COLUMNS.GROUP_TOTAL], MONTH: [this.TABLE_COLUMNS.GROUP_MONTH, this.TABLE_COLUMNS.GROUP_EXPENSES, this.TABLE_COLUMNS.GROUP_TOTAL], + WEEK: [this.TABLE_COLUMNS.GROUP_WEEK, this.TABLE_COLUMNS.GROUP_EXPENSES, this.TABLE_COLUMNS.GROUP_TOTAL], }; }, BOOLEAN: { @@ -7133,6 +7140,7 @@ const CONST = { GROUP_MERCHANT: 'groupMerchant', GROUP_TAG: 'groupTag', GROUP_MONTH: 'groupmonth', + GROUP_WEEK: 'groupweek', }, SYNTAX_OPERATORS: { AND: 'and', @@ -7324,6 +7332,8 @@ const CONST = { [this.TABLE_COLUMNS.GROUP_CATEGORY]: 'group-category', [this.TABLE_COLUMNS.GROUP_MERCHANT]: 'group-merchant', [this.TABLE_COLUMNS.GROUP_TAG]: 'group-tag', + [this.TABLE_COLUMNS.GROUP_MONTH]: 'group-month', + [this.TABLE_COLUMNS.GROUP_WEEK]: 'group-week', }; }, NOT_MODIFIER: 'Not', diff --git a/src/components/Search/SearchList/index.tsx b/src/components/Search/SearchList/index.tsx index 62d90dc279c6f..991fa43b5a713 100644 --- a/src/components/Search/SearchList/index.tsx +++ b/src/components/Search/SearchList/index.tsx @@ -30,6 +30,7 @@ import type { TransactionListItemType, TransactionMerchantGroupListItemType, TransactionMonthGroupListItemType, + TransactionWeekGroupListItemType, } from '@components/SelectionListWithSections/types'; import Text from '@components/Text'; import useKeyboardState from '@hooks/useKeyboardState'; @@ -167,6 +168,13 @@ function isTransactionMatchWithGroupItem(transaction: Transaction, groupItem: Se const transactionDateString = transaction.modifiedCreated ?? transaction.created ?? ''; return DateUtils.isDateStringInMonth(transactionDateString, monthGroup.year, monthGroup.month); } + if (groupBy === CONST.SEARCH.GROUP_BY.WEEK) { + const weekGroup = groupItem as TransactionWeekGroupListItemType; + const transactionDateString = transaction.modifiedCreated ?? transaction.created ?? ''; + const datePart = transactionDateString.substring(0, 10); + const {start: weekStart, end: weekEnd} = DateUtils.getWeekDateRange(weekGroup.week); + return datePart >= weekStart && datePart <= weekEnd; + } return false; } diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 2acd2951b5d4f..1d98d3998db40 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -41,6 +41,7 @@ import {isSplitAction} from '@libs/ReportSecondaryActionUtils'; import {canEditFieldOfMoneyRequest, canHoldUnholdReportAction, canRejectReportAction, isOneTransactionReport, selectFilteredReportActions} from '@libs/ReportUtils'; import {buildCannedSearchQuery, buildSearchQueryJSON, buildSearchQueryString} from '@libs/SearchQueryUtils'; import { + adjustTimeRangeToDateFilters, createAndOpenSearchTransactionThread, getColumnsToShow, getListItem, @@ -60,6 +61,7 @@ import { isTransactionMerchantGroupListItemType, isTransactionMonthGroupListItemType, isTransactionTagGroupListItemType, + isTransactionWeekGroupListItemType, isTransactionWithdrawalIDGroupListItemType, shouldShowEmptyState, shouldShowYear as shouldShowYearUtil, @@ -545,7 +547,7 @@ function Search({ // For expense reports: when ANY transaction is selected, we want ALL transactions in the report selected. // This ensures report-level selection persists when new transactions are added. - const hasAnySelected = isExpenseReportType && transactionGroup.transactions.some((transaction) => transaction.transactionID in selectedTransactions); + const hasAnySelected = isExpenseReportType && transactionGroup.transactions.some((transaction: TransactionListItemType) => transaction.transactionID in selectedTransactions); for (const transactionItem of transactionGroup.transactions) { const isSelected = transactionItem.transactionID in selectedTransactions; @@ -905,13 +907,46 @@ function Search({ return; } - let reportID = item.reportID; - if (isTransactionItem && item?.reportAction?.childReportID) { - const isFromSelfDM = item.reportID === CONST.REPORT.UNREPORTED_REPORT_ID; - const isFromOneTransactionReport = isOneTransactionReport(item.report); + if (isTransactionWeekGroupListItemType(item)) { + if (!item.week) { + return; + } + // Extract the existing date filter to check for year-to-date or other date limits + const existingDateFilter = queryJSON.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); + const {start: weekStart, end: weekEnd} = adjustTimeRangeToDateFilters(DateUtils.getWeekDateRange(item.week), existingDateFilter); + const newFlatFilters = queryJSON.flatFilters.filter((filter) => filter.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); + newFlatFilters.push({ + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + filters: [ + {operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN_OR_EQUAL_TO, value: weekStart}, + {operator: CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO, value: weekEnd}, + ], + }); + const newQueryJSON: SearchQueryJSON = {...queryJSON, groupBy: undefined, flatFilters: newFlatFilters}; + const newQuery = buildSearchQueryString(newQueryJSON); + const newQueryJSONWithHash = buildSearchQueryJSON(newQuery); + if (!newQueryJSONWithHash) { + return; + } + handleSearch({queryJSON: newQueryJSONWithHash, searchKey, offset: 0, shouldCalculateTotals: false, isLoading: false}); + return; + } + + // After handling all group types, item should be TransactionListItemType, ReportActionListItemType, or TransactionGroupListItemType + if (!isTransactionItem && !isReportActionListItemType(item) && !isTransactionGroupListItemType(item)) { + return; + } + + const transactionItem = item as TransactionListItemType; + const reportActionItem = item as ReportActionListItemType; + + let reportID = transactionItem.reportID ?? reportActionItem.reportID; + if (isTransactionItem && transactionItem?.reportAction?.childReportID) { + const isFromSelfDM = transactionItem.reportID === CONST.REPORT.UNREPORTED_REPORT_ID; + const isFromOneTransactionReport = isOneTransactionReport(transactionItem.report); if (isFromSelfDM || !isFromOneTransactionReport) { - reportID = item?.reportAction?.childReportID; + reportID = transactionItem?.reportAction?.childReportID; } } @@ -927,8 +962,9 @@ function Search({ }); if (isTransactionGroupListItemType(item)) { - const firstTransaction = item.transactions.at(0); - if (item.isOneTransactionReport && firstTransaction && transactionPreviewData) { + const groupItem = item as TransactionGroupListItemType; + const firstTransaction = groupItem.transactions.at(0); + if (groupItem.isOneTransactionReport && firstTransaction && transactionPreviewData) { if (!firstTransaction?.reportAction?.childReportID) { createAndOpenSearchTransactionThread(firstTransaction, backTo, firstTransaction?.reportAction?.childReportID, transactionPreviewData, false); } else { @@ -936,7 +972,7 @@ function Search({ } } - if (item.transactions.length > 1) { + if (groupItem.transactions.length > 1) { markReportIDAsMultiTransactionExpense(reportID); } else { unmarkReportIDAsMultiTransactionExpense(reportID); @@ -947,7 +983,7 @@ function Search({ } if (isReportActionListItemType(item)) { - const reportActionID = item.reportActionID; + const reportActionID = reportActionItem.reportActionID; Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID, reportActionID, backTo})); return; } @@ -955,7 +991,7 @@ function Search({ markReportIDAsExpense(reportID); if (isTransactionItem && transactionPreviewData) { - setOptimisticDataForTransactionThreadPreview(item, transactionPreviewData, item?.reportAction?.childReportID); + setOptimisticDataForTransactionThreadPreview(transactionItem, transactionPreviewData, transactionItem?.reportAction?.childReportID); } requestAnimationFrame(() => Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID, backTo}))); diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 5b5ac89fa1cd5..d9cae3b69282c 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -139,7 +139,8 @@ type SearchCustomColumnIds = | ValueOf | ValueOf | ValueOf - | ValueOf; + | ValueOf + | ValueOf; type SearchContextData = { currentSearchHash: number; diff --git a/src/components/SelectionListWithSections/Search/BaseListItemHeader.tsx b/src/components/SelectionListWithSections/Search/BaseListItemHeader.tsx index 576f06511391a..ceba4f113af50 100644 --- a/src/components/SelectionListWithSections/Search/BaseListItemHeader.tsx +++ b/src/components/SelectionListWithSections/Search/BaseListItemHeader.tsx @@ -30,14 +30,16 @@ type GroupColumnKey = | typeof CONST.SEARCH.TABLE_COLUMNS.GROUP_CATEGORY | typeof CONST.SEARCH.TABLE_COLUMNS.GROUP_MERCHANT | typeof CONST.SEARCH.TABLE_COLUMNS.GROUP_TAG - | typeof CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH; + | typeof CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH + | typeof CONST.SEARCH.TABLE_COLUMNS.GROUP_WEEK; /** Supported column style keys for sizing */ type ColumnStyleKey = | typeof CONST.SEARCH.TABLE_COLUMNS.CATEGORY | typeof CONST.SEARCH.TABLE_COLUMNS.MERCHANT | typeof CONST.SEARCH.TABLE_COLUMNS.TAG - | typeof CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH; + | typeof CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH + | typeof CONST.SEARCH.TABLE_COLUMNS.GROUP_WEEK; type BaseListItemHeaderProps = { /** The group item being rendered */ diff --git a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx index 12d5e4e1dd91d..c99e50489dc4a 100644 --- a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx @@ -22,6 +22,7 @@ import type { TransactionMonthGroupListItemType, TransactionReportGroupListItemType, TransactionTagGroupListItemType, + TransactionWeekGroupListItemType, TransactionWithdrawalIDGroupListItemType, } from '@components/SelectionListWithSections/types'; import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; @@ -50,6 +51,7 @@ import MonthListItemHeader from './MonthListItemHeader'; import ReportListItemHeader from './ReportListItemHeader'; import TagListItemHeader from './TagListItemHeader'; import TransactionGroupListExpandedItem from './TransactionGroupListExpanded'; +import WeekListItemHeader from './WeekListItemHeader'; import WithdrawalIDListItemHeader from './WithdrawalIDListItemHeader'; function TransactionGroupListItem({ @@ -340,6 +342,19 @@ function TransactionGroupListItem({ isExpanded={isExpanded} /> ), + [CONST.SEARCH.GROUP_BY.WEEK]: ( + + ), }; if (searchType === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT) { diff --git a/src/components/SelectionListWithSections/Search/WeekListItemHeader.tsx b/src/components/SelectionListWithSections/Search/WeekListItemHeader.tsx new file mode 100644 index 0000000000000..680dc53f5133e --- /dev/null +++ b/src/components/SelectionListWithSections/Search/WeekListItemHeader.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import type {ListItem, TransactionWeekGroupListItemType} from '@components/SelectionListWithSections/types'; +import CONST from '@src/CONST'; +import type {BaseListItemHeaderProps} from './BaseListItemHeader'; +import BaseListItemHeader from './BaseListItemHeader'; + +type WeekListItemHeaderProps = Omit, 'item' | 'displayName' | 'groupColumnKey' | 'columnStyleKey'> & { + /** The week group currently being looked at */ + week: TransactionWeekGroupListItemType; +}; + +function WeekListItemHeader({ + week: weekItem, + onCheckboxPress, + isDisabled, + canSelectMultiple, + isSelectAllChecked, + isIndeterminate, + isExpanded, + onDownArrowClick, + columns, +}: WeekListItemHeaderProps) { + const weekName = weekItem.formattedWeek; + + return ( + + ); +} + +export default WeekListItemHeader; diff --git a/src/components/SelectionListWithSections/SearchTableHeader.tsx b/src/components/SelectionListWithSections/SearchTableHeader.tsx index cc7ff13f5bbad..7414a7d9de480 100644 --- a/src/components/SelectionListWithSections/SearchTableHeader.tsx +++ b/src/components/SelectionListWithSections/SearchTableHeader.tsx @@ -409,6 +409,24 @@ const getTransactionGroupHeaders = (groupBy: SearchGroupBy, icons: SearchHeaderI isColumnSortable: true, }, ]; + case CONST.SEARCH.GROUP_BY.WEEK: + return [ + { + columnName: CONST.SEARCH.TABLE_COLUMNS.GROUP_WEEK, + translationKey: 'common.week', + isColumnSortable: true, + }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.GROUP_EXPENSES, + translationKey: 'common.expenses', + isColumnSortable: true, + }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.GROUP_TOTAL, + translationKey: 'common.total', + isColumnSortable: true, + }, + ]; default: return []; } diff --git a/src/components/SelectionListWithSections/types.ts b/src/components/SelectionListWithSections/types.ts index 000a55439b94b..418b99def5f34 100644 --- a/src/components/SelectionListWithSections/types.ts +++ b/src/components/SelectionListWithSections/types.ts @@ -38,6 +38,7 @@ import type { SearchTagGroup, SearchTask, SearchTransactionAction, + SearchWeekGroup, SearchWithdrawalIDGroup, } from '@src/types/onyx/SearchResults'; import type {ReceiptErrors} from '@src/types/onyx/Transaction'; @@ -523,6 +524,11 @@ type TransactionTagGroupListItemType = TransactionGroupListItemType & {groupedBy formattedTag?: string; }; +type TransactionWeekGroupListItemType = TransactionGroupListItemType & {groupedBy: typeof CONST.SEARCH.GROUP_BY.WEEK} & SearchWeekGroup & { + /** Final and formatted "week" value used for displaying */ + formattedWeek: string; + }; + type ListItemProps = CommonListItemProps & { /** The section list item */ item: TItem; @@ -1168,6 +1174,7 @@ export type { TransactionCategoryGroupListItemType, TransactionMerchantGroupListItemType, TransactionTagGroupListItemType, + TransactionWeekGroupListItemType, Section, SectionListDataType, SectionWithIndexOffset, diff --git a/src/languages/de.ts b/src/languages/de.ts index 37cc5389e0499..211b1dc74c0bd 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -641,6 +641,7 @@ const translations: TranslationDeepObject = { newFeature: 'Neue Funktion', month: 'Monat', home: 'Startseite', + week: 'Woche', }, supportalNoAccess: { title: 'Nicht so schnell', @@ -7002,6 +7003,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard [CONST.SEARCH.GROUP_BY.MERCHANT]: 'Händler', [CONST.SEARCH.GROUP_BY.TAG]: 'Stichwort', [CONST.SEARCH.GROUP_BY.MONTH]: 'Monat', + [CONST.SEARCH.GROUP_BY.WEEK]: 'Woche', }, feed: 'Feed', withdrawalType: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 6ca777013fd35..321f42b01b1fd 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -633,6 +633,7 @@ const translations = { reimbursableTotal: 'Reimbursable total', nonReimbursableTotal: 'Non-reimbursable total', month: 'Month', + week: 'Week', }, supportalNoAccess: { title: 'Not so fast', @@ -6893,6 +6894,7 @@ const translations = { [CONST.SEARCH.GROUP_BY.MERCHANT]: 'Merchant', [CONST.SEARCH.GROUP_BY.TAG]: 'Tag', [CONST.SEARCH.GROUP_BY.MONTH]: 'Month', + [CONST.SEARCH.GROUP_BY.WEEK]: 'Week', }, feed: 'Feed', withdrawalType: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 42065f9145a82..eb89429a1865e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -392,6 +392,7 @@ const translations: TranslationDeepObject = { reimbursableTotal: 'Total reembolsable', nonReimbursableTotal: 'Total no reembolsable', month: 'Monat', + week: 'Semana', }, supportalNoAccess: { title: 'No tan rápido', @@ -6644,6 +6645,7 @@ ${amount} para ${merchant} - ${date}`, [CONST.SEARCH.GROUP_BY.MERCHANT]: 'Comerciante', [CONST.SEARCH.GROUP_BY.TAG]: 'Etiqueta', [CONST.SEARCH.GROUP_BY.MONTH]: 'Mes', + [CONST.SEARCH.GROUP_BY.WEEK]: 'Semana', }, feed: 'Feed', withdrawalType: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 1001b7ec7b44b..5a0cc2dcc9ace 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -643,6 +643,7 @@ const translations: TranslationDeepObject = { newFeature: 'Nouvelle fonctionnalité', month: 'Mois', home: 'Accueil', + week: 'Semaine', }, supportalNoAccess: { title: 'Pas si vite', @@ -7014,6 +7015,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin [CONST.SEARCH.GROUP_BY.MERCHANT]: 'Commerçant', [CONST.SEARCH.GROUP_BY.TAG]: 'Étiquette', [CONST.SEARCH.GROUP_BY.MONTH]: 'Mois', + [CONST.SEARCH.GROUP_BY.WEEK]: 'Semaine', }, feed: 'Flux', withdrawalType: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 2c9602fd0ceee..dd80e98c2bf59 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -642,6 +642,7 @@ const translations: TranslationDeepObject = { newFeature: 'Nuova funzionalità', month: 'Mese', home: 'Home', + week: 'Settimana', }, supportalNoAccess: { title: 'Non così in fretta', @@ -6991,6 +6992,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori [CONST.SEARCH.GROUP_BY.MERCHANT]: 'Commerciante', [CONST.SEARCH.GROUP_BY.TAG]: 'Etichetta', [CONST.SEARCH.GROUP_BY.MONTH]: 'Mese', + [CONST.SEARCH.GROUP_BY.WEEK]: 'Settimana', }, feed: 'Feed', withdrawalType: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index e6e27d1105acf..5088ce87f5eae 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -641,6 +641,7 @@ const translations: TranslationDeepObject = { newFeature: '新機能', month: '月', home: 'ホーム', + week: '週', }, supportalNoAccess: { title: 'ちょっと待ってください', @@ -6931,6 +6932,7 @@ ${reportName} [CONST.SEARCH.GROUP_BY.MERCHANT]: '加盟店', [CONST.SEARCH.GROUP_BY.TAG]: 'タグ', [CONST.SEARCH.GROUP_BY.MONTH]: '月', + [CONST.SEARCH.GROUP_BY.WEEK]: '週', }, feed: 'フィード', withdrawalType: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 36fed3dc90e96..8a8ccfda35a22 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -642,6 +642,7 @@ const translations: TranslationDeepObject = { newFeature: 'Nieuwe functie', month: 'Maand', home: 'Start', + week: 'Week', }, supportalNoAccess: { title: 'Niet zo snel', @@ -6974,6 +6975,7 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten [CONST.SEARCH.GROUP_BY.MERCHANT]: 'Verkoper', [CONST.SEARCH.GROUP_BY.TAG]: 'Label', [CONST.SEARCH.GROUP_BY.MONTH]: 'Maand', + [CONST.SEARCH.GROUP_BY.WEEK]: 'Week', }, feed: 'Feed', withdrawalType: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index bf491b0313a37..a1994258b1b78 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -642,6 +642,7 @@ const translations: TranslationDeepObject = { newFeature: 'Nowa funkcja', month: 'Miesiąc', home: 'Strona główna', + week: 'Tydzień', }, supportalNoAccess: { title: 'Nie tak szybko', @@ -6963,6 +6964,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i [CONST.SEARCH.GROUP_BY.MERCHANT]: 'Sprzedawca', [CONST.SEARCH.GROUP_BY.TAG]: 'Etykieta', [CONST.SEARCH.GROUP_BY.MONTH]: 'Miesiąc', + [CONST.SEARCH.GROUP_BY.WEEK]: 'Tydzień', }, feed: 'Kanał', withdrawalType: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 3af89a7d3daff..ba2cea47e0ee2 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -641,6 +641,7 @@ const translations: TranslationDeepObject = { newFeature: 'Novo recurso', month: 'Mês', home: 'Início', + week: 'Semana', }, supportalNoAccess: { title: 'Não tão rápido', @@ -6964,6 +6965,7 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe [CONST.SEARCH.GROUP_BY.MERCHANT]: 'Comerciante', [CONST.SEARCH.GROUP_BY.TAG]: 'Etiqueta', [CONST.SEARCH.GROUP_BY.MONTH]: 'Mês', + [CONST.SEARCH.GROUP_BY.WEEK]: 'Semana', }, feed: 'Feed', withdrawalType: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index ecdb4078892f3..692863b6fab83 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -638,6 +638,7 @@ const translations: TranslationDeepObject = { newFeature: '新功能', month: '月', home: '首页', + week: '周', }, supportalNoAccess: { title: '先别急', @@ -6811,6 +6812,7 @@ ${reportName} [CONST.SEARCH.GROUP_BY.MERCHANT]: '商家', [CONST.SEARCH.GROUP_BY.TAG]: '标签', [CONST.SEARCH.GROUP_BY.MONTH]: '月', + [CONST.SEARCH.GROUP_BY.WEEK]: '周', }, feed: '动态', withdrawalType: { diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 8b5341ae2d4e8..9a512833eeaac 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -928,6 +928,23 @@ function getMonthDateRange(year: number, month: number): {start: string; end: st }; } +/** + * Returns the start and end dates of a week in the format yyyy-MM-dd. + * @param weekStartDate - Week start date string in YYYY-MM-DD format + */ +function getWeekDateRange(weekStartDate: string): {start: string; end: string} { + // Parse the date string as a local date to avoid timezone issues + // Using parse with explicit format ensures it's treated as local time, not UTC + // This prevents dates like '2026-01-25' from being interpreted as UTC midnight + // which would shift to the previous day in timezones behind UTC (e.g., PST) + const weekStart = parse(weekStartDate, 'yyyy-MM-dd', new Date()); + const weekEnd = addDays(weekStart, 6); + return { + start: format(weekStart, 'yyyy-MM-dd'), + end: format(weekEnd, 'yyyy-MM-dd'), + }; +} + /** * Checks if a date string (yyyy-MM-dd or yyyy-MM-dd HH:mm:ss) falls within a specific month. * Uses string comparison to avoid timezone issues. @@ -944,6 +961,18 @@ function isDateStringInMonth(dateString: string, year: number, month: number): b return datePart >= monthStart && datePart <= monthEnd; } +/** + * Returns a formatted date range. + */ +function getFormattedDateRangeForSearch(startDate: string, endDate: string): string { + const start = parse(startDate, 'yyyy-MM-dd', new Date()); + const end = parse(endDate, 'yyyy-MM-dd', new Date()); + if (isSameYear(new Date(start), new Date(end))) { + return `${format(start, 'MMM d')} - ${format(end, 'MMM d, yyyy')}`; + } + return `${format(start, 'MMM d, yyyy')} - ${format(end, 'MMM d, yyyy')}`; +} + const DateUtils = { isDate, formatToDayOfWeek, @@ -1003,7 +1032,9 @@ const DateUtils = { isCurrentTimeWithinRange, formatInTimeZoneWithFallback, getMonthDateRange, + getWeekDateRange, isDateStringInMonth, + getFormattedDateRangeForSearch, }; export default DateUtils; diff --git a/src/libs/SearchParser/autocompleteParser.js b/src/libs/SearchParser/autocompleteParser.js index eb73ff9aa89fc..df1170954c7fe 100644 --- a/src/libs/SearchParser/autocompleteParser.js +++ b/src/libs/SearchParser/autocompleteParser.js @@ -273,14 +273,15 @@ function peg$parse(input, options) { var peg$c87 = "group-tag"; var peg$c88 = "group-merchant"; var peg$c89 = "group-month"; - var peg$c90 = "!="; - var peg$c91 = ">="; - var peg$c92 = ">"; - var peg$c93 = "<="; - var peg$c94 = "<"; - var peg$c95 = "\u201C"; - var peg$c96 = "\u201D"; - var peg$c97 = "\""; + var peg$c90 = "group-week"; + var peg$c91 = "!="; + var peg$c92 = ">="; + var peg$c93 = ">"; + var peg$c94 = "<="; + var peg$c95 = "<"; + var peg$c96 = "\u201C"; + var peg$c97 = "\u201D"; + var peg$c98 = "\""; var peg$r0 = /^[ \t\r\n\xA0,:=<>!]/; var peg$r1 = /^[:=]/; @@ -389,30 +390,31 @@ function peg$parse(input, options) { var peg$e90 = peg$literalExpectation("group-tag", true); var peg$e91 = peg$literalExpectation("group-merchant", true); var peg$e92 = peg$literalExpectation("group-month", true); - var peg$e93 = peg$otherExpectation("operator"); - var peg$e94 = peg$classExpectation([":", "="], false, false); - var peg$e95 = peg$literalExpectation("!=", false); - var peg$e96 = peg$literalExpectation(">=", false); - var peg$e97 = peg$literalExpectation(">", false); - var peg$e98 = peg$literalExpectation("<=", false); - var peg$e99 = peg$literalExpectation("<", false); - var peg$e100 = peg$otherExpectation("word"); - var peg$e101 = peg$classExpectation([" ", ",", "\t", "\n", "\r", "\xA0"], true, false); - var peg$e102 = peg$otherExpectation("whitespace"); - var peg$e103 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], false, false); - var peg$e104 = peg$otherExpectation("quote"); - var peg$e105 = peg$classExpectation([" ", ",", "\"", "\u201D", "\u201C", "\t", "\n", "\r", "\xA0"], true, false); - var peg$e106 = peg$classExpectation(["\"", ["\u201C", "\u201D"]], false, false); - var peg$e107 = peg$classExpectation(["\"", "\u201D", "\u201C", "\r", "\n"], true, false); - var peg$e108 = peg$literalExpectation("\u201C", false); - var peg$e109 = peg$literalExpectation("\u201D", false); - var peg$e110 = peg$literalExpectation("\"", false); - var peg$e111 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"], ["0", "9"]], false, false); - var peg$e112 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false); - var peg$e113 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0"], false, false); - var peg$e114 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"]], false, false); - var peg$e115 = peg$classExpectation([","], false, false); - var peg$e116 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ","], false, false); + var peg$e93 = peg$literalExpectation("group-week", true); + var peg$e94 = peg$otherExpectation("operator"); + var peg$e95 = peg$classExpectation([":", "="], false, false); + var peg$e96 = peg$literalExpectation("!=", false); + var peg$e97 = peg$literalExpectation(">=", false); + var peg$e98 = peg$literalExpectation(">", false); + var peg$e99 = peg$literalExpectation("<=", false); + var peg$e100 = peg$literalExpectation("<", false); + var peg$e101 = peg$otherExpectation("word"); + var peg$e102 = peg$classExpectation([" ", ",", "\t", "\n", "\r", "\xA0"], true, false); + var peg$e103 = peg$otherExpectation("whitespace"); + var peg$e104 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], false, false); + var peg$e105 = peg$otherExpectation("quote"); + var peg$e106 = peg$classExpectation([" ", ",", "\"", "\u201D", "\u201C", "\t", "\n", "\r", "\xA0"], true, false); + var peg$e107 = peg$classExpectation(["\"", ["\u201C", "\u201D"]], false, false); + var peg$e108 = peg$classExpectation(["\"", "\u201D", "\u201C", "\r", "\n"], true, false); + var peg$e109 = peg$literalExpectation("\u201C", false); + var peg$e110 = peg$literalExpectation("\u201D", false); + var peg$e111 = peg$literalExpectation("\"", false); + var peg$e112 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"], ["0", "9"]], false, false); + var peg$e113 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false); + var peg$e114 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0"], false, false); + var peg$e115 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"]], false, false); + var peg$e116 = peg$classExpectation([","], false, false); + var peg$e117 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ","], false, false); var peg$f0 = function(ranges) { return { autocomplete, ranges }; }; var peg$f1 = function(filters) { return filters.filter(Boolean).flat(); }; @@ -554,32 +556,33 @@ function peg$parse(input, options) { var peg$f79 = function() { return "groupTag"; }; var peg$f80 = function() { return "groupMerchant"; }; var peg$f81 = function() { return "groupMonth"; }; - var peg$f82 = function() { return "eq"; }; - var peg$f83 = function() { return "neq"; }; - var peg$f84 = function() { return "gte"; }; - var peg$f85 = function() { return "gt"; }; - var peg$f86 = function() { return "lte"; }; - var peg$f87 = function() { return "lt"; }; - var peg$f88 = function(o) { + var peg$f82 = function() { return "groupWeek"; }; + var peg$f83 = function() { return "eq"; }; + var peg$f84 = function() { return "neq"; }; + var peg$f85 = function() { return "gte"; }; + var peg$f86 = function() { return "gt"; }; + var peg$f87 = function() { return "lte"; }; + var peg$f88 = function() { return "lt"; }; + var peg$f89 = function(o) { if (nameOperator) { expectingNestedQuote = (o === "eq"); // Use simple parser if no valid operator is found } isColumnsContext = false; return o; }; - var peg$f89 = function(chars) { return chars.join("").trim(); }; - var peg$f90 = function() { + var peg$f90 = function(chars) { return chars.join("").trim(); }; + var peg$f91 = function() { isColumnsContext = false; return "and"; }; - var peg$f91 = function() { return expectingNestedQuote; }; - var peg$f92 = function(start, inner, end) { //handle no-breaking space + var peg$f92 = function() { return expectingNestedQuote; }; + var peg$f93 = function(start, inner, end) { //handle no-breaking space return [...start, '"', ...inner, '"', ...end].join(""); }; - var peg$f93 = function(start) {return "“"}; - var peg$f94 = function(start) {return "”"}; - var peg$f95 = function(start) {return "\""}; - var peg$f96 = function(start, inner, end) { + var peg$f94 = function(start) {return "“"}; + var peg$f95 = function(start) {return "”"}; + var peg$f96 = function(start) {return "\""}; + var peg$f97 = function(start, inner, end) { return [...start, '"', ...inner, '"'].join(""); }; var peg$currPos = options.peg$currPos | 0; @@ -2386,6 +2389,9 @@ function peg$parse(input, options) { s0 = peg$parsegroupMerchant(); if (s0 === peg$FAILED) { s0 = peg$parsegroupMonth(); + if (s0 === peg$FAILED) { + s0 = peg$parsegroupWeek(); + } } } } @@ -3384,6 +3390,43 @@ function peg$parse(input, options) { return s0; } + function peg$parsegroupWeek() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = input.substr(peg$currPos, 10); + if (s1.toLowerCase() === peg$c90) { + peg$currPos += 10; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e93); } + } + if (s1 !== peg$FAILED) { + s2 = peg$currPos; + peg$silentFails++; + s3 = peg$parsewordBoundary(); + peg$silentFails--; + if (s3 !== peg$FAILED) { + peg$currPos = s2; + s2 = undefined; + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f82(); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + function peg$parseoperator() { var s0, s1; @@ -3394,81 +3437,81 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e94); } + if (peg$silentFails === 0) { peg$fail(peg$e95); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f82(); + s1 = peg$f83(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c90) { - s1 = peg$c90; + if (input.substr(peg$currPos, 2) === peg$c91) { + s1 = peg$c91; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e95); } + if (peg$silentFails === 0) { peg$fail(peg$e96); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f83(); + s1 = peg$f84(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c91) { - s1 = peg$c91; + if (input.substr(peg$currPos, 2) === peg$c92) { + s1 = peg$c92; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e96); } + if (peg$silentFails === 0) { peg$fail(peg$e97); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f84(); + s1 = peg$f85(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 62) { - s1 = peg$c92; + s1 = peg$c93; peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e97); } + if (peg$silentFails === 0) { peg$fail(peg$e98); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f85(); + s1 = peg$f86(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c93) { - s1 = peg$c93; + if (input.substr(peg$currPos, 2) === peg$c94) { + s1 = peg$c94; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e98); } + if (peg$silentFails === 0) { peg$fail(peg$e99); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f86(); + s1 = peg$f87(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 60) { - s1 = peg$c94; + s1 = peg$c95; peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e99); } + if (peg$silentFails === 0) { peg$fail(peg$e100); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f87(); + s1 = peg$f88(); } s0 = s1; } @@ -3479,7 +3522,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e93); } + if (peg$silentFails === 0) { peg$fail(peg$e94); } } return s0; @@ -3492,7 +3535,7 @@ function peg$parse(input, options) { s1 = peg$parseoperator(); if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f88(s1); + s1 = peg$f89(s1); } s0 = s1; @@ -3510,7 +3553,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e101); } + if (peg$silentFails === 0) { peg$fail(peg$e102); } } if (s2 !== peg$FAILED) { while (s2 !== peg$FAILED) { @@ -3520,7 +3563,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e101); } + if (peg$silentFails === 0) { peg$fail(peg$e102); } } } } else { @@ -3528,13 +3571,13 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f89(s1); + s1 = peg$f90(s1); } s0 = s1; peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e100); } + if (peg$silentFails === 0) { peg$fail(peg$e101); } } return s0; @@ -3546,7 +3589,7 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = peg$parse_(); peg$savedPos = s0; - s1 = peg$f90(); + s1 = peg$f91(); s0 = s1; return s0; @@ -3562,7 +3605,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e103); } + if (peg$silentFails === 0) { peg$fail(peg$e104); } } while (s1 !== peg$FAILED) { s0.push(s1); @@ -3571,12 +3614,12 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e103); } + if (peg$silentFails === 0) { peg$fail(peg$e104); } } } peg$silentFails--; s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e102); } + if (peg$silentFails === 0) { peg$fail(peg$e103); } return s0; } @@ -3586,7 +3629,7 @@ function peg$parse(input, options) { s0 = peg$currPos; peg$savedPos = peg$currPos; - s1 = peg$f91(); + s1 = peg$f92(); if (s1) { s1 = undefined; } else { @@ -3623,7 +3666,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e105); } + if (peg$silentFails === 0) { peg$fail(peg$e106); } } while (s2 !== peg$FAILED) { s1.push(s2); @@ -3632,7 +3675,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e105); } + if (peg$silentFails === 0) { peg$fail(peg$e106); } } } s2 = input.charAt(peg$currPos); @@ -3640,7 +3683,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e106); } + if (peg$silentFails === 0) { peg$fail(peg$e107); } } if (s2 !== peg$FAILED) { s3 = []; @@ -3649,7 +3692,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e107); } + if (peg$silentFails === 0) { peg$fail(peg$e108); } } while (s4 !== peg$FAILED) { s3.push(s4); @@ -3658,7 +3701,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e107); } + if (peg$silentFails === 0) { peg$fail(peg$e108); } } } s4 = input.charAt(peg$currPos); @@ -3666,7 +3709,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e106); } + if (peg$silentFails === 0) { peg$fail(peg$e107); } } if (s4 !== peg$FAILED) { s5 = []; @@ -3675,7 +3718,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e101); } + if (peg$silentFails === 0) { peg$fail(peg$e102); } } while (s6 !== peg$FAILED) { s5.push(s6); @@ -3684,11 +3727,11 @@ function peg$parse(input, options) { peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e101); } + if (peg$silentFails === 0) { peg$fail(peg$e102); } } } peg$savedPos = s0; - s0 = peg$f92(s1, s3, s5); + s0 = peg$f93(s1, s3, s5); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3700,7 +3743,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e104); } + if (peg$silentFails === 0) { peg$fail(peg$e105); } } return s0; @@ -3717,7 +3760,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e105); } + if (peg$silentFails === 0) { peg$fail(peg$e106); } } while (s2 !== peg$FAILED) { s1.push(s2); @@ -3726,7 +3769,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e105); } + if (peg$silentFails === 0) { peg$fail(peg$e106); } } } s2 = input.charAt(peg$currPos); @@ -3734,7 +3777,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e106); } + if (peg$silentFails === 0) { peg$fail(peg$e107); } } if (s2 !== peg$FAILED) { s3 = []; @@ -3743,7 +3786,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e107); } + if (peg$silentFails === 0) { peg$fail(peg$e108); } } if (s4 === peg$FAILED) { s4 = peg$currPos; @@ -3759,15 +3802,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 8220) { - s6 = peg$c95; + s6 = peg$c96; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e108); } + if (peg$silentFails === 0) { peg$fail(peg$e109); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f93(s1); + s4 = peg$f94(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -3790,15 +3833,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 8221) { - s6 = peg$c96; + s6 = peg$c97; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e109); } + if (peg$silentFails === 0) { peg$fail(peg$e110); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f94(s1); + s4 = peg$f95(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -3821,15 +3864,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 34) { - s6 = peg$c97; + s6 = peg$c98; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e110); } + if (peg$silentFails === 0) { peg$fail(peg$e111); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f95(s1); + s4 = peg$f96(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -3848,7 +3891,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e107); } + if (peg$silentFails === 0) { peg$fail(peg$e108); } } if (s4 === peg$FAILED) { s4 = peg$currPos; @@ -3864,15 +3907,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 8220) { - s6 = peg$c95; + s6 = peg$c96; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e108); } + if (peg$silentFails === 0) { peg$fail(peg$e109); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f93(s1); + s4 = peg$f94(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -3895,15 +3938,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 8221) { - s6 = peg$c96; + s6 = peg$c97; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e109); } + if (peg$silentFails === 0) { peg$fail(peg$e110); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f94(s1); + s4 = peg$f95(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -3926,15 +3969,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 34) { - s6 = peg$c97; + s6 = peg$c98; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e110); } + if (peg$silentFails === 0) { peg$fail(peg$e111); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f95(s1); + s4 = peg$f96(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -3950,7 +3993,7 @@ function peg$parse(input, options) { s4 = peg$parseclosingQuote(); if (s4 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f96(s1, s3, s4); + s0 = peg$f97(s1, s3, s4); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3962,7 +4005,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e104); } + if (peg$silentFails === 0) { peg$fail(peg$e105); } } return s0; @@ -3977,7 +4020,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e106); } + if (peg$silentFails === 0) { peg$fail(peg$e107); } } if (s1 !== peg$FAILED) { s2 = peg$currPos; @@ -4015,7 +4058,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e111); } + if (peg$silentFails === 0) { peg$fail(peg$e112); } } while (s2 !== peg$FAILED) { s1.push(s2); @@ -4024,7 +4067,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e111); } + if (peg$silentFails === 0) { peg$fail(peg$e112); } } } s2 = []; @@ -4033,7 +4076,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e112); } + if (peg$silentFails === 0) { peg$fail(peg$e113); } } while (s3 !== peg$FAILED) { s2.push(s3); @@ -4042,7 +4085,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e112); } + if (peg$silentFails === 0) { peg$fail(peg$e113); } } } s3 = []; @@ -4051,7 +4094,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e113); } + if (peg$silentFails === 0) { peg$fail(peg$e114); } } while (s4 !== peg$FAILED) { s3.push(s4); @@ -4060,7 +4103,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e113); } + if (peg$silentFails === 0) { peg$fail(peg$e114); } } } s4 = peg$parseoperator(); @@ -4079,7 +4122,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e114); } + if (peg$silentFails === 0) { peg$fail(peg$e115); } } while (s2 !== peg$FAILED) { s1.push(s2); @@ -4088,7 +4131,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e114); } + if (peg$silentFails === 0) { peg$fail(peg$e115); } } } s2 = peg$currPos; @@ -4135,7 +4178,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e115); } + if (peg$silentFails === 0) { peg$fail(peg$e116); } } } } @@ -4151,7 +4194,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e116); } + if (peg$silentFails === 0) { peg$fail(peg$e117); } } if (s0 === peg$FAILED) { s0 = peg$currPos; diff --git a/src/libs/SearchParser/baseRules.peggy b/src/libs/SearchParser/baseRules.peggy index 1495b03a72bff..5195e46cfd1af 100644 --- a/src/libs/SearchParser/baseRules.peggy +++ b/src/libs/SearchParser/baseRules.peggy @@ -117,6 +117,7 @@ columnsValues / groupTag / groupMerchant / groupMonth + / groupWeek perDiem = "per-diem"i &wordBoundary { return "perDiem"; } draft = ("drafts"i / "draft"i) &wordBoundary { return "drafts"; } @@ -144,6 +145,7 @@ groupCategory = "group-category"i &wordBoundary { return "gro groupTag = "group-tag"i &wordBoundary { return "groupTag"; } groupMerchant = "group-merchant"i &wordBoundary { return "groupMerchant"; } groupMonth = "group-month"i &wordBoundary { return "groupMonth"; } +groupWeek = "group-week"i &wordBoundary { return "groupWeek"; } operator "operator" diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js index bffc22cfbfa33..0b307d3b01d9d 100644 --- a/src/libs/SearchParser/searchParser.js +++ b/src/libs/SearchParser/searchParser.js @@ -274,14 +274,15 @@ function peg$parse(input, options) { var peg$c87 = "group-tag"; var peg$c88 = "group-merchant"; var peg$c89 = "group-month"; - var peg$c90 = "!="; - var peg$c91 = ">="; - var peg$c92 = ">"; - var peg$c93 = "<="; - var peg$c94 = "<"; - var peg$c95 = "\u201C"; - var peg$c96 = "\u201D"; - var peg$c97 = "\""; + var peg$c90 = "group-week"; + var peg$c91 = "!="; + var peg$c92 = ">="; + var peg$c93 = ">"; + var peg$c94 = "<="; + var peg$c95 = "<"; + var peg$c96 = "\u201C"; + var peg$c97 = "\u201D"; + var peg$c98 = "\""; var peg$r0 = /^[^ \t\r\n\xA0]/; var peg$r1 = /^[ \t\r\n\xA0,:=<>!]/; @@ -393,30 +394,31 @@ function peg$parse(input, options) { var peg$e92 = peg$literalExpectation("group-tag", true); var peg$e93 = peg$literalExpectation("group-merchant", true); var peg$e94 = peg$literalExpectation("group-month", true); - var peg$e95 = peg$otherExpectation("operator"); - var peg$e96 = peg$classExpectation([":", "="], false, false); - var peg$e97 = peg$literalExpectation("!=", false); - var peg$e98 = peg$literalExpectation(">=", false); - var peg$e99 = peg$literalExpectation(">", false); - var peg$e100 = peg$literalExpectation("<=", false); - var peg$e101 = peg$literalExpectation("<", false); - var peg$e102 = peg$otherExpectation("word"); - var peg$e103 = peg$classExpectation([" ", ",", "\t", "\n", "\r", "\xA0"], true, false); - var peg$e104 = peg$otherExpectation("whitespace"); - var peg$e105 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], false, false); - var peg$e106 = peg$otherExpectation("quote"); - var peg$e107 = peg$classExpectation([" ", ",", "\"", "\u201D", "\u201C", "\t", "\n", "\r", "\xA0"], true, false); - var peg$e108 = peg$classExpectation(["\"", ["\u201C", "\u201D"]], false, false); - var peg$e109 = peg$classExpectation(["\"", "\u201D", "\u201C", "\r", "\n"], true, false); - var peg$e110 = peg$literalExpectation("\u201C", false); - var peg$e111 = peg$literalExpectation("\u201D", false); - var peg$e112 = peg$literalExpectation("\"", false); - var peg$e113 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"], ["0", "9"]], false, false); - var peg$e114 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false); - var peg$e115 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0"], false, false); - var peg$e116 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"]], false, false); - var peg$e117 = peg$classExpectation([","], false, false); - var peg$e118 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ","], false, false); + var peg$e95 = peg$literalExpectation("group-week", true); + var peg$e96 = peg$otherExpectation("operator"); + var peg$e97 = peg$classExpectation([":", "="], false, false); + var peg$e98 = peg$literalExpectation("!=", false); + var peg$e99 = peg$literalExpectation(">=", false); + var peg$e100 = peg$literalExpectation(">", false); + var peg$e101 = peg$literalExpectation("<=", false); + var peg$e102 = peg$literalExpectation("<", false); + var peg$e103 = peg$otherExpectation("word"); + var peg$e104 = peg$classExpectation([" ", ",", "\t", "\n", "\r", "\xA0"], true, false); + var peg$e105 = peg$otherExpectation("whitespace"); + var peg$e106 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], false, false); + var peg$e107 = peg$otherExpectation("quote"); + var peg$e108 = peg$classExpectation([" ", ",", "\"", "\u201D", "\u201C", "\t", "\n", "\r", "\xA0"], true, false); + var peg$e109 = peg$classExpectation(["\"", ["\u201C", "\u201D"]], false, false); + var peg$e110 = peg$classExpectation(["\"", "\u201D", "\u201C", "\r", "\n"], true, false); + var peg$e111 = peg$literalExpectation("\u201C", false); + var peg$e112 = peg$literalExpectation("\u201D", false); + var peg$e113 = peg$literalExpectation("\"", false); + var peg$e114 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"], ["0", "9"]], false, false); + var peg$e115 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false); + var peg$e116 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0"], false, false); + var peg$e117 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"]], false, false); + var peg$e118 = peg$classExpectation([","], false, false); + var peg$e119 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ","], false, false); var peg$f0 = function(filters) { return applyDefaults(filters); }; var peg$f1 = function(head, tail) { @@ -581,32 +583,33 @@ function peg$parse(input, options) { var peg$f80 = function() { return "groupTag"; }; var peg$f81 = function() { return "groupMerchant"; }; var peg$f82 = function() { return "groupMonth"; }; - var peg$f83 = function() { return "eq"; }; - var peg$f84 = function() { return "neq"; }; - var peg$f85 = function() { return "gte"; }; - var peg$f86 = function() { return "gt"; }; - var peg$f87 = function() { return "lte"; }; - var peg$f88 = function() { return "lt"; }; - var peg$f89 = function(o) { + var peg$f83 = function() { return "groupWeek"; }; + var peg$f84 = function() { return "eq"; }; + var peg$f85 = function() { return "neq"; }; + var peg$f86 = function() { return "gte"; }; + var peg$f87 = function() { return "gt"; }; + var peg$f88 = function() { return "lte"; }; + var peg$f89 = function() { return "lt"; }; + var peg$f90 = function(o) { if (nameOperator) { expectingNestedQuote = (o === "eq"); // Use simple parser if no valid operator is found } isColumnsContext = false; return o; }; - var peg$f90 = function(chars) { return chars.join("").trim(); }; - var peg$f91 = function() { + var peg$f91 = function(chars) { return chars.join("").trim(); }; + var peg$f92 = function() { isColumnsContext = false; return "and"; }; - var peg$f92 = function() { return expectingNestedQuote; }; - var peg$f93 = function(start, inner, end) { //handle no-breaking space + var peg$f93 = function() { return expectingNestedQuote; }; + var peg$f94 = function(start, inner, end) { //handle no-breaking space return [...start, '"', ...inner, '"', ...end].join(""); }; - var peg$f94 = function(start) {return "“"}; - var peg$f95 = function(start) {return "”"}; - var peg$f96 = function(start) {return "\""}; - var peg$f97 = function(start, inner, end) { + var peg$f95 = function(start) {return "“"}; + var peg$f96 = function(start) {return "”"}; + var peg$f97 = function(start) {return "\""}; + var peg$f98 = function(start, inner, end) { return [...start, '"', ...inner, '"'].join(""); }; var peg$currPos = options.peg$currPos | 0; @@ -2573,6 +2576,9 @@ function peg$parse(input, options) { s0 = peg$parsegroupMerchant(); if (s0 === peg$FAILED) { s0 = peg$parsegroupMonth(); + if (s0 === peg$FAILED) { + s0 = peg$parsegroupWeek(); + } } } } @@ -3571,6 +3577,43 @@ function peg$parse(input, options) { return s0; } + function peg$parsegroupWeek() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = input.substr(peg$currPos, 10); + if (s1.toLowerCase() === peg$c90) { + peg$currPos += 10; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e95); } + } + if (s1 !== peg$FAILED) { + s2 = peg$currPos; + peg$silentFails++; + s3 = peg$parsewordBoundary(); + peg$silentFails--; + if (s3 !== peg$FAILED) { + peg$currPos = s2; + s2 = undefined; + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f83(); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + function peg$parseoperator() { var s0, s1; @@ -3581,81 +3624,81 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e96); } + if (peg$silentFails === 0) { peg$fail(peg$e97); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f83(); + s1 = peg$f84(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c90) { - s1 = peg$c90; + if (input.substr(peg$currPos, 2) === peg$c91) { + s1 = peg$c91; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e97); } + if (peg$silentFails === 0) { peg$fail(peg$e98); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f84(); + s1 = peg$f85(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c91) { - s1 = peg$c91; + if (input.substr(peg$currPos, 2) === peg$c92) { + s1 = peg$c92; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e98); } + if (peg$silentFails === 0) { peg$fail(peg$e99); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f85(); + s1 = peg$f86(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 62) { - s1 = peg$c92; + s1 = peg$c93; peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e99); } + if (peg$silentFails === 0) { peg$fail(peg$e100); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f86(); + s1 = peg$f87(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c93) { - s1 = peg$c93; + if (input.substr(peg$currPos, 2) === peg$c94) { + s1 = peg$c94; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e100); } + if (peg$silentFails === 0) { peg$fail(peg$e101); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f87(); + s1 = peg$f88(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 60) { - s1 = peg$c94; + s1 = peg$c95; peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e101); } + if (peg$silentFails === 0) { peg$fail(peg$e102); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f88(); + s1 = peg$f89(); } s0 = s1; } @@ -3666,7 +3709,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e95); } + if (peg$silentFails === 0) { peg$fail(peg$e96); } } return s0; @@ -3679,7 +3722,7 @@ function peg$parse(input, options) { s1 = peg$parseoperator(); if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f89(s1); + s1 = peg$f90(s1); } s0 = s1; @@ -3697,7 +3740,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e103); } + if (peg$silentFails === 0) { peg$fail(peg$e104); } } if (s2 !== peg$FAILED) { while (s2 !== peg$FAILED) { @@ -3707,7 +3750,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e103); } + if (peg$silentFails === 0) { peg$fail(peg$e104); } } } } else { @@ -3715,13 +3758,13 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f90(s1); + s1 = peg$f91(s1); } s0 = s1; peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e102); } + if (peg$silentFails === 0) { peg$fail(peg$e103); } } return s0; @@ -3733,7 +3776,7 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = peg$parse_(); peg$savedPos = s0; - s1 = peg$f91(); + s1 = peg$f92(); s0 = s1; return s0; @@ -3749,7 +3792,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e105); } + if (peg$silentFails === 0) { peg$fail(peg$e106); } } while (s1 !== peg$FAILED) { s0.push(s1); @@ -3758,12 +3801,12 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e105); } + if (peg$silentFails === 0) { peg$fail(peg$e106); } } } peg$silentFails--; s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e104); } + if (peg$silentFails === 0) { peg$fail(peg$e105); } return s0; } @@ -3773,7 +3816,7 @@ function peg$parse(input, options) { s0 = peg$currPos; peg$savedPos = peg$currPos; - s1 = peg$f92(); + s1 = peg$f93(); if (s1) { s1 = undefined; } else { @@ -3810,7 +3853,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e107); } + if (peg$silentFails === 0) { peg$fail(peg$e108); } } while (s2 !== peg$FAILED) { s1.push(s2); @@ -3819,7 +3862,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e107); } + if (peg$silentFails === 0) { peg$fail(peg$e108); } } } s2 = input.charAt(peg$currPos); @@ -3827,7 +3870,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e108); } + if (peg$silentFails === 0) { peg$fail(peg$e109); } } if (s2 !== peg$FAILED) { s3 = []; @@ -3836,7 +3879,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e109); } + if (peg$silentFails === 0) { peg$fail(peg$e110); } } while (s4 !== peg$FAILED) { s3.push(s4); @@ -3845,7 +3888,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e109); } + if (peg$silentFails === 0) { peg$fail(peg$e110); } } } s4 = input.charAt(peg$currPos); @@ -3853,7 +3896,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e108); } + if (peg$silentFails === 0) { peg$fail(peg$e109); } } if (s4 !== peg$FAILED) { s5 = []; @@ -3862,7 +3905,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e103); } + if (peg$silentFails === 0) { peg$fail(peg$e104); } } while (s6 !== peg$FAILED) { s5.push(s6); @@ -3871,11 +3914,11 @@ function peg$parse(input, options) { peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e103); } + if (peg$silentFails === 0) { peg$fail(peg$e104); } } } peg$savedPos = s0; - s0 = peg$f93(s1, s3, s5); + s0 = peg$f94(s1, s3, s5); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3887,7 +3930,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e106); } + if (peg$silentFails === 0) { peg$fail(peg$e107); } } return s0; @@ -3904,7 +3947,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e107); } + if (peg$silentFails === 0) { peg$fail(peg$e108); } } while (s2 !== peg$FAILED) { s1.push(s2); @@ -3913,7 +3956,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e107); } + if (peg$silentFails === 0) { peg$fail(peg$e108); } } } s2 = input.charAt(peg$currPos); @@ -3921,7 +3964,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e108); } + if (peg$silentFails === 0) { peg$fail(peg$e109); } } if (s2 !== peg$FAILED) { s3 = []; @@ -3930,7 +3973,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e109); } + if (peg$silentFails === 0) { peg$fail(peg$e110); } } if (s4 === peg$FAILED) { s4 = peg$currPos; @@ -3946,15 +3989,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 8220) { - s6 = peg$c95; + s6 = peg$c96; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e110); } + if (peg$silentFails === 0) { peg$fail(peg$e111); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f94(s1); + s4 = peg$f95(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -3977,15 +4020,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 8221) { - s6 = peg$c96; + s6 = peg$c97; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e111); } + if (peg$silentFails === 0) { peg$fail(peg$e112); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f95(s1); + s4 = peg$f96(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -4008,15 +4051,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 34) { - s6 = peg$c97; + s6 = peg$c98; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e112); } + if (peg$silentFails === 0) { peg$fail(peg$e113); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f96(s1); + s4 = peg$f97(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -4035,7 +4078,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e109); } + if (peg$silentFails === 0) { peg$fail(peg$e110); } } if (s4 === peg$FAILED) { s4 = peg$currPos; @@ -4051,15 +4094,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 8220) { - s6 = peg$c95; + s6 = peg$c96; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e110); } + if (peg$silentFails === 0) { peg$fail(peg$e111); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f94(s1); + s4 = peg$f95(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -4082,15 +4125,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 8221) { - s6 = peg$c96; + s6 = peg$c97; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e111); } + if (peg$silentFails === 0) { peg$fail(peg$e112); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f95(s1); + s4 = peg$f96(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -4113,15 +4156,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 34) { - s6 = peg$c97; + s6 = peg$c98; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e112); } + if (peg$silentFails === 0) { peg$fail(peg$e113); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f96(s1); + s4 = peg$f97(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -4137,7 +4180,7 @@ function peg$parse(input, options) { s4 = peg$parseclosingQuote(); if (s4 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f97(s1, s3, s4); + s0 = peg$f98(s1, s3, s4); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -4149,7 +4192,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e106); } + if (peg$silentFails === 0) { peg$fail(peg$e107); } } return s0; @@ -4164,7 +4207,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e108); } + if (peg$silentFails === 0) { peg$fail(peg$e109); } } if (s1 !== peg$FAILED) { s2 = peg$currPos; @@ -4202,7 +4245,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e113); } + if (peg$silentFails === 0) { peg$fail(peg$e114); } } while (s2 !== peg$FAILED) { s1.push(s2); @@ -4211,7 +4254,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e113); } + if (peg$silentFails === 0) { peg$fail(peg$e114); } } } s2 = []; @@ -4220,7 +4263,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e114); } + if (peg$silentFails === 0) { peg$fail(peg$e115); } } while (s3 !== peg$FAILED) { s2.push(s3); @@ -4229,7 +4272,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e114); } + if (peg$silentFails === 0) { peg$fail(peg$e115); } } } s3 = []; @@ -4238,7 +4281,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e115); } + if (peg$silentFails === 0) { peg$fail(peg$e116); } } while (s4 !== peg$FAILED) { s3.push(s4); @@ -4247,7 +4290,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e115); } + if (peg$silentFails === 0) { peg$fail(peg$e116); } } } s4 = peg$parseoperator(); @@ -4266,7 +4309,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e116); } + if (peg$silentFails === 0) { peg$fail(peg$e117); } } while (s2 !== peg$FAILED) { s1.push(s2); @@ -4275,7 +4318,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e116); } + if (peg$silentFails === 0) { peg$fail(peg$e117); } } } s2 = peg$currPos; @@ -4322,7 +4365,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e117); } + if (peg$silentFails === 0) { peg$fail(peg$e118); } } } } @@ -4338,7 +4381,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e118); } + if (peg$silentFails === 0) { peg$fail(peg$e119); } } if (s0 === peg$FAILED) { s0 = peg$currPos; @@ -4371,6 +4414,7 @@ function peg$parse(input, options) { tag: "tag", merchant: "groupMerchant", month: "groupmonth", + week: "groupweek", }; const GROUP_BY_DEFAULT_SORT_ORDER = { @@ -4380,7 +4424,8 @@ function peg$parse(input, options) { category: "asc", tag: "asc", merchant: "asc", - month: "desc" + month: "desc", + week: "desc", }; const DEFAULT_SORT_BY_VALUES = new Set([...Object.values(GROUP_BY_DEFAULT_SORT), "date"]); diff --git a/src/libs/SearchParser/searchParser.peggy b/src/libs/SearchParser/searchParser.peggy index 90e200051b8e0..c09a6d97c0add 100644 --- a/src/libs/SearchParser/searchParser.peggy +++ b/src/libs/SearchParser/searchParser.peggy @@ -36,6 +36,7 @@ tag: "tag", merchant: "groupMerchant", month: "groupmonth", + week: "groupweek", }; const GROUP_BY_DEFAULT_SORT_ORDER = { @@ -45,7 +46,8 @@ category: "asc", tag: "asc", merchant: "asc", - month: "desc" + month: "desc", + week: "desc", }; const DEFAULT_SORT_BY_VALUES = new Set([...Object.values(GROUP_BY_DEFAULT_SORT), "date"]); diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 871e3ebabaed0..bc5341c227ff3 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ // TODO: Remove this disable once SearchUIUtils is refactored (see dedicated refactor issue) -import {format} from 'date-fns'; +import {endOfMonth, format, startOfMonth, startOfYear, subMonths} from 'date-fns'; import type {TextStyle, ViewStyle} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; @@ -11,6 +11,7 @@ import type {MenuItemWithLink} from '@components/MenuItemList'; import type {MultiSelectItem} from '@components/Search/FilterDropdowns/MultiSelectPopup'; import type {SingleSelectItem} from '@components/Search/FilterDropdowns/SingleSelectPopup'; import type { + QueryFilters, SearchAction, SearchColumnType, SearchCustomColumnIds, @@ -44,6 +45,7 @@ import type { TransactionMonthGroupListItemType, TransactionReportGroupListItemType, TransactionTagGroupListItemType, + TransactionWeekGroupListItemType, TransactionWithdrawalIDGroupListItemType, } from '@components/SelectionListWithSections/types'; import type {ThemeColors} from '@styles/theme/types'; @@ -162,6 +164,7 @@ type TransactionCategoryGroupSorting = ColumnSortMapping; type TransactionTagGroupSorting = ColumnSortMapping; type TransactionMonthGroupSorting = ColumnSortMapping; +type TransactionWeekGroupSorting = ColumnSortMapping; type GetReportSectionsParams = { data: OnyxTypes.SearchResults['data']; @@ -275,6 +278,11 @@ const transactionMonthGroupColumnNamesToSortingProperty: TransactionMonthGroupSo ...transactionGroupBaseSortingProperties, }; +const transactionWeekGroupColumnNamesToSortingProperty: TransactionWeekGroupSorting = { + [CONST.SEARCH.TABLE_COLUMNS.GROUP_WEEK]: 'week' as const, + ...transactionGroupBaseSortingProperties, +}; + const expenseStatusActionMapping = { [CONST.SEARCH.STATUS.EXPENSE.DRAFTS]: (expenseReport?: OnyxTypes.Report) => expenseReport?.stateNum === CONST.REPORT.STATE_NUM.OPEN && expenseReport.statusNum === CONST.REPORT.STATUS_NUM.OPEN, @@ -984,6 +992,10 @@ function isTransactionMonthGroupListItemType(item: ListItem): item is Transactio return isTransactionGroupListItemType(item) && 'groupedBy' in item && item.groupedBy === CONST.SEARCH.GROUP_BY.MONTH; } +function isTransactionWeekGroupListItemType(item: ListItem): item is TransactionWeekGroupListItemType { + return isTransactionGroupListItemType(item) && 'groupedBy' in item && item.groupedBy === CONST.SEARCH.GROUP_BY.WEEK; +} + /** * Type guard that checks if something is a TransactionListItemType */ @@ -1390,6 +1402,7 @@ function getTransactionsSections( isActionLoadingSet: ReadonlySet | undefined, bankAccountList: OnyxEntry, reportActions: Record = {}, + queryJSON?: SearchQueryJSON, ): [TransactionListItemType[], number] { const shouldShowMerchant = getShouldShowMerchant(data); const {shouldShowYearCreated, shouldShowYearSubmitted, shouldShowYearApproved, shouldShowYearPosted, shouldShowYearExported} = shouldShowYear(data); @@ -1408,7 +1421,8 @@ function getTransactionsSections( const lastExportedActionByReportID = buildLastExportedActionByReportIDMap(data); - const queryJSON = getCurrentSearchQueryJSON(); + // Use the provided queryJSON if available, otherwise fall back to getCurrentSearchQueryJSON() + const currentQueryJSON = queryJSON ?? getCurrentSearchQueryJSON(); for (const key of transactionKeys) { const transactionItem = data[key]; @@ -1417,9 +1431,9 @@ function getTransactionsSections( let shouldShow = true; const isActionLoading = isActionLoadingSet?.has(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${transactionItem.reportID}`); - if (queryJSON && !isActionLoading) { - if (queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE) { - const status = queryJSON.status; + if (currentQueryJSON && !isActionLoading) { + if (currentQueryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE) { + const status = currentQueryJSON.status; if (Array.isArray(status)) { shouldShow = status.some((expenseStatus) => { return isValidExpenseStatus(expenseStatus) ? expenseStatusActionMapping[expenseStatus](report) : false; @@ -2416,6 +2430,53 @@ function getMonthSections(data: OnyxTypes.SearchResults['data'], queryJSON: Sear return [monthSectionsValues, monthSectionsValues.length]; } +/** + * Returns sections for week-grouped search results. + * Do not use directly, use only via `getSections()` facade. + */ +function getWeekSections(data: OnyxTypes.SearchResults['data'], queryJSON: SearchQueryJSON | undefined): [TransactionWeekGroupListItemType[], number] { + const weekSections: Record = {}; + for (const key in data) { + if (isGroupEntry(key)) { + const weekGroup = data[key]; + // Check if this is a week group by checking for week property + if (!('week' in weekGroup)) { + continue; + } + let transactionsQueryJSON: SearchQueryJSON | undefined; + const {start: weekStart, end: weekEnd} = adjustTimeRangeToDateFilters( + DateUtils.getWeekDateRange(weekGroup.week), + queryJSON?.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE), + ); + if (queryJSON && weekGroup.week) { + const newFlatFilters = queryJSON.flatFilters.filter((filter) => filter.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); + newFlatFilters.push({ + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + filters: [ + {operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN_OR_EQUAL_TO, value: weekStart}, + {operator: CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO, value: weekEnd}, + ], + }); + const newQueryJSON: SearchQueryJSON = {...queryJSON, groupBy: undefined, flatFilters: newFlatFilters}; + const newQuery = buildSearchQueryString(newQueryJSON); + transactionsQueryJSON = buildSearchQueryJSON(newQuery); + } + const formattedWeek = DateUtils.getFormattedDateRangeForSearch(weekStart, weekEnd); + + weekSections[key] = { + groupedBy: CONST.SEARCH.GROUP_BY.WEEK, + transactions: [], + transactionsQueryJSON, + ...weekGroup, + formattedWeek, + }; + } + } + + const weekSectionsValues = Object.values(weekSections); + return [weekSectionsValues, weekSectionsValues.length]; +} + /** * Returns the appropriate list item component based on the type and status of the search data. */ @@ -2497,10 +2558,12 @@ function getSections({ return getTagSections(data, queryJSON, translate); case CONST.SEARCH.GROUP_BY.MONTH: return getMonthSections(data, queryJSON); + case CONST.SEARCH.GROUP_BY.WEEK: + return getWeekSections(data, queryJSON); } } - return getTransactionsSections(data, currentSearch, currentAccountID, currentUserEmail, formatPhoneNumber, isActionLoadingSet, bankAccountList, reportActions); + return getTransactionsSections(data, currentSearch, currentAccountID, currentUserEmail, formatPhoneNumber, isActionLoadingSet, bankAccountList, reportActions, queryJSON); } /** @@ -2544,6 +2607,8 @@ function getSortedSections( return getSortedTagData(data as TransactionTagGroupListItemType[], localeCompare, sortBy, sortOrder); case CONST.SEARCH.GROUP_BY.MONTH: return getSortedMonthData(data as TransactionMonthGroupListItemType[], localeCompare, sortBy, sortOrder); + case CONST.SEARCH.GROUP_BY.WEEK: + return getSortedWeekData(data as TransactionWeekGroupListItemType[], localeCompare, sortBy, sortOrder); } } @@ -2946,6 +3011,13 @@ function getSortedMonthData(data: TransactionMonthGroupListItemType[], localeCom return getSortedData(data, localeCompare, transactionMonthGroupColumnNamesToSortingProperty, (a, b) => a.sortKey - b.sortKey, sortBy, sortOrder); } +/** + * Sorts week group data based on a specified column and sort order. + */ +function getSortedWeekData(data: TransactionWeekGroupListItemType[], localeCompare: LocaleContextProps['localeCompare'], sortBy?: SearchColumnType, sortOrder?: SortOrder) { + return getSortedData(data, localeCompare, transactionWeekGroupColumnNamesToSortingProperty, (a, b) => localeCompare(a.week, b.week), sortBy, sortOrder); +} + /** * @private * Sorts report actions sections based on a specified column and sort order. @@ -3005,6 +3077,8 @@ function getCustomColumns(value?: SearchDataTypes | SearchGroupBy): SearchCustom return Object.values(CONST.SEARCH.GROUP_CUSTOM_COLUMNS.TAG); case CONST.SEARCH.GROUP_BY.MONTH: return Object.values(CONST.SEARCH.GROUP_CUSTOM_COLUMNS.MONTH); + case CONST.SEARCH.GROUP_BY.WEEK: + return Object.values(CONST.SEARCH.GROUP_CUSTOM_COLUMNS.WEEK); default: return []; } @@ -3038,6 +3112,8 @@ function getCustomColumnDefault(value?: SearchDataTypes | SearchGroupBy): Search return CONST.SEARCH.GROUP_DEFAULT_COLUMNS.TAG; case CONST.SEARCH.GROUP_BY.MONTH: return CONST.SEARCH.GROUP_DEFAULT_COLUMNS.MONTH; + case CONST.SEARCH.GROUP_BY.WEEK: + return CONST.SEARCH.GROUP_DEFAULT_COLUMNS.WEEK; default: return []; } @@ -3118,6 +3194,8 @@ function getSearchColumnTranslationKey(columnId: SearchCustomColumnIds): Transla return 'common.withdrawalID'; case CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH: return 'common.month'; + case CONST.SEARCH.TABLE_COLUMNS.GROUP_WEEK: + return 'common.week'; case CONST.SEARCH.TABLE_COLUMNS.GROUP_WITHDRAWN: return 'search.filters.withdrawn'; case CONST.SEARCH.TABLE_COLUMNS.GROUP_FEED: @@ -3608,6 +3686,7 @@ function getColumnsToShow( [CONST.SEARCH.GROUP_BY.MERCHANT]: CONST.SEARCH.GROUP_CUSTOM_COLUMNS.MERCHANT, [CONST.SEARCH.GROUP_BY.TAG]: CONST.SEARCH.GROUP_CUSTOM_COLUMNS.TAG, [CONST.SEARCH.GROUP_BY.MONTH]: CONST.SEARCH.GROUP_CUSTOM_COLUMNS.MONTH, + [CONST.SEARCH.GROUP_BY.WEEK]: CONST.SEARCH.GROUP_CUSTOM_COLUMNS.WEEK, }[groupBy]; const defaultCustomColumns = { @@ -3618,22 +3697,23 @@ function getColumnsToShow( [CONST.SEARCH.GROUP_BY.MERCHANT]: CONST.SEARCH.GROUP_DEFAULT_COLUMNS.MERCHANT, [CONST.SEARCH.GROUP_BY.TAG]: CONST.SEARCH.GROUP_DEFAULT_COLUMNS.TAG, [CONST.SEARCH.GROUP_BY.MONTH]: CONST.SEARCH.GROUP_DEFAULT_COLUMNS.MONTH, + [CONST.SEARCH.GROUP_BY.WEEK]: CONST.SEARCH.GROUP_DEFAULT_COLUMNS.WEEK, }[groupBy]; - const filteredVisibleColumns = visibleColumns.filter((column) => Object.values(customColumns).includes(column as ValueOf)); - const columnsToShow = filteredVisibleColumns.length ? filteredVisibleColumns : defaultCustomColumns; + const filteredVisibleColumns = customColumns ? visibleColumns.filter((column) => Object.values(customColumns).includes(column as ValueOf)) : []; + const columnsToShow: SearchColumnType[] = filteredVisibleColumns.length ? filteredVisibleColumns : (defaultCustomColumns ?? []); if (groupBy === CONST.SEARCH.GROUP_BY.FROM) { const requiredColumns = new Set([CONST.SEARCH.TABLE_COLUMNS.AVATAR, CONST.SEARCH.TABLE_COLUMNS.GROUP_FROM]); const result: SearchColumnType[] = []; for (const col of requiredColumns) { - if (!columnsToShow.includes(col as SearchCustomColumnIds)) { + if (!columnsToShow.includes(col)) { result.push(col); } } - for (const col of columnsToShow) { + for (const col of columnsToShow ?? []) { result.push(col); } @@ -3645,12 +3725,12 @@ function getColumnsToShow( const result: SearchColumnType[] = []; for (const col of requiredColumns) { - if (!columnsToShow.includes(col as SearchCustomColumnIds)) { + if (!columnsToShow.includes(col)) { result.push(col); } } - for (const col of columnsToShow) { + for (const col of columnsToShow ?? []) { result.push(col); } @@ -3662,12 +3742,12 @@ function getColumnsToShow( const result: SearchColumnType[] = []; for (const col of requiredColumns) { - if (!columnsToShow.includes(col as SearchCustomColumnIds)) { + if (!columnsToShow.includes(col)) { result.push(col); } } - for (const col of columnsToShow) { + for (const col of columnsToShow ?? []) { result.push(col); } @@ -3679,12 +3759,12 @@ function getColumnsToShow( const result: SearchColumnType[] = []; for (const col of requiredColumns) { - if (!columnsToShow.includes(col as SearchCustomColumnIds)) { + if (!columnsToShow.includes(col)) { result.push(col); } } - for (const col of columnsToShow) { + for (const col of columnsToShow ?? []) { result.push(col); } @@ -3730,12 +3810,29 @@ function getColumnsToShow( const result: SearchColumnType[] = []; for (const col of requiredColumns) { - if (!columnsToShow.includes(col as SearchCustomColumnIds)) { + if (!columnsToShow.includes(col)) { result.push(col); } } - for (const col of columnsToShow) { + for (const col of columnsToShow ?? []) { + result.push(col); + } + + return result; + } + + if (groupBy === CONST.SEARCH.GROUP_BY.WEEK) { + const requiredColumns = new Set([CONST.SEARCH.TABLE_COLUMNS.GROUP_WEEK]); + const result: SearchColumnType[] = []; + + for (const col of requiredColumns) { + if (!columnsToShow.includes(col)) { + result.push(col); + } + } + + for (const col of columnsToShow ?? []) { result.push(col); } @@ -4035,6 +4132,128 @@ function shouldShowDeleteOption(selectedTransactions: Record datePreset === value); +} + +function adjustTimeRangeToDateFilters(timeRange: {start: string; end: string}, dateFilter: QueryFilters[0] | undefined): {start: string; end: string} { + if (!dateFilter?.filters) { + return timeRange; + } + + const {start: timeRangeStart, end: timeRangeEnd} = timeRange; + const startLimitFilter = dateFilter.filters.find((filter) => filter.operator === CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN_OR_EQUAL_TO); + const endLimitFilter = dateFilter.filters.find((filter) => filter.operator === CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO); + const equalToFilter = dateFilter.filters.find((filter) => filter.operator === CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO); + + // Check if any filter value is a date preset and convert it to actual date range + let limitsStart: string | undefined; + let limitsEnd: string | undefined; + + // Handle GREATER_THAN_OR_EQUAL_TO operator + if (startLimitFilter?.value) { + const value = String(startLimitFilter.value); + if (isDatePreset(value)) { + const presetRange = getDateRangeForPreset(value); + limitsStart = presetRange.start || undefined; + } else { + limitsStart = value; + } + } + + // Handle LOWER_THAN_OR_EQUAL_TO operator + if (endLimitFilter?.value) { + const value = String(endLimitFilter.value); + if (isDatePreset(value)) { + const presetRange = getDateRangeForPreset(value); + limitsEnd = presetRange.end || undefined; + } else { + limitsEnd = value; + } + } + + // Handle EQUAL_TO operator (for presets like "this-month", "last-month", "year-to-date") + if (equalToFilter?.value) { + const value = String(equalToFilter.value); + if (isDatePreset(value)) { + const presetRange = getDateRangeForPreset(value); + if (presetRange.start && presetRange.end) { + // If we don't have start/end limits yet, use the preset range + if (!limitsStart) { + limitsStart = presetRange.start; + } + if (!limitsEnd) { + limitsEnd = presetRange.end; + } + // If we have both limits, use the intersection (max start, min end) + if (limitsStart && presetRange.start > limitsStart) { + limitsStart = presetRange.start; + } + if (limitsEnd && presetRange.end < limitsEnd) { + limitsEnd = presetRange.end; + } + } + } + } + + // Adjust the start date: use max(timeRangeStart, limitsStart) if limitsStart exists + // Dates are in YYYY-MM-DD format, so lexicographic comparison works correctly + let adjustedStart = timeRangeStart; + if (limitsStart && limitsStart > timeRangeStart) { + adjustedStart = limitsStart; + } + + // Adjust the end date: use min(timeRangeEnd, limitsEnd) if limitsEnd exists + let adjustedEnd = timeRangeEnd; + if (limitsEnd && limitsEnd < timeRangeEnd) { + adjustedEnd = limitsEnd; + } + + return { + start: adjustedStart, + end: adjustedEnd, + }; +} + export { getSuggestedSearches, getDefaultActionableSearchMenuItem, @@ -4052,6 +4271,7 @@ export { isTransactionMerchantGroupListItemType, isTransactionTagGroupListItemType, isTransactionMonthGroupListItemType, + isTransactionWeekGroupListItemType, isSearchResultsEmpty, isTransactionListItemType, isReportActionListItemType, @@ -4090,5 +4310,6 @@ export { shouldShowDeleteOption, getToFieldValueForTransaction, isTodoSearch, + adjustTimeRangeToDateFilters, }; export type {SavedSearchMenuItem, SearchTypeMenuSection, SearchTypeMenuItem, SearchDateModifier, SearchDateModifierLower, SearchKey, ArchivedReportsIDSet}; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index e3f1f221a33f0..eb0622bea9557 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1765,6 +1765,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ case CONST.SEARCH.TABLE_COLUMNS.CATEGORY: case CONST.SEARCH.TABLE_COLUMNS.GROUP_CATEGORY: case CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH: + case CONST.SEARCH.TABLE_COLUMNS.GROUP_WEEK: case CONST.SEARCH.TABLE_COLUMNS.TAG: columnWidth = {...getWidthStyle(variables.w36), ...styles.flex1}; break; diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index 1a7da00fcb4bc..27f1e521aaf35 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -235,6 +235,21 @@ type SearchMonthGroup = { currency: string; }; +/** Model of week grouped search result */ +type SearchWeekGroup = { + /** Week start date in YYYY-MM-DD format */ + week: string; + + /** Number of transactions */ + count: number; + + /** Total value of transactions */ + total: number; + + /** Currency of total value */ + currency: string; +}; + /** Model of search results */ type SearchResults = { /** Current search results state */ @@ -251,7 +266,7 @@ type SearchResults = { PrefixedRecord & PrefixedRecord< typeof CONST.SEARCH.GROUP_PREFIX, - SearchMemberGroup | SearchCardGroup | SearchWithdrawalIDGroup | SearchCategoryGroup | SearchMerchantGroup | SearchTagGroup | SearchMonthGroup + SearchMemberGroup | SearchCardGroup | SearchWithdrawalIDGroup | SearchCategoryGroup | SearchMerchantGroup | SearchTagGroup | SearchMonthGroup | SearchWeekGroup >; /** Whether search data is being fetched from server */ @@ -277,4 +292,5 @@ export type { SearchMerchantGroup, SearchTagGroup, SearchMonthGroup, + SearchWeekGroup, }; diff --git a/tests/ui/WeekListItemHeaderTest.tsx b/tests/ui/WeekListItemHeaderTest.tsx new file mode 100644 index 0000000000000..f02864ab3fb52 --- /dev/null +++ b/tests/ui/WeekListItemHeaderTest.tsx @@ -0,0 +1,319 @@ +import {act, fireEvent, render, screen} from '@testing-library/react-native'; +import React from 'react'; +import Onyx from 'react-native-onyx'; +import ComposeProviders from '@components/ComposeProviders'; +import {LocaleContextProvider} from '@components/LocaleContextProvider'; +import OnyxListItemProvider from '@components/OnyxListItemProvider'; +import {SearchContext} from '@components/Search/SearchContext'; +import type {SearchColumnType} from '@components/Search/types'; +import WeekListItemHeader from '@components/SelectionListWithSections/Search/WeekListItemHeader'; +import type {TransactionWeekGroupListItemType} from '@components/SelectionListWithSections/types'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; + +jest.mock('@components/ConfirmedRoute.tsx'); +jest.mock('@libs/Navigation/Navigation'); + +// Mock useResponsiveLayout to control screen size in tests +jest.mock('@hooks/useResponsiveLayout', () => jest.fn()); +const mockedUseResponsiveLayout = useResponsiveLayout as jest.MockedFunction; + +// Mock search context with all required SearchContextProps fields +const mockSearchContext = { + currentSearchHash: 12345, + currentSearchKey: undefined, + currentSearchQueryJSON: undefined, + currentSearchResults: undefined, + selectedReports: [], + selectedTransactionIDs: [], + selectedTransactions: {}, + isOnSearch: false, + shouldTurnOffSelectionMode: false, + shouldResetSearchQuery: false, + lastSearchType: undefined, + areAllMatchingItemsSelected: false, + showSelectAllMatchingItems: false, + shouldShowFiltersBarLoading: false, + shouldUseLiveData: false, + setLastSearchType: jest.fn(), + setCurrentSearchHashAndKey: jest.fn(), + setCurrentSearchQueryJSON: jest.fn(), + setSelectedTransactions: jest.fn(), + removeTransaction: jest.fn(), + clearSelectedTransactions: jest.fn(), + setShouldShowFiltersBarLoading: jest.fn(), + shouldShowSelectAllMatchingItems: jest.fn(), + selectAllMatchingItems: jest.fn(), + setShouldResetSearchQuery: jest.fn(), +}; + +const createWeekListItem = (week: string, options: Partial = {}): TransactionWeekGroupListItemType => ({ + week, + formattedWeek: options.formattedWeek ?? 'Jan 25 - Jan 31, 2026', + count: options.count ?? 5, + currency: options.currency ?? 'USD', + total: options.total ?? 250, + groupedBy: CONST.SEARCH.GROUP_BY.WEEK, + transactions: [], + transactionsQueryJSON: undefined, + keyForList: `week-${week}`, + ...options, +}); + +// Helper function to wrap component with context +const renderWeekListItemHeader = ( + weekItem: TransactionWeekGroupListItemType, + props: Partial<{ + onCheckboxPress: jest.Mock; + isDisabled: boolean; + canSelectMultiple: boolean; + isSelectAllChecked: boolean; + isIndeterminate: boolean; + onDownArrowClick: jest.Mock; + isExpanded: boolean; + columns: SearchColumnType[]; + }> = {}, +) => { + return render( + + + + + , + ); +}; + +describe('WeekListItemHeader', () => { + beforeAll(() => + Onyx.init({ + keys: ONYXKEYS, + evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], + }), + ); + + beforeEach(() => { + // Default to small screen (mobile) layout + mockedUseResponsiveLayout.mockReturnValue({ + isLargeScreenWidth: false, + shouldUseNarrowLayout: true, + isSmallScreenWidth: true, + isMediumScreenWidth: false, + isExtraSmallScreenWidth: false, + isExtraSmallScreenHeight: false, + isExtraLargeScreenWidth: false, + isSmallScreen: true, + isInNarrowPaneModal: false, + onboardingIsMediumOrLargerScreenWidth: false, + }); + }); + + afterEach(async () => { + await act(async () => { + await Onyx.clear(); + }); + jest.clearAllMocks(); + }); + + describe('Week display', () => { + it('should display the formatted week', async () => { + const weekItem = createWeekListItem('2026-01-25', {formattedWeek: 'Jan 25 - Jan 31, 2026'}); + renderWeekListItemHeader(weekItem); + await waitForBatchedUpdatesWithAct(); + + expect(screen.getByText('Jan 25 - Jan 31, 2026')).toBeOnTheScreen(); + }); + + it('should display different weeks correctly', async () => { + const weekItem = createWeekListItem('2025-12-28', {formattedWeek: 'Dec 28, 2025 - Jan 3, 2026'}); + renderWeekListItemHeader(weekItem); + await waitForBatchedUpdatesWithAct(); + + expect(screen.getByText('Dec 28, 2025 - Jan 3, 2026')).toBeOnTheScreen(); + }); + + it('should display week with different year', async () => { + const weekItem = createWeekListItem('2024-06-15', {formattedWeek: 'Jun 15 - Jun 21, 2024'}); + renderWeekListItemHeader(weekItem); + await waitForBatchedUpdatesWithAct(); + + expect(screen.getByText('Jun 15 - Jun 21, 2024')).toBeOnTheScreen(); + }); + }); + + describe('Checkbox functionality', () => { + it('should render checkbox when canSelectMultiple is true', async () => { + const weekItem = createWeekListItem('2026-01-25'); + renderWeekListItemHeader(weekItem, {canSelectMultiple: true}); + await waitForBatchedUpdatesWithAct(); + + expect(screen.getByRole('checkbox')).toBeOnTheScreen(); + }); + + it('should not render checkbox when canSelectMultiple is false', async () => { + const weekItem = createWeekListItem('2026-01-25'); + renderWeekListItemHeader(weekItem, {canSelectMultiple: false}); + await waitForBatchedUpdatesWithAct(); + + expect(screen.queryByRole('checkbox')).not.toBeOnTheScreen(); + }); + + it('should call onCheckboxPress when checkbox is pressed', async () => { + const onCheckboxPress = jest.fn(); + const weekItem = createWeekListItem('2026-01-25'); + renderWeekListItemHeader(weekItem, {canSelectMultiple: true, onCheckboxPress}); + await waitForBatchedUpdatesWithAct(); + + const checkbox = screen.getByRole('checkbox'); + fireEvent.press(checkbox); + + expect(onCheckboxPress).toHaveBeenCalledWith(weekItem); + }); + + it('should show checkbox as checked when isSelectAllChecked is true', async () => { + const weekItem = createWeekListItem('2026-01-25'); + renderWeekListItemHeader(weekItem, {canSelectMultiple: true, isSelectAllChecked: true}); + await waitForBatchedUpdatesWithAct(); + + expect(screen.getByRole('checkbox')).toBeChecked(); + }); + }); + + describe('Total and count display', () => { + it('should display the total amount', async () => { + const weekItem = createWeekListItem('2026-01-25', {total: 50000, currency: 'USD'}); + renderWeekListItemHeader(weekItem); + await waitForBatchedUpdatesWithAct(); + + // TotalCell formats the amount, so we check for the formatted version + // $500.00 is 50000 cents + expect(screen.getByText('$500.00')).toBeOnTheScreen(); + }); + + it('should display the total amount with different currencies', async () => { + const weekItem = createWeekListItem('2026-01-25', {total: 10000, currency: 'EUR'}); + renderWeekListItemHeader(weekItem); + await waitForBatchedUpdatesWithAct(); + + // Should display EUR formatted amount + expect(screen.getByTestId('TotalCell')).toBeOnTheScreen(); + }); + }); + + describe('Disabled state', () => { + it('should render checkbox with disabled styling when isDisabled is true', async () => { + const onCheckboxPress = jest.fn(); + const weekItem = createWeekListItem('2026-01-25'); + renderWeekListItemHeader(weekItem, {canSelectMultiple: true, isDisabled: true, onCheckboxPress}); + await waitForBatchedUpdatesWithAct(); + + const checkbox = screen.getByRole('checkbox'); + // The checkbox should still be rendered + expect(checkbox).toBeOnTheScreen(); + }); + + it('should render checkbox with disabled styling when isDisabledCheckbox is true on week item', async () => { + const onCheckboxPress = jest.fn(); + const weekItem = createWeekListItem('2026-01-25', {isDisabledCheckbox: true}); + renderWeekListItemHeader(weekItem, {canSelectMultiple: true, onCheckboxPress}); + await waitForBatchedUpdatesWithAct(); + + const checkbox = screen.getByRole('checkbox'); + // The checkbox should still be rendered + expect(checkbox).toBeOnTheScreen(); + }); + }); + + describe('Large screen layout', () => { + beforeEach(() => { + mockedUseResponsiveLayout.mockReturnValue({ + isLargeScreenWidth: true, + shouldUseNarrowLayout: false, + isSmallScreenWidth: false, + isMediumScreenWidth: false, + isExtraSmallScreenWidth: false, + isExtraSmallScreenHeight: false, + isExtraLargeScreenWidth: true, + isSmallScreen: false, + isInNarrowPaneModal: false, + onboardingIsMediumOrLargerScreenWidth: true, + }); + }); + + it('should render column components on large screen', async () => { + const weekItem = createWeekListItem('2026-01-25', {count: 5, total: 25000}); + renderWeekListItemHeader(weekItem, { + columns: [CONST.SEARCH.TABLE_COLUMNS.GROUP_WEEK, CONST.SEARCH.TABLE_COLUMNS.GROUP_EXPENSES, CONST.SEARCH.TABLE_COLUMNS.GROUP_TOTAL], + }); + await waitForBatchedUpdatesWithAct(); + + // Should display week name, expense count, and total + expect(screen.getByText('Jan 25 - Jan 31, 2026')).toBeOnTheScreen(); + expect(screen.getByText('5')).toBeOnTheScreen(); + expect(screen.getByText('$250.00')).toBeOnTheScreen(); + }); + + it('should render checkbox on large screen when canSelectMultiple is true', async () => { + const weekItem = createWeekListItem('2026-01-25'); + renderWeekListItemHeader(weekItem, {canSelectMultiple: true}); + await waitForBatchedUpdatesWithAct(); + + expect(screen.getByRole('checkbox')).toBeOnTheScreen(); + }); + }); + + describe('Expand/Collapse functionality', () => { + it('should render expand/collapse button when onDownArrowClick is provided', async () => { + const onDownArrowClick = jest.fn(); + const weekItem = createWeekListItem('2026-01-25'); + renderWeekListItemHeader(weekItem, {onDownArrowClick, isExpanded: false}); + await waitForBatchedUpdatesWithAct(); + + // The expand/collapse button should be rendered with "Expand" label when not expanded + const expandButton = screen.getByLabelText('Expand'); + expect(expandButton).toBeOnTheScreen(); + }); + + it('should call onDownArrowClick when expand/collapse button is pressed', async () => { + const onDownArrowClick = jest.fn(); + const weekItem = createWeekListItem('2026-01-25'); + renderWeekListItemHeader(weekItem, {onDownArrowClick, isExpanded: false}); + await waitForBatchedUpdatesWithAct(); + + const expandButton = screen.getByLabelText('Expand'); + fireEvent.press(expandButton); + + expect(onDownArrowClick).toHaveBeenCalled(); + }); + + it('should show "Collapse" label when isExpanded is true', async () => { + const onDownArrowClick = jest.fn(); + const weekItem = createWeekListItem('2026-01-25'); + renderWeekListItemHeader(weekItem, {onDownArrowClick, isExpanded: true}); + await waitForBatchedUpdatesWithAct(); + + const collapseButton = screen.getByLabelText('Collapse'); + expect(collapseButton).toBeOnTheScreen(); + }); + + it('should not render expand/collapse button when onDownArrowClick is not provided', async () => { + const weekItem = createWeekListItem('2026-01-25'); + renderWeekListItemHeader(weekItem); + await waitForBatchedUpdatesWithAct(); + + expect(screen.queryByLabelText('Expand')).not.toBeOnTheScreen(); + expect(screen.queryByLabelText('Collapse')).not.toBeOnTheScreen(); + }); + }); +}); diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 823b3e45595d4..b00dabba72449 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -17,6 +17,7 @@ import type { TransactionMonthGroupListItemType, TransactionReportGroupListItemType, TransactionTagGroupListItemType, + TransactionWeekGroupListItemType, TransactionWithdrawalIDGroupListItemType, } from '@components/SelectionListWithSections/types'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -1863,6 +1864,62 @@ const transactionMerchantGroupListItemsSorted: TransactionMerchantGroupListItemT }, ]; +const searchResultsGroupByWeek: OnyxTypes.SearchResults = { + data: { + personalDetailsList: {}, + [`${CONST.SEARCH.GROUP_PREFIX}2026-01-25` as const]: { + week: '2026-01-25', + count: 5, + currency: 'USD', + total: 250, + }, + [`${CONST.SEARCH.GROUP_PREFIX}2025-12-28` as const]: { + week: '2025-12-28', + count: 3, + currency: 'USD', + total: 75, + }, + }, + search: { + count: 8, + currency: 'USD', + hasMoreResults: false, + hasResults: true, + offset: 0, + status: CONST.SEARCH.STATUS.EXPENSE.ALL, + total: 325, + isLoading: false, + type: 'expense', + }, +}; + +// Note: formattedWeek uses DateUtils.getFormattedDateRangeForSearch which returns a date range format +// For week starting 2026-01-25 (Sunday), it would format as "Jan 25 - Jan 31, 2026" (if same year) +// or "Jan 25, 2026 - Feb 1, 2026" (if different year) +// We'll use a placeholder that matches the actual format +const transactionWeekGroupListItems: TransactionWeekGroupListItemType[] = [ + { + week: '2026-01-25', + count: 5, + currency: 'USD', + total: 250, + groupedBy: CONST.SEARCH.GROUP_BY.WEEK, + formattedWeek: 'Jan 25 - Jan 31, 2026', + transactions: [], + transactionsQueryJSON: undefined, + }, + { + week: '2025-12-28', + count: 3, + currency: 'USD', + total: 75, + groupedBy: CONST.SEARCH.GROUP_BY.WEEK, + formattedWeek: 'Dec 28, 2025 - Jan 3, 2026', + transactions: [], + transactionsQueryJSON: undefined, + }, +]; + const searchResultsGroupByMonth: OnyxTypes.SearchResults = { data: { personalDetailsList: {}, @@ -2700,6 +2757,70 @@ describe('SearchUIUtils', () => { expect(SearchUIUtils.isTransactionMonthGroupListItemType(monthItem)).toBe(true); }); + it('should return isTransactionWeekGroupListItemType true for week group items', () => { + const weekItem: TransactionWeekGroupListItemType = { + week: '2026-01-25', + count: 5, + currency: 'USD', + total: 250, + groupedBy: CONST.SEARCH.GROUP_BY.WEEK, + formattedWeek: 'Jan 25 - Jan 31, 2026', + transactions: [], + transactionsQueryJSON: undefined, + }; + + expect(SearchUIUtils.isTransactionWeekGroupListItemType(weekItem)).toBe(true); + }); + + it('should return getWeekSections result when type is EXPENSE and groupBy is week', () => { + expect( + SearchUIUtils.getSections({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + data: searchResultsGroupByWeek.data, + currentAccountID: 2074551, + currentUserEmail: '', + translate: translateLocal, + formatPhoneNumber, + bankAccountList: {}, + groupBy: CONST.SEARCH.GROUP_BY.WEEK, + })[0], + ).toStrictEqual(transactionWeekGroupListItems); + }); + + it('should format week dates correctly', () => { + const dataWithDifferentWeeks: OnyxTypes.SearchResults['data'] = { + personalDetailsList: {}, + [`${CONST.SEARCH.GROUP_PREFIX}2026-01-25` as const]: { + week: '2026-01-25', + count: 2, + currency: 'USD', + total: 50, + }, + [`${CONST.SEARCH.GROUP_PREFIX}2026-06-15` as const]: { + week: '2026-06-15', + count: 1, + currency: 'USD', + total: 25, + }, + }; + + const [result] = SearchUIUtils.getSections({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + data: dataWithDifferentWeeks, + currentAccountID: 2074551, + currentUserEmail: '', + translate: translateLocal, + formatPhoneNumber, + bankAccountList: {}, + groupBy: CONST.SEARCH.GROUP_BY.WEEK, + }) as [TransactionWeekGroupListItemType[], number]; + + expect(result).toHaveLength(2); + // Check that formatted week contains the start date + expect(result.some((item) => item.formattedWeek.includes('Jan 25'))).toBe(true); + expect(result.some((item) => item.formattedWeek.includes('Jun 15'))).toBe(true); + }); + it('should return isTransactionCategoryGroupListItemType false for non-category group items', () => { const memberItem: TransactionMemberGroupListItemType = { accountID: 123,