Skip to content

Commit

Permalink
TYLERB/APPEALS-56610: Allow Filtering of completed tasks by completio…
Browse files Browse the repository at this point in the history
…n date (#23128)

* Initial commit that alters the business line logic to accept a date filter for the tasks completed at column and logic to support and filter by it via the database query.

* Added new tests for the tasksClosedAt filter to the business line spec. Adjusted the between lambda filter to work for either ordering of the start date or end date as the first chronological date in the query. Optimized the business line spec by swapping to before all loops and instance variables to shorten the time it takes to run the test.

* Rewrote a bit more of the business line spec to speed it up via one time test data setup rather than for each test.

* Updated the vha business line spec file. Fixed a code climate warning.

* Removed some commented out code and changed the database cleaner to Task.delete all to be a bit more specific on cleanup before the change history tests.

* Removed QueueTable code that was unrelated to the DatePicker that was causing errors.

* Updated the wording in the filter summary for the closedAt filter to say Date Compeleted instead of closedAt.

* Fixed a bug where the filter preservation was not working with the way the date filter params were added to the get params. Altered the Completed tasks tab description based on the Date Completed filter. Started adding a feature test for the completed date filtering.

* Updated the formatting of the date in the completed tasks tab description to match mm/dd/yyyy format. Finished up the feature spec test for the completed date filtering.

* Updated the expected values from the DatePicker to match what was in the feature branch. Updated the completed tasks tab description to once again be last 7 days for any business line that is not the VHA. Fixed a code climate issue with the new regex used for the filter preservation hook. Updated the new vha completed by date column to match the old column value so sorting and filtering will work correctly.

* Updated the aria label in the new column.

* Fixed failing tests related to the completed date column, completed tasks description, and the clear filters change from the DatePicker.

* Changed single quotes to double quotes in ruby.

* Fixed a few more test failures.

* Altered the date picker validation code to disable the button if no date is selected for on, before, or after.

* Updated the completed tasks tab description filtered by completed text to be less verbose. Change .nil? to .blank? in the closed_at filter parsing. Updated the test to reflect the new wording.

---------

Co-authored-by: Brandon Lee Dorner <51007432+brandondorner@users.noreply.github.com>
  • Loading branch information
TylerBroyles and brandondorner authored Oct 24, 2024
1 parent a5e9c78 commit 2fb9b65
Show file tree
Hide file tree
Showing 16 changed files with 699 additions and 324 deletions.
65 changes: 65 additions & 0 deletions app/models/organizations/business_line.rb
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ def build_query
.from(combined_decision_review_tasks_query)
.includes(*decision_review_task_includes)
.where(task_filter_predicate(query_params[:filters]))
.where(closed_at_filter_predicate(query_params[:filters]))
.order(order_clause)
end

Expand Down Expand Up @@ -1082,6 +1083,70 @@ def locate_issue_type_filter(filters)
def where_clause_from_array(table_class, column, values_array)
table_class.arel_table[column].in(values_array)
end

def closed_at_filter_predicate(filters)
return "" if filters.blank?

closed_at_filter = locate_closed_at_filter(filters)

return "" unless closed_at_filter

# ex: "val"=> val=[before,2024-09-08,]
closed_at_params = closed_at_filter["val"].first.split(",")

build_closed_at_filter_predicate(closed_at_params) || ""
end

def locate_closed_at_filter(filters)
parsed_filters = parse_filters(filters)

parsed_filters.find do |filter|
filter["col"].include?("completedDateColumn")
end
end

def build_closed_at_filter_predicate(closed_at_params)
return "" if closed_at_params.blank?

mode, start_date, end_date = closed_at_params
operator = date_filter_mode_to_operator(mode)

# Break early if start date is not present and it's not one of these 3 filter types
return "" if !%w[last_7_days last_30_days last_365_days].include?(operator) && start_date.blank?

date_filter_lambda_hash(start_date, end_date).fetch(operator, lambda {
Rails.logger.error("Unsupported mode **#{operator}** used for closed at date filtering")
""
}).call
end

def date_filter_lambda_hash(start_date, end_date)
{
">" => -> { "tasks.closed_at::date > '#{start_date}'::date" },
"<" => -> { "tasks.closed_at::date < '#{start_date}'::date" },
"=" => -> { "tasks.closed_at::date = '#{start_date}'::date" },
"between" => lambda {
# Ensure the dates are sorted correctly so either ordering works e.g. start > end or end > start
start_date, end_date = [start_date, end_date].map(&:to_date).sort
end_date ? "tasks.closed_at::date BETWEEN '#{start_date}'::date AND '#{end_date}'::date" : ""
},
"last_7_days" => -> { { closed_at: 1.week.ago..Time.zone.now } },
"last_30_days" => -> { { closed_at: 30.days.ago..Time.zone.now } },
"last_365_days" => -> { { closed_at: 365.days.ago..Time.zone.now } }
}
end

def date_filter_mode_to_operator(mode)
{
"between" => "between",
"after" => ">",
"before" => "<",
"on" => "=",
"last7" => "last_7_days",
"last30" => "last_30_days",
"last365" => "last_365_days"
}[mode]
end
end
# rubocop:enable Metrics/ClassLength
end
Expand Down
2 changes: 1 addition & 1 deletion app/models/organizations/vha_business_line.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def tasks_query_type
{
incomplete: "on_hold",
in_progress: "active",
completed: "recently_completed",
completed: "completed",
pending: "active"
}
end
Expand Down
1 change: 1 addition & 0 deletions client/COPY.json
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@
"ORGANIZATIONAL_QUEUE_PAGE_IN_PROGRESS_TAB_TITLE": "In Progress (%d)",
"ORGANIZATIONAL_QUEUE_ON_HOLD_TAB_TITLE": "On Hold (%d)",
"VHA_QUEUE_PAGE_COMPLETE_TASKS_DESCRIPTION": "Cases completed:",
"VHA_QUEUE_PAGE_COMPLETE_TASKS_DESCRIPTION_WITH_FILTER": "Cases completed (%s)",
"EDUCATION_RPO_QUEUE_PAGE_COMPLETED_TASKS_DESCRIPTION": "Cases completed in the last 7 days:",
"VHA_ORGANIZATIONAL_QUEUE_PAGE_READY_FOR_REVIEW_TAB_TITLE": "Ready for Review",
"VHA_ORGANIZATIONAL_QUEUE_PAGE_ON_HOLD_TAB_TITLE": "On Hold",
Expand Down
5 changes: 0 additions & 5 deletions client/app/components/DatePicker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,6 @@ class DatePicker extends React.PureComponent {

disabled = startDate >= endDate;
}
} else if (this.state.mode === 'before' || this.state.mode === 'after' || this.state.mode === 'on') {
const startDate = moment(`${this.state.startDate} 00:00:00`).valueOf();
const currentDate = moment().valueOf();

disabled = startDate >= currentDate;
} else if (this.state.mode !== '') {
disabled = this.state.startDate === '';
}
Expand Down
3 changes: 2 additions & 1 deletion client/app/components/FilterSummary.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ const ALTERNATE_COLUMN_NAMES = {
suggestedLocation: 'Suggested Location',
hearingLocation: 'Hearing Location',
readableEventType: 'Actvitity',
eventUser: 'User'
eventUser: 'User',
closedAt: 'Date Completed'
};

const FilterSummary = ({ filteredByList, clearFilteredByList }) => {
Expand Down
6 changes: 5 additions & 1 deletion client/app/components/TableFilter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ class TableFilter extends React.PureComponent {
});

// Case insensitive ordering for the filter options
return _.orderBy(filterOptionsFromApi, [(option) => option.displayText.toLowerCase()], ['asc']);
return _.orderBy(
filterOptionsFromApi.filter((option) => option.displayText),
[(option) => option.displayText.toLowerCase()],
['asc']
);
}

const columnValues = tableDataByRow.map((obj) => {
Expand Down
41 changes: 40 additions & 1 deletion client/app/nonComp/components/NonCompTabs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import COPY from '../../../COPY';
import TaskTableTab from './TaskTableTab';
import useLocalFilterStorage from '../hooks/useLocalFilterStorage';
import { mapValues, sumBy } from 'lodash';
import { sprintf } from 'sprintf-js';
import { formatDateStr } from '../../util/DateUtil';

const NonCompTabsUnconnected = (props) => {
const [localFilter, setFilter] = useLocalFilterStorage('nonCompFilter', []);
Expand Down Expand Up @@ -47,6 +49,43 @@ const NonCompTabsUnconnected = (props) => {
mapValues(props.taskFilterDetails, (obj) => sumBy(Object.values(obj)))
), [props.taskFilterDetails]);

const buildCompletedTabDescriptionFromFilter = (filters) => {
const completedDateFilter = filters.find((value) => value.includes('col=completedDateColumn'));

if (completedDateFilter) {
const match = completedDateFilter.match(/val=([^&]*)/);

if (match) {
const dateFilter = match[1];
const [mode, startDate = null, endDate = null] = dateFilter.split(',');

const formattedStartDate = startDate ? formatDateStr(startDate) : '';

const formattedEndDate = endDate ? formatDateStr(endDate) : '';

// Object that defines how to build the string based on the mode
const completedDateFilterModeHandlers = {
before: `Before ${formattedStartDate}`,
after: `After ${formattedStartDate}`,
on: `On ${formattedStartDate}`,
between: `Between ${formattedStartDate} and ${formattedEndDate}`,
last7: 'Last 7 Days',
last30: 'Last 30 Days',
last365: 'Last 365 Days'
};

return sprintf(COPY.VHA_QUEUE_PAGE_COMPLETE_TASKS_DESCRIPTION_WITH_FILTER,
completedDateFilterModeHandlers[mode]);
}

} else if (!isVhaBusinessLine) {
return COPY.QUEUE_PAGE_COMPLETE_LAST_SEVEN_DAYS_TASKS_DESCRIPTION;
}

return COPY.QUEUE_PAGE_COMPLETE_TASKS_DESCRIPTION;

};

const ALL_TABS = {
incomplete: {
label: `Incomplete Tasks (${taskCounts.incomplete})`,
Expand Down Expand Up @@ -97,7 +136,7 @@ const NonCompTabsUnconnected = (props) => {
{...(isVhaBusinessLine ? { onHistoryUpdate } : {})}
filterableTaskTypes={props.taskFilterDetails.completed}
filterableTaskIssueTypes={props.taskFilterDetails.completed_issue_types}
description={COPY.QUEUE_PAGE_COMPLETE_LAST_SEVEN_DAYS_TASKS_DESCRIPTION}
description={buildCompletedTabDescriptionFromFilter(filter)}
tabName="completed" />
}
};
Expand Down
4 changes: 2 additions & 2 deletions client/app/nonComp/components/TaskTableColumns.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const pendingIssueModificationColumn = () => {
export const vhaTaskCompletedDateColumn = () => {
return {
header: COPY.CASE_LIST_TABLE_COMPLETED_ON_DATE_COLUMN_TITLE,
name: 'completedOnDateColumn',
name: QUEUE_CONFIG.COLUMNS.TASK_CLOSED_DATE.name,
valueFunction: (task) => task.closedAt ? <DateString date={task.closedAt} /> : null,
backendCanSort: true,
enableFilter: true,
Expand All @@ -78,7 +78,7 @@ export const vhaTaskCompletedDateColumn = () => {
},
columnName: 'closedAt',
valueName: 'closedAt',
label: 'Date Completed',
label: 'Filter by completed date',
getSortValue: (task) => task.closedAt ? new Date(task.closedAt) : null
};
};
10 changes: 9 additions & 1 deletion client/app/nonComp/hooks/useLocalFilterStorage.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { compact } from 'lodash';
import { useEffect, useState } from 'react';

const useLocalFilterStorage = (key, defaultValue) => {
const [value, setValue] = useState(() => {
const storedValue = localStorage.getItem(key);

return storedValue && storedValue !== 'null' ? [storedValue?.split(',')].flat() : defaultValue;
if (storedValue === null) {
return defaultValue;
}

const regex = /col=[^&]+&val=[^,]+(?:,[^&,]+)*(?=,|$)/g;
const columnsWithValues = [...storedValue.matchAll(regex)].map((match) => match[0]);

return compact(columnsWithValues);
});

useEffect(() => {
Expand Down
9 changes: 1 addition & 8 deletions client/app/queue/QueueTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,7 @@ export default class QueueTable extends React.PureComponent {
const responseFromCache = this.props.useReduxCache ? this.props.reduxCache[endpointUrl] :
this.state.cachedResponses[endpointUrl];

if (responseFromCache && !this.props.skipCache) {
if (responseFromCache) {
this.setState({ tasksFromApi: responseFromCache.tasks });

return Promise.resolve(true);
Expand Down Expand Up @@ -692,10 +692,6 @@ export default class QueueTable extends React.PureComponent {
loadingComponent: null
});

if (this.props.onTableDataUpdated) {
this.props.onTableDataUpdated(preparedTasks);
}

if (this.props.useReduxCache) {
this.props.updateReduxCache({ key: endpointUrl, value: preparedResponse });
}
Expand Down Expand Up @@ -897,9 +893,6 @@ HeaderRow.propTypes = FooterRow.propTypes = Row.propTypes = BodyRows.propTypes =
}),
onHistoryUpdate: PropTypes.func,
preserveFilter: PropTypes.bool,
prepareTasks: PropTypes.bool,
onTableDataUpdated: PropTypes.func,
skipCache: PropTypes.bool,
useReduxCache: PropTypes.bool,
reduxCache: PropTypes.object,
updateReduxCache: PropTypes.func
Expand Down
3 changes: 2 additions & 1 deletion client/test/app/nonComp/NonCompTabs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ describe('NonCompTabsVha', () => {
await tabs[3].click();

await waitFor(() => {
expect(screen.getByText('Cases completed (last 7 days):')).toBeInTheDocument();
// expect(screen.getByText('Cases completed (last 7 days):')).toBeInTheDocument();
expect(screen.getByText('Cases completed:')).toBeInTheDocument();
});

// Check for the correct completed tasks header values
Expand Down
2 changes: 1 addition & 1 deletion spec/feature/non_comp/individual_claim_history_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def clear_filter_option(filter_text)
sort = find("[aria-label='Filter by Activity. Filtering by #{filter_text}']")
sort.click

clear_button_filter = page.find(class: "cf-clear-filter-button-wrapper")
clear_button_filter = page.first(:css, ".cf-clear-filter-button-wrapper, .clear-wrapper")
clear_button_filter.click
end

Expand Down
Loading

0 comments on commit 2fb9b65

Please sign in to comment.