feat(wohnungen): "Rausgefilterte Wohnungen" section with reason chips

Below "Abgelehnte Wohnungen", surface flats that survived the time
filter and aren't rejected but failed at least one of the user's
filters. Same collapsed-card style. Action buttons are replaced by
chips naming each failed dimension — "Zimmer", "Preis", "Größe",
"WBS", "Bezirk" — so it's obvious which constraint to relax.

Refactored matching: flat_matches_filter now delegates to a new
flat_filter_failures(flat, f) that returns the failed-dimension
labels (empty list = full match). rooms_min and rooms_max collapse
to a single "Zimmer" chip; reasons emit in stable _REASON_ORDER for
consistent rendering. The section is suppressed entirely when the
user has no filters set, since "everything matches" makes the chips
meaningless.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
EiSiMo 2026-04-23 11:27:49 +02:00
parent b5b4908ee7
commit fe43a402d8
3 changed files with 75 additions and 18 deletions

View file

@ -28,7 +28,7 @@ from common import (
templates,
)
from berlin_districts import DISTRICTS, district_for_address
from matching import flat_matches_filter, row_to_dict
from matching import flat_filter_failures, row_to_dict
router = APIRouter()
@ -50,7 +50,9 @@ def _wohnungen_context(user) -> dict:
# One query for this user's latest application per flat, instead of a
# per-flat query inside the loop.
latest_apps = db.latest_applications_by_flat(uid)
has_filters_set = _has_filters(filters_row)
flats_view = []
filtered_out_view = []
for f in flats:
if f["id"] in rejected:
continue
@ -62,11 +64,17 @@ def _wohnungen_context(user) -> dict:
disc = disc.replace(tzinfo=timezone.utc)
if disc < age_cutoff:
continue
if not flat_matches_filter({
failures = flat_filter_failures({
"rooms": f["rooms"], "total_rent": f["total_rent"], "size": f["size"],
"wbs": f["wbs"],
"district": district_for_address(f["address"]),
}, filters):
}, filters)
if failures:
# Only surface a "Rausgefiltert" entry if the user actually has
# filters set — otherwise nothing fails and the section is empty
# anyway, but skip the work to keep the loop tight.
if has_filters_set:
filtered_out_view.append({"row": f, "reasons": failures})
continue
flats_view.append({"row": f, "last": latest_apps.get(f["id"])})
@ -124,6 +132,7 @@ def _wohnungen_context(user) -> dict:
return {
"flats": flats_view,
"rejected_flats": rejected_view,
"filtered_out_flats": filtered_out_view,
"enrichment_counts": enrichment_counts,
"partner": partner_info,
"map_points": map_points,