Added a "Published in the last X days" filter, applied by the backend
GET /api/opportunities takes released_from, an RFC 3339 time, and lists only the opportunities whose latest release is at or after it. It works with paging and with changed_after, which keeps the same batch either way; an invalid value is 400. The frontend's filter defaults to 7 days, from midnight UK time, and sends it with every load and catch-up, so a year of opportunities isn't downloaded to show a week's. Changing the days loads the list again; Clear filters goes back to 7.
This commit is contained in:
1 parent
b8244da2e5
commit
746fa0a9c1
19 files changed
+333
-64
No files matched your search
@@ -7,6 +7,7 @@ import {
|
||||
emptyFilters,
|
||||
filterOpportunities,
|
||||
NOT_MARKED,
|
||||
releasedFrom,
|
||||
statusLabel,
|
||||
statusOptions,
|
||||
} from './filterOpportunities';
|
||||
@@ -73,6 +74,16 @@ test('activeFilterCount counts the filters in use', () => {
|
||||
expect(
|
||||
activeFilterCount({ ...emptyFilters(), title: ' ', smeSuitable: true, showHidden: true }),
|
||||
).toBe(2);
|
||||
expect(activeFilterCount({ ...emptyFilters(), releasedWithinDays: 30 })).toBe(1);
|
||||
});
|
||||
|
||||
test('releasedFrom is the start of the day that many days ago, in UK time', () => {
|
||||
// 19 October 2026 is in British Summer Time; 12 October's midnight is 23:00 UTC.
|
||||
const now = new Date('2026-10-19T09:30:00Z');
|
||||
expect(releasedFrom(7, now)).toBe('2026-10-11T23:00:00.000Z');
|
||||
expect(releasedFrom(0, now)).toBe('2026-10-18T23:00:00.000Z');
|
||||
// Across the end of BST (25 October), midnight is 00:00 UTC again.
|
||||
expect(releasedFrom(7, new Date('2026-11-02T12:00:00Z'))).toBe('2026-10-26T00:00:00.000Z');
|
||||
});
|
||||
|
||||
test('statusOptions lists the known statuses, then others sorted', () => {
|
||||
|
||||
@@ -3,12 +3,21 @@ import type { Opportunity, OpportunityStatus } from '@/types/Opportunities';
|
||||
import { OPPORTUNITY_STATUSES } from '@/types/Opportunities';
|
||||
import { parseDate } from './format';
|
||||
|
||||
/** How many days back opportunities are loaded from, unless the user chooses otherwise. */
|
||||
export const DEFAULT_RELEASED_WITHIN_DAYS = 7;
|
||||
|
||||
/** Marks filter option for opportunities with no marks at all. */
|
||||
export const NOT_MARKED = 'none';
|
||||
export type MarkFilter = OpportunityMark | typeof NOT_MARKED;
|
||||
|
||||
/** What the table is filtered by. */
|
||||
export interface OpportunityFilters {
|
||||
/**
|
||||
* Only opportunities with a release in the last this many days, and
|
||||
* today. The backend applies it (see releasedFrom); the rest are applied
|
||||
* here.
|
||||
*/
|
||||
releasedWithinDays: number;
|
||||
title: string;
|
||||
/** Empty means every status. */
|
||||
statuses: ReadonlySet<string>;
|
||||
@@ -21,8 +30,12 @@ export interface OpportunityFilters {
|
||||
showHidden: boolean;
|
||||
}
|
||||
|
||||
/** Filters that let every opportunity through, apart from hidden ones. */
|
||||
/**
|
||||
* The filters to start with: the last DEFAULT_RELEASED_WITHIN_DAYS days'
|
||||
* opportunities, apart from hidden ones.
|
||||
*/
|
||||
export const emptyFilters = (): OpportunityFilters => ({
|
||||
releasedWithinDays: DEFAULT_RELEASED_WITHIN_DAYS,
|
||||
title: '',
|
||||
statuses: new Set(),
|
||||
marks: new Set(),
|
||||
@@ -31,9 +44,13 @@ export const emptyFilters = (): OpportunityFilters => ({
|
||||
showHidden: false,
|
||||
});
|
||||
|
||||
/** How many separate filters are in use, for the collapsed panel's badge. */
|
||||
/**
|
||||
* How many separate filters are changed from emptyFilters, for the collapsed
|
||||
* panel's badge.
|
||||
*/
|
||||
export const activeFilterCount = (filters: OpportunityFilters): number =>
|
||||
[
|
||||
filters.releasedWithinDays !== DEFAULT_RELEASED_WITHIN_DAYS,
|
||||
filters.title.trim() !== '',
|
||||
filters.statuses.size > 0,
|
||||
filters.marks.size > 0,
|
||||
@@ -46,6 +63,15 @@ export const activeFilterCount = (filters: OpportunityFilters): number =>
|
||||
export const hasActiveFilters = (filters: OpportunityFilters): boolean =>
|
||||
activeFilterCount(filters) > 0;
|
||||
|
||||
/**
|
||||
* The start of the day days before now, in local time, as an RFC 3339 time
|
||||
* for the backend's released_from: 7 days on a Monday is from the Monday
|
||||
* before, so a whole week and today.
|
||||
*/
|
||||
export function releasedFrom(days: number, now: Date = new Date()): string {
|
||||
return new Date(now.getFullYear(), now.getMonth(), now.getDate() - days).toISOString();
|
||||
}
|
||||
|
||||
/** The opportunities that pass filters, newest release first. */
|
||||
export function filterOpportunities(
|
||||
opportunities: readonly Opportunity[],
|
||||
|
||||
Reference in new issue
Block a user