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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7077,6 +7077,8 @@ const CONST = {
DATE: this.TABLE_COLUMNS.DATE,
SUBMITTED: this.TABLE_COLUMNS.SUBMITTED,
APPROVED: this.TABLE_COLUMNS.APPROVED,
FIRST_APPROVER: this.TABLE_COLUMNS.FIRST_APPROVER,
FIRST_APPROVED: this.TABLE_COLUMNS.FIRST_APPROVED,
EXPORTED: this.TABLE_COLUMNS.EXPORTED,
STATUS: this.TABLE_COLUMNS.STATUS,
TITLE: this.TABLE_COLUMNS.TITLE,
Expand Down Expand Up @@ -7277,6 +7279,8 @@ const CONST = {
DATE: 'date',
SUBMITTED: 'submitted',
APPROVED: 'approved',
FIRST_APPROVER: 'firstApprover',
FIRST_APPROVED: 'firstApproved',
POSTED: 'posted',
EXPORTED: 'exported',
MERCHANT: 'merchant',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,27 @@ function ExpenseReportListItemRowWide({
/>
</View>
),
[CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER]: (
<View style={[StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER)]}>
{!!item.firstApproverAccountID && (
<UserInfoCell
accountID={item.firstApproverAccountID}
avatar={item.firstApprover?.avatar}
displayName={item.formattedFirstApprover ?? ''}
isLargeScreenWidth
/>
)}
</View>
),
[CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED]: (
<View style={[StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED, {isFirstApprovedColumnWide: item.shouldShowYearFirstApproved})]}>
<DateCell
date={item.firstApproved ?? ''}
showTooltip
isLargeScreenWidth
/>
</View>
),
[CONST.SEARCH.TABLE_COLUMNS.EXPORTED]: (
<View style={[StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.EXPORTED, {isExportedColumnWide: item.shouldShowYearExported})]}>
<DateCell
Expand Down
18 changes: 18 additions & 0 deletions src/components/Search/SearchList/ListItem/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,18 @@ type TransactionReportGroupListItemType = TransactionGroupListItemType & {groupe
/** The date the report was exported */
exported?: string;

/** The date the report received its first approval (earliest APPROVED report action) */
firstApproved?: string;

/** The personal details of the first approver */
firstApprover?: PersonalDetails;

/** Account ID of the first approver (actor on the earliest APPROVED report action) */
firstApproverAccountID?: number;

/** Final and formatted "first approver" value used for displaying and sorting */
formattedFirstApprover?: string;

/** Whether the status field should be shown in a pending state */
shouldShowStatusAsPending?: boolean;

Expand All @@ -237,6 +249,12 @@ type TransactionReportGroupListItemType = TransactionGroupListItemType & {groupe
*/
shouldShowYearApproved: boolean;

/**
* Whether we should show the year for the first approved date.
* This is true if at least one report in the dataset was first-approved in past years
*/
shouldShowYearFirstApproved?: boolean;

/**
* Whether we should show the year for the exported date.
* This is true if at least one report in the dataset was exported in past years
Expand Down
9 changes: 9 additions & 0 deletions src/components/Search/SearchTableHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,15 @@ const getExpenseReportHeaders = (profileIcon?: IconAsset): SearchColumnConfig[]
columnName: CONST.SEARCH.TABLE_COLUMNS.APPROVED,
translationKey: 'search.filters.approved',
},
{
columnName: CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER,
translationKey: 'search.filters.firstApprover',
isColumnSortable: false,
},
{
columnName: CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED,
translationKey: 'search.filters.firstApproved',
},
{
columnName: CONST.SEARCH.TABLE_COLUMNS.EXPORTED,
translationKey: 'search.filters.exported',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8001,6 +8001,8 @@ const translations = {
past: 'Past',
submitted: 'Submitted',
approved: 'Approved',
firstApprover: 'First approver',
firstApproved: 'First approved',
paid: 'Paid',
exported: 'Exported',
posted: 'Posted',
Expand Down
99 changes: 99 additions & 0 deletions src/libs/SearchUIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ const expenseReportColumnNamesToSortingProperty: ExpenseReportSorting = {
[CONST.SEARCH.TABLE_COLUMNS.DATE]: 'created' as const,
[CONST.SEARCH.TABLE_COLUMNS.SUBMITTED]: 'submitted' as const,
[CONST.SEARCH.TABLE_COLUMNS.APPROVED]: 'approved' as const,
[CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED]: 'firstApproved' as const,
[CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER]: 'formattedFirstApprover' as const,
[CONST.SEARCH.TABLE_COLUMNS.EXPORTED]: 'exported' as const,
[CONST.SEARCH.TABLE_COLUMNS.STATUS]: 'formattedStatus' as const,
[CONST.SEARCH.TABLE_COLUMNS.TITLE]: 'reportName' as const,
Expand Down Expand Up @@ -1229,6 +1231,68 @@ function isReportActionEntry(key: string): key is ReportActionKey {
return key.startsWith(ONYXKEYS.COLLECTION.REPORT_ACTIONS);
}

/**
* Reports-list (EXPENSE_REPORT) columns that are hidden when no report in the current results
* has a value for them — mirrors the per-row data-presence convention used by `updateColumns`
* in the transaction-search path. Columns not listed here are always shown (they always have a
* value: avatar, date, status, title, from, totals, ids, action).
*/
const HIDEABLE_EXPENSE_REPORT_COLUMNS = new Set<SearchColumnType>([
CONST.SEARCH.TABLE_COLUMNS.SUBMITTED,
CONST.SEARCH.TABLE_COLUMNS.APPROVED,
CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED,
CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER,
CONST.SEARCH.TABLE_COLUMNS.EXPORTED,
CONST.SEARCH.TABLE_COLUMNS.EXPORTED_TO,
CONST.SEARCH.TABLE_COLUMNS.TO,
CONST.SEARCH.TABLE_COLUMNS.POLICY_NAME,
]);

/**
* Scans the search results once and returns the set of hideable Reports-list columns that have
* at least one report with a value. Used to drop empty columns from the Reports table, the same
* way `updateColumns` does for the transaction-search path.
* @private
*/
function getReportListColumnsWithData(data: OnyxTypes.SearchResults['data'] | OnyxTypes.Transaction[]): Set<SearchColumnType> {
const present = new Set<SearchColumnType>();
if (Array.isArray(data)) {
return present;
}
for (const key of Object.keys(data)) {
if (isReportActionEntry(key)) {
for (const action of Object.values(data[key])) {
if (action.actionName === CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_CSV || action.actionName === CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION) {
present.add(CONST.SEARCH.TABLE_COLUMNS.EXPORTED);
present.add(CONST.SEARCH.TABLE_COLUMNS.EXPORTED_TO);
}
}
continue;
}
if (isReportEntry(key)) {
const report = data[key];
if (report?.submitted) {
present.add(CONST.SEARCH.TABLE_COLUMNS.SUBMITTED);
}
if (report?.approved) {
present.add(CONST.SEARCH.TABLE_COLUMNS.APPROVED);
}
// Mirrors export's `report.approvers[0]`; needs backend `approvers` to render.
if (report?.approvers?.length) {
present.add(CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED);
present.add(CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER);
}
if (report?.managerID) {
present.add(CONST.SEARCH.TABLE_COLUMNS.TO);
}
if (report?.policyID) {
present.add(CONST.SEARCH.TABLE_COLUMNS.POLICY_NAME);
}
}
}
return present;
}

/**
* @private
*/
Expand Down Expand Up @@ -1738,6 +1802,7 @@ type PreprocessingContext = {
shouldShowYearCreatedReport: boolean;
shouldShowYearSubmittedReport: boolean;
shouldShowYearApprovedReport: boolean;
shouldShowYearFirstApprovedReport: boolean;
shouldShowYearExportedReport: boolean;
shouldShowAmountInWideColumn: boolean;
shouldShowTaxAmountInWideColumn: boolean;
Expand All @@ -1764,6 +1829,7 @@ function createPreprocessingContext(): PreprocessingContext {
shouldShowYearCreatedReport: false,
shouldShowYearSubmittedReport: false,
shouldShowYearApprovedReport: false,
shouldShowYearFirstApprovedReport: false,
shouldShowYearExportedReport: false,
shouldShowAmountInWideColumn: false,
shouldShowTaxAmountInWideColumn: false,
Expand Down Expand Up @@ -1824,6 +1890,10 @@ function processReportEntry(ctx: PreprocessingContext, report: OnyxTypes.Report)
if (!ctx.shouldShowYearApprovedReport && report.approved && DateUtils.doesDateBelongToAPastYear(report.approved)) {
ctx.shouldShowYearApprovedReport = true;
}
const firstApprovedDate = report.approvers?.at(0)?.date;
if (!ctx.shouldShowYearFirstApprovedReport && firstApprovedDate && DateUtils.doesDateBelongToAPastYear(firstApprovedDate)) {
ctx.shouldShowYearFirstApprovedReport = true;
}

if (!ctx.shouldShowYearSubmitted && report.submitted && DateUtils.doesDateBelongToAPastYear(report.submitted)) {
ctx.shouldShowYearSubmitted = true;
Expand Down Expand Up @@ -2711,6 +2781,7 @@ function getReportSections({
shouldShowYearCreatedReport,
shouldShowYearSubmittedReport,
shouldShowYearApprovedReport,
shouldShowYearFirstApprovedReport,
shouldShowYearExportedReport,
shouldShowAmountInWideColumn,
shouldShowTaxAmountInWideColumn,
Expand Down Expand Up @@ -2765,8 +2836,17 @@ function getReportSections({
emptyPersonalDetails;
const toDetails = !shouldShowBlankTo && reportItem.managerID ? mergedPersonalDetails?.[reportItem.managerID] : emptyPersonalDetails;

// First approver/approved come from BE `report.approvers[0]`; blank when absent.
const reportFirstApprover = reportItem.approvers?.at(0);
const firstApproverAccountID = reportFirstApprover
? (reportFirstApprover.accountID ?? Object.values(mergedPersonalDetails).find((details) => details?.login === reportFirstApprover.email)?.accountID)
: undefined;
const firstApproverDetails = firstApproverAccountID ? (mergedPersonalDetails?.[firstApproverAccountID] ?? emptyPersonalDetails) : emptyPersonalDetails;
const firstApproved = reportFirstApprover?.date ?? '';

const formattedFrom = formatPhoneNumber(getDisplayNameOrDefault(fromDetails));
const formattedTo = !shouldShowBlankTo ? formatPhoneNumber(getDisplayNameOrDefault(toDetails)) : '';
const formattedFirstApprover = firstApproverAccountID ? formatPhoneNumber(getDisplayNameOrDefault(firstApproverDetails)) : '';

const formattedStatus = getReportStatusTranslation({stateNum: reportItem.stateNum, statusNum: reportItem.statusNum, translate});
const policyFromKey = getPolicyFromKey(data, reportItem);
Expand Down Expand Up @@ -2806,6 +2886,10 @@ function getReportSections({
from: (fromDetails ?? emptyPersonalDetails) as OnyxTypes.PersonalDetails,
to: (toDetails ?? emptyPersonalDetails) as OnyxTypes.PersonalDetails,
exported: lastExportedActionByReportID.get(reportItem.reportID)?.created ?? '',
firstApproved,
firstApprover: (firstApproverDetails ?? emptyPersonalDetails) as OnyxTypes.PersonalDetails,
firstApproverAccountID,
formattedFirstApprover,
formattedFrom,
formattedTo,
formattedStatus,
Expand All @@ -2815,6 +2899,7 @@ function getReportSections({
shouldShowYear: shouldShowYearCreatedReport,
shouldShowYearSubmitted: shouldShowYearSubmittedReport,
shouldShowYearApproved: shouldShowYearApprovedReport,
shouldShowYearFirstApproved: shouldShowYearFirstApprovedReport,
shouldShowYearExported: shouldShowYearExportedReport,
hasVisibleViolations: hasVisibleViolationsForReport,
isRejectedReport,
Expand Down Expand Up @@ -4111,6 +4196,10 @@ function getSearchColumnTranslationKey(column: SearchColumnType): TranslationPat
return 'common.submitted';
case CONST.SEARCH.TABLE_COLUMNS.APPROVED:
return 'search.filters.approved';
case CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER:
return 'search.filters.firstApprover';
case CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED:
return 'search.filters.firstApproved';
case CONST.SEARCH.TABLE_COLUMNS.POSTED:
return 'search.filters.posted';
case CONST.SEARCH.TABLE_COLUMNS.EXPORTED:
Expand Down Expand Up @@ -5264,6 +5353,16 @@ function getColumnsToShow({
result.push(col);
}

// Drop hideable columns that have no value in any report in the current results — an
// empty column is just noise. Mirrors the per-row data-presence convention that
// `updateColumns` applies to the transaction-search path. Columns not in
// HIDEABLE_EXPENSE_REPORT_COLUMNS always have a value and are kept as-is.
const hasHideableColumnSelected = result.some((col) => HIDEABLE_EXPENSE_REPORT_COLUMNS.has(col));
if (hasHideableColumnSelected) {
const columnsWithData = getReportListColumnsWithData(data);
return result.filter((col) => !HIDEABLE_EXPENSE_REPORT_COLUMNS.has(col) || columnsWithData.has(col));
}

return result;
}

Expand Down
6 changes: 6 additions & 0 deletions src/styles/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ type GetReportTableColumnStylesParams = {
isTaxAmountColumnWide?: boolean;
isSubmittedColumnWide?: boolean;
isApprovedColumnWide?: boolean;
isFirstApprovedColumnWide?: boolean;
isPostedColumnWide?: boolean;
isExportedColumnWide?: boolean;
shouldRemoveTotalColumnFlex?: boolean;
Expand Down Expand Up @@ -1872,6 +1873,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({
const {
isSubmittedColumnWide,
isApprovedColumnWide,
isFirstApprovedColumnWide,
isPostedColumnWide,
isExportedColumnWide,
isDateColumnWide,
Expand Down Expand Up @@ -1905,6 +1907,9 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({
case CONST.SEARCH.TABLE_COLUMNS.APPROVED:
columnWidth = {...getWidthStyle(isApprovedColumnWide ? variables.w92 : variables.w72)};
break;
case CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVED:
columnWidth = {...getWidthStyle(isFirstApprovedColumnWide ? variables.w92 : variables.w72)};
break;
case CONST.SEARCH.TABLE_COLUMNS.POSTED:
columnWidth = {...getWidthStyle(isPostedColumnWide ? variables.w92 : variables.w72)};
break;
Expand Down Expand Up @@ -1978,6 +1983,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({
case CONST.SEARCH.TABLE_COLUMNS.MERCHANT:
case CONST.SEARCH.TABLE_COLUMNS.FROM:
case CONST.SEARCH.TABLE_COLUMNS.TO:
case CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER:
case CONST.SEARCH.TABLE_COLUMNS.ASSIGNEE:
case CONST.SEARCH.TABLE_COLUMNS.TITLE:
case CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION:
Expand Down
12 changes: 12 additions & 0 deletions src/types/onyx/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback<
/** The date the report was approved */
approved?: string;

/** Backend-computed approver list (mirrors export's `report.approvers`); `[0]` is the first approver. */
approvers?: Array<{
/** Email of the approver */
email: string;

/** Account ID of the approver, when provided by the backend */
accountID?: number;

/** Timestamp of this approval */
date: string;
}>;

/** The specific type of chat */
chatType?: ValueOf<typeof CONST.REPORT.CHAT_TYPE>;

Expand Down
Loading
Loading