diff --git a/web/notifications.py b/web/notifications.py index baa32da..f8c36c4 100644 --- a/web/notifications.py +++ b/web/notifications.py @@ -7,6 +7,7 @@ Channels: """ import logging from typing import Optional +from urllib.parse import quote import requests @@ -70,15 +71,75 @@ def notify_user(user_id: int, event: EventType, *, # -- Convenience builders ----------------------------------------------------- +def _address_lines(address: str) -> tuple[str, str]: + """Split "Street Nr, PLZ, District" into (line1, line2) for the two-line + layout in notifications. Falls back gracefully for unexpected shapes.""" + parts = [p.strip() for p in (address or "").split(",") if p.strip()] + if len(parts) >= 3: + return parts[0], f"{parts[1]} {', '.join(parts[2:])}" + if len(parts) == 2: + return parts[0], parts[1] + return (address or "").strip(), "" + + +def _gmaps_url(address: str, lat, lng) -> str: + if lat is not None and lng is not None: + return f"https://www.google.com/maps/search/?api=1&query={lat},{lng}" + return f"https://www.google.com/maps/search/?api=1&query={quote(address or '')}" + + +def _wbs_label(wbs: str) -> str: + w = (wbs or "").strip().lower() + if w == "erforderlich": + return "erforderlich" + if w in ("nicht erforderlich", "kein", "nein", "no", "ohne", "-", ""): + return "nicht erforderlich" + return wbs # pass through unrecognised literals + + +def _fmt_num(v) -> str: + return "—" if v is None else f"{v}" + + def on_match(user_id: int, flat: dict) -> None: - addr = flat.get("address") or flat.get("link") - rent = flat.get("total_rent") - rooms = flat.get("rooms") + address = (flat.get("address") or "").strip() + line1, line2 = _address_lines(address) link = flat.get("link", "") - body = f"Neue passende Wohnung: {addr}\nMiete: {rent} €\nZimmer: {rooms}\n{link}" - md = (f"*Neue passende Wohnung*\n[{addr}]({link})\n" - f"Miete: {rent} € · Zimmer: {rooms}\n{PUBLIC_URL}") - notify_user(user_id, "match", subject="[lazyflat] passende Wohnung", body_plain=body, body_markdown=md) + rent = flat.get("total_rent") + sqm_price = flat.get("sqm_price") + rooms = flat.get("rooms") + size = flat.get("size") + wbs_txt = _wbs_label(flat.get("wbs", "")) + gmaps = flat.get("address_link_gmaps") or _gmaps_url( + address, flat.get("lat"), flat.get("lng"), + ) + flat_id = str(flat.get("id", "")) + deep_link = f"{PUBLIC_URL}/?flat={quote(flat_id, safe='')}" + + rent_str = _fmt_num(rent) + if sqm_price: + rent_str += f" ({sqm_price} €/m²)" + + body = ( + f"{line1}\n{line2}\n" + f"Miete: {rent_str}\n" + f"Fläche: {_fmt_num(size)}\n" + f"Zimmer: {_fmt_num(rooms)}\n" + f"WBS: {wbs_txt}\n\n" + f"Original: {link}\n" + f"lazyflat: {deep_link}" + ) + md = ( + f"[{line1}]({gmaps})\n{line2}\n" + f"Miete: {rent_str}\n" + f"Fläche: {_fmt_num(size)}\n" + f"Zimmer: {_fmt_num(rooms)}\n" + f"WBS: {wbs_txt}\n\n" + f"[Zur original Anzeige]({link})\n" + f"[Zur lazyflat Seite]({deep_link})" + ) + notify_user(user_id, "match", subject="[lazyflat] passende Wohnung", + body_plain=body, body_markdown=md) def on_apply_ok(user_id: int, flat: dict, message: str) -> None: diff --git a/web/static/app.css b/web/static/app.css index 0382c07..a438e02 100644 --- a/web/static/app.css +++ b/web/static/app.css @@ -115,6 +115,13 @@ body:has(#v_map:checked) .view-map { display: block; } .flat-detail { background: #fafcfe; border-top: 1px solid var(--border); } .flat-detail:empty { display: none; } +/* Temporary row highlight after arriving via a Telegram deep-link (?flat=…). */ +.flat-highlight { animation: flat-highlight-pulse 3s ease-out; } +@keyframes flat-highlight-pulse { + 0% { background-color: #fff4dd; } + 100% { background-color: transparent; } +} + /* Normalised image gallery — every tile has the same aspect ratio */ .flat-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); diff --git a/web/static/app.js b/web/static/app.js index bae21c2..fb88a94 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -95,3 +95,36 @@ document.addEventListener("click", (ev) => { pane.innerHTML = '
Detail konnte nicht geladen werden.
'; }); }); + +// Deep-link landing: ?flat= on first load expands + highlights that row. +// Used from Telegram match notifications so "Zur lazyflat Seite" jumps +// straight to the relevant card. +function openDeepLinkedFlat() { + const params = new URLSearchParams(location.search); + const targetId = params.get("flat"); + if (!targetId) return; + let found = null; + for (const btn of document.querySelectorAll(".flat-expand-btn")) { + if (btn.dataset.flatId === targetId) { found = btn; break; } + } + if (!found) { + // Flat not in the visible list (filter/age/rejected). Drop the param so + // a reload doesn't keep failing silently. + params.delete("flat"); + const qs = params.toString(); + history.replaceState(null, "", location.pathname + (qs ? "?" + qs : "") + location.hash); + return; + } + const row = found.closest(".flat-row"); + if (!found.classList.contains("open")) found.click(); + if (row) { + row.scrollIntoView({ behavior: "smooth", block: "center" }); + row.classList.add("flat-highlight"); + setTimeout(() => row.classList.remove("flat-highlight"), 3000); + } + params.delete("flat"); + const qs = params.toString(); + history.replaceState(null, "", location.pathname + (qs ? "?" + qs : "") + location.hash); +} + +document.addEventListener("DOMContentLoaded", openDeepLinkedFlat);