Skip to content

Commit a8d35fa

Browse files
committed
[#2111] set-up for filter web-components
1 parent dc7d9a9 commit a8d35fa

File tree

14 files changed

+857
-1
lines changed

14 files changed

+857
-1
lines changed

src/open_inwoner/mijn_afval/views.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,21 @@ class _BAGObjectData(TypedDict):
7979
containers: list[_AfvalContainerData]
8080

8181

82+
class _FilterChoice(TypedDict):
83+
"""A single choice in a filter group."""
84+
85+
value: str
86+
label: str
87+
88+
89+
class _FilterGroup(TypedDict):
90+
"""A group of filter choices."""
91+
92+
name: str
93+
label: str
94+
choices: list[_FilterChoice]
95+
96+
8297
def _format_container_for_table(
8398
ledigingen: list[_LedigingData],
8499
totaal_gewicht: str,
@@ -194,6 +209,89 @@ def _format_bag_objects(bag_objects: list[BAGObject]) -> list[_BAGObjectData]:
194209
return result
195210

196211

212+
def _extract_filter_options(afval_data: list[_BAGObjectData]) -> list[_FilterGroup]:
213+
"""
214+
Extract unique filter options from the formatted afval data.
215+
216+
Args:
217+
afval_data: List of formatted BAG object data
218+
219+
Returns:
220+
List of filter groups with their available choices
221+
"""
222+
addresses: dict[str, str] = {} # value: label
223+
container_types: dict[str, str] = {} # value: label
224+
periods: dict[str, str] = {} # year: year (as label)
225+
226+
# Extract unique values from the data
227+
for bag_obj in afval_data:
228+
# Extract address
229+
if bag_obj["object_address"]:
230+
addresses[bag_obj["object_address"]] = bag_obj["object_address"]
231+
232+
# Extract container types and periods
233+
for container in bag_obj["containers"]:
234+
# Container type
235+
container_type = container["type"]
236+
if container_type not in container_types:
237+
container_types[container_type] = _get_container_type_label(
238+
container_type
239+
)
240+
241+
# Extract years from ledigingen dates
242+
for lediging in container["ledigingen"]:
243+
# Date format is "dd-mm-yyyy", extract year
244+
date_parts = lediging["tijdstip_datum"].split("-")
245+
if len(date_parts) == 3:
246+
year = date_parts[2]
247+
if year not in periods:
248+
periods[year] = _("Jaar {year}").format(year=year)
249+
250+
# Build filter groups
251+
filter_groups: list[_FilterGroup] = []
252+
253+
# Adres filter
254+
if addresses:
255+
filter_groups.append(
256+
{
257+
"name": "adres",
258+
"label": _("Adres"),
259+
"choices": [
260+
{"value": value, "label": label}
261+
for value, label in sorted(addresses.items())
262+
],
263+
}
264+
)
265+
266+
# Type container filter
267+
if container_types:
268+
filter_groups.append(
269+
{
270+
"name": "type-container",
271+
"label": _("Type container"),
272+
"choices": [
273+
{"value": value, "label": label}
274+
for value, label in sorted(container_types.items())
275+
],
276+
}
277+
)
278+
279+
# Periode filter (sorted by year descending)
280+
if periods:
281+
filter_groups.append(
282+
{
283+
"name": "periode",
284+
"label": _("Periode"),
285+
"choices": [
286+
{"value": value, "label": label}
287+
for value, label in sorted(periods.items(), reverse=True)
288+
],
289+
}
290+
)
291+
292+
return filter_groups
293+
294+
197295
class AfvalView(LoginRequiredMixin, BaseBreadcrumbMixin, AppConfigMixin, TemplateView):
198296
template_name = "pages/mijn_afval/index.html"
199297

@@ -218,14 +316,19 @@ def get_context_data(self, **kwargs):
218316

219317
client = AfvalApiClient(base_url="")
220318
data = client.fetch_bag_objects_for_bsn(bsn=self.request.user.bsn)
319+
formatted_data = _format_bag_objects(data)
320+
321+
# Extract filter options from the formatted data
322+
filter_groups = _extract_filter_options(formatted_data)
221323

222324
# check for apphook config in case it is manually deleted (defensive)
223325
page_heading = self.config.page_heading if self.config else _("Mijn Afval")
224326
page_description = self.config.page_description if self.config else ""
225327

226328
context.update(
227329
{
228-
"afval_data": _format_bag_objects(data),
330+
"afval_data": formatted_data,
331+
"filter_groups": filter_groups,
229332
"page_heading": page_heading,
230333
"page_description": page_description,
231334
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
.filter-button {
2+
display: inline-flex;
3+
align-items: center;
4+
gap: 0.5rem;
5+
padding: 0.5rem 1rem;
6+
background: transparent;
7+
border: 1px solid currentColor;
8+
border-radius: 4px;
9+
cursor: pointer;
10+
font-size: 1rem;
11+
transition: all 0.2s ease;
12+
13+
&:hover {
14+
background-color: rgba(0, 0, 0, 0.05);
15+
}
16+
17+
&:focus-visible {
18+
outline: 2px solid #0066cc;
19+
outline-offset: 2px;
20+
}
21+
22+
&--active {
23+
position: relative;
24+
}
25+
}
26+
27+
.filter-button__content {
28+
display: flex;
29+
align-items: center;
30+
gap: 0.5rem;
31+
}
32+
33+
.filter-button__label {
34+
white-space: nowrap;
35+
}
36+
37+
.filter-button__indicator {
38+
display: flex;
39+
align-items: center;
40+
margin-left: 0.5rem;
41+
color: #00a63d;
42+
}
43+
44+
.sr-only {
45+
position: absolute;
46+
width: 1px;
47+
height: 1px;
48+
padding: 0;
49+
margin: -1px;
50+
overflow: hidden;
51+
clip: rect(0, 0, 0, 0);
52+
white-space: nowrap;
53+
border-width: 0;
54+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import clsx from 'clsx';
2+
import { AnyComponent as AC } from 'preact';
3+
import { useRef } from 'preact/hooks';
4+
import { MaterialIcon } from '../MaterialIcon';
5+
import './FilterButton.scss';
6+
7+
export interface IFilterButtonProps {
8+
label?: string;
9+
currentUrl?: string;
10+
ariaLabel?: string;
11+
}
12+
13+
const FilterButton: AC<IFilterButtonProps> = ({
14+
label = 'Filters',
15+
currentUrl = '',
16+
ariaLabel = 'Filters',
17+
}) => {
18+
const buttonRef = useRef<HTMLButtonElement>(null);
19+
20+
// Check if there are active filters by parsing the URL
21+
const hasActiveFilters =
22+
currentUrl &&
23+
new URLSearchParams(new URL(currentUrl, window.location.origin).search)
24+
.size > 0;
25+
26+
const handleClick = () => {
27+
// Try to find and open the modal directly (for Storybook/simple cases)
28+
const modal = document.querySelector('oip-filter-modal') as any;
29+
if (modal && typeof modal.openModal === 'function') {
30+
modal.openModal();
31+
}
32+
33+
// Also dispatch an event for other listeners (for complex page setups)
34+
const event = new CustomEvent('open-filter-modal', { bubbles: true });
35+
buttonRef.current?.dispatchEvent(event);
36+
};
37+
38+
return (
39+
<button
40+
ref={buttonRef}
41+
type="button"
42+
class={clsx('filter-button', {
43+
'filter-button--active': hasActiveFilters,
44+
})}
45+
onClick={handleClick}
46+
aria-label={ariaLabel}
47+
title={label}
48+
>
49+
<div class="filter-button__content">
50+
<MaterialIcon name="filter_alt" />
51+
<span class="filter-button__label">{label}</span>
52+
</div>
53+
54+
{hasActiveFilters && (
55+
<div class="filter-button__indicator">
56+
<MaterialIcon name="check" />
57+
<span class="sr-only">Gekozen filters</span>
58+
</div>
59+
)}
60+
</button>
61+
);
62+
};
63+
64+
export default FilterButton;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { WebComponentDefinition } from '@react/lib/web-component';
2+
import type { IFilterButtonProps } from './FilterButton';
3+
4+
export const FILTER_BUTTON_DEFINITION: WebComponentDefinition<
5+
'oip-filter-button',
6+
IFilterButtonProps
7+
> = {
8+
tagName: 'oip-filter-button',
9+
propNames: ['label', 'currentUrl', 'ariaLabel'],
10+
options: { shadow: false },
11+
importer: () => import('./FilterButton'),
12+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Preact Component
2+
export { default as FilterButton } from './FilterButton';
3+
4+
// Types
5+
export type { IFilterButtonProps } from './FilterButton';
6+
7+
// Constants
8+
export * from './constants';

0 commit comments

Comments
 (0)