Skip to content
Merged
1 change: 1 addition & 0 deletions public/locales/de/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@
"district": "Bezirk",
"languages": "Sprachen",
"activities": "Aktivitäten",
"skills": "Fähigkeiten",
"engagement": {
"header": "Engagement",
"vol-active": "Aktiv",
Expand Down
1 change: 1 addition & 0 deletions public/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@
"district": "District",
"languages": "Languages",
"activities": "Activities",
"skills": "Skills",
"engagement": {
"header": "Engagement",
"vol-active": "Active",
Expand Down
12 changes: 10 additions & 2 deletions src/components/Dashboard/Opportunities/Filters/FiltersContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,23 @@ export default function FiltersContent({ setFilter, filter }: Props) {
const { t } = useTranslation();
const { isAuthorized } = useAuth();

const { districtFilters, languageFilters, statusFilters, typeFilters, activityFilters, availabilityFilters } =
createOpportunityFilterItems(filter, setFilter, t);
const {
districtFilters,
languageFilters,
statusFilters,
typeFilters,
activityFilters,
skillFilters,
availabilityFilters,
} = createOpportunityFilterItems(filter, setFilter, t);
return (
<FiltersContentContainer data-testid="opportunity-filters-content">
<AccordionFilter header={t("dashboard.opportunities.filters.type.header")} items={typeFilters} />
<AccordionFilter header={t("dashboard.opportunities.filters.status.header")} items={statusFilters} />
<AccordionFilter header={t("dashboard.volunteers.filters.district")} items={districtFilters} />
<AccordionFilter header={t("dashboard.volunteers.filters.languages")} items={languageFilters} />
<AccordionFilter header={t("dashboard.volunteers.filters.activities")} items={activityFilters} />
<AccordionFilter header={t("dashboard.volunteers.filters.skills")} items={skillFilters} />
{isAuthorized && (
<AccordionFilter
header={t("dashboard.opportunities.filters.schedule.header")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const defaultOpportunityCardsFilter: OpportunityCardsFilter = {
[OpportunityType.REGULAR]: false,
},
[EntityTableName.ACTIVITY]: {},
[EntityTableName.SKILL]: {},
[QueryParamsKeys.AVAILABILITY]: {
times: {
[TimeSlot.morning]: false,
Expand Down
29 changes: 26 additions & 3 deletions src/components/Dashboard/Opportunities/Filters/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,24 @@ export const createOpportunityFilterItems = (
(key) => key,
);

const skillFilters = generateNestedFilterControlItems(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this filter required copy-pasting the same wiring across ~6 files (types, constants, this item-builder, the flat-array builder, option loading, serialize/deserialize) instead of a shared data-driven config for district/language/activity/skill. Likely how the falsy-0 bug above slipped in — the next filter (or a fix) has to be replicated by hand across all 6 sites with no compiler check for a missed spot.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood, I'll work on it. Thank you.

filter[EntityTableName.SKILL],
setFilter,
EntityTableName.SKILL,
(key) => key,
);

const availabilityFilters = createAvailabilityFilterItems(filter[QueryParamsKeys.AVAILABILITY], setFilter, t);

return { districtFilters, languageFilters, statusFilters, typeFilters, activityFilters, availabilityFilters };
return {
districtFilters,
languageFilters,
statusFilters,
typeFilters,
activityFilters,
skillFilters,
availabilityFilters,
};
};

/**
Expand Down Expand Up @@ -86,15 +101,23 @@ export const createSelectedOpportunityFiltersAsFlatArray = (
setFilter: SetFilter<OpportunityCardsFilter>,
t: TFunction,
) => {
const { districtFilters, languageFilters, statusFilters, typeFilters, activityFilters, availabilityFilters } =
createOpportunityFilterItems(filter, setFilter, t);
const {
districtFilters,
languageFilters,
statusFilters,
typeFilters,
activityFilters,
skillFilters,
availabilityFilters,
} = createOpportunityFilterItems(filter, setFilter, t);
const flatAvFilters = availabilityFilters.map((avFilter) => avFilter.items).flat();
return [
...districtFilters,
...languageFilters,
...statusFilters,
...typeFilters,
...activityFilters,
...skillFilters,
...flatAvFilters,
].filter((f) => f.checked);
};
1 change: 1 addition & 0 deletions src/components/Dashboard/Opportunities/Filters/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface OpportunityCardsFilter {
status: SelectionMap;
type: SelectionMap;
[EntityTableName.ACTIVITY]: SelectionMap;
[EntityTableName.SKILL]: SelectionMap;
[QueryParamsKeys.AVAILABILITY]: ScheduleFilter;
}

Expand Down
1 change: 1 addition & 0 deletions src/components/Dashboard/Opportunities/Opportunities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export function Opportunities() {
district: createFilterFromOption(apiFilterOptions, EntityTableName.DISTRICT),
language: createFilterFromOption(apiFilterOptions, EntityTableName.LANGUAGE),
activity: createFilterFromOption(apiFilterOptions, EntityTableName.ACTIVITY),
skill: createFilterFromOption(apiFilterOptions, EntityTableName.SKILL),
};

return deserializeOpportunityFilters(baseFilters, searchParams);
Expand Down
95 changes: 34 additions & 61 deletions src/components/Dashboard/Opportunities/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,30 @@ export const DEFAULT_SORT_ORDER: string = SortOrder.NewToOld;
export const APPOINTMENT_SORT_VALUES = ["appointment-proximal", "appointment-distant"] as const;
export type AppointmentSort = (typeof APPOINTMENT_SORT_VALUES)[number];

const ID_MAPPED_FILTER_KEYS = [
EntityTableName.DISTRICT,
EntityTableName.LANGUAGE,
EntityTableName.ACTIVITY,
EntityTableName.SKILL,
] as const;
const PLAIN_FILTER_KEYS = [STATUS_PARAM, "type"] as const;

function appendIdMappedFilter(
params: URLSearchParams,
filter: OpportunityCardsFilter,
name: (typeof ID_MAPPED_FILTER_KEYS)[number],
options?: SerializeFiltersOptions,
) {
params.delete(name);
Object.entries(filter[name]).forEach(([key, value]) => {
if (value === true) {
const paramValue =
(options?.serializeToIDs && options.apiFilterOptions?.[name]?.find((d) => d.title === key)?.id) || key;
params.append(name, String(paramValue));
}
});
}

export function isAppointmentSort(sort: string): sort is AppointmentSort {
return (APPOINTMENT_SORT_VALUES as readonly string[]).includes(sort);
}
Expand Down Expand Up @@ -63,45 +87,12 @@ export function serializeOpportunityFilters(
if (filter.search) params.set(QueryParamsKeys.SEARCH, filter.search);
else params.delete(QueryParamsKeys.SEARCH);

params.delete(QueryParamsKeys.DISTRICT);
Object.entries(filter.district).forEach(([key, value]) => {
if (value === true) {
const paramValue =
(options?.serializeToIDs && options.apiFilterOptions?.district?.find((d) => d.title === key)?.id) || key;
params.append(QueryParamsKeys.DISTRICT, String(paramValue));
}
});

params.delete(QueryParamsKeys.LANGUAGE);
Object.entries(filter.language).forEach(([key, value]) => {
if (value === true) {
const paramValue =
(options?.serializeToIDs && options.apiFilterOptions?.language?.find((d) => d.title === key)?.id) || key;
params.append(QueryParamsKeys.LANGUAGE, String(paramValue));
}
});

params.delete(STATUS_PARAM);
Object.entries(filter.status).forEach(([key, value]) => {
if (value === true) {
params.append(STATUS_PARAM, key);
}
});

params.delete("type");
Object.entries(filter.type).forEach(([key, value]) => {
if (value === true) {
params.append("type", key);
}
});

params.delete(EntityTableName.ACTIVITY);
Object.entries(filter.activity).forEach(([key, value]) => {
if (value === true) {
const paramValue =
(options?.serializeToIDs && options.apiFilterOptions?.activity?.find((d) => d.title === key)?.id) || key;
params.append(EntityTableName.ACTIVITY, String(paramValue));
}
ID_MAPPED_FILTER_KEYS.forEach((name) => appendIdMappedFilter(params, filter, name, options));
PLAIN_FILTER_KEYS.forEach((name) => {
params.delete(name);
Object.entries(filter[name]).forEach(([key, value]) => {
if (value === true) params.append(name, key);
});
});

params.delete(QueryParamsKeys.AVAILABILITY);
Expand All @@ -126,28 +117,10 @@ export function deserializeOpportunityFilters(
const search = searchParams.get(QueryParamsKeys.SEARCH);
if (search !== null) newFilter.search = search;

const queryDistricts = searchParams.getAll(QueryParamsKeys.DISTRICT);
queryDistricts.forEach((d) => {
newFilter.district[d] = true;
});
const queryLanguages = searchParams.getAll(QueryParamsKeys.LANGUAGE);
queryLanguages.forEach((l) => {
newFilter.language[l] = true;
});

const queryStatus = searchParams.getAll(STATUS_PARAM);
queryStatus.forEach((s) => {
newFilter.status[s] = true;
});

const queryType = searchParams.getAll("type");
queryType.forEach((s) => {
newFilter.type[s] = true;
});

const queryActivities = searchParams.getAll(EntityTableName.ACTIVITY);
queryActivities.forEach((l) => {
newFilter.activity[l] = true;
[...ID_MAPPED_FILTER_KEYS, ...PLAIN_FILTER_KEYS].forEach((name) => {
searchParams.getAll(name).forEach((value) => {
newFilter[name][value] = true;
});
});

const queryAvailability = searchParams.getAll(QueryParamsKeys.AVAILABILITY);
Expand Down
Loading