UX: alarm-status, ablehnen-button, annika-footer, map polish
* Alarm-Status ist jetzt nur 'aktiv' wenn ein echter Push-Channel (Telegram mit Token+Chat oder E-Mail mit Adresse) konfiguriert ist. UI-only zählt nicht mehr als eingerichteter Alarm. * Ablehnen-Button in der Wohnungsliste: flat_rejections (migration v4) speichert pro-User-Ablehnungen, abgelehnte Flats fallen aus Liste und Karte raus. Wiederholbar pro User unabhängig. * Footer 'Programmiert für Annika ♥' erscheint nur auf Seiten, wenn annika angemeldet ist. * Map: Hinweistext unter leerer Karte entfernt; alle Zoom-Mechanismen deaktiviert (Scrollrad, Doppelklick, Box, Touch, Tastatur, +/- Buttons). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
376551213a
commit
42377f0b67
5 changed files with 97 additions and 18 deletions
47
web/app.py
47
web/app.py
|
|
@ -217,16 +217,25 @@ def _has_filters(f) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def _alert_status(filters_row, notifications_row) -> tuple[str, str]:
|
||||
"""Return (label, chip_kind) describing the user's alert setup."""
|
||||
if not _has_filters(filters_row):
|
||||
def _alert_status(notifications_row) -> tuple[str, str]:
|
||||
"""Return (label, chip_kind) for the user's alarm (notification) setup.
|
||||
|
||||
'aktiv' only if a real push channel (telegram/email) is configured with
|
||||
credentials. 'ui' is not a real alarm — the dashboard already shows
|
||||
matches when you happen to be looking.
|
||||
"""
|
||||
if not notifications_row:
|
||||
return "nicht eingerichtet", "warn"
|
||||
ch = (notifications_row["channel"] or "ui").strip()
|
||||
if ch == "telegram":
|
||||
if notifications_row["telegram_bot_token"] and notifications_row["telegram_chat_id"]:
|
||||
return "aktiv (Telegram)", "ok"
|
||||
return "unvollständig", "warn"
|
||||
if ch == "email":
|
||||
if notifications_row["email_address"]:
|
||||
return "aktiv (E-Mail)", "ok"
|
||||
return "unvollständig", "warn"
|
||||
return "nicht eingerichtet", "warn"
|
||||
ch = (notifications_row["channel"] if notifications_row else "ui") or "ui"
|
||||
if ch == "telegram" and not (notifications_row["telegram_bot_token"] and notifications_row["telegram_chat_id"]):
|
||||
return "Benachrichtigung fehlt", "warn"
|
||||
if ch == "email" and not notifications_row["email_address"]:
|
||||
return "Benachrichtigung fehlt", "warn"
|
||||
return "aktiv", "ok"
|
||||
|
||||
|
||||
def _filter_summary(f) -> str:
|
||||
|
|
@ -382,8 +391,11 @@ def _wohnungen_context(user) -> dict:
|
|||
filters = row_to_dict(filters_row)
|
||||
flats = db.recent_flats(100)
|
||||
|
||||
rejected = db.rejected_flat_ids(uid)
|
||||
flats_view = []
|
||||
for f in flats:
|
||||
if f["id"] in rejected:
|
||||
continue
|
||||
if not flat_matches_filter({
|
||||
"rooms": f["rooms"], "total_rent": f["total_rent"], "size": f["size"],
|
||||
"wbs": f["wbs"], "connectivity": {"morning_time": f["connectivity_morning_time"]},
|
||||
|
|
@ -393,7 +405,7 @@ def _wohnungen_context(user) -> dict:
|
|||
flats_view.append({"row": f, "last": last})
|
||||
|
||||
allowed, reason = _manual_apply_allowed()
|
||||
alert_label, alert_chip = _alert_status(filters_row, notif_row)
|
||||
alert_label, alert_chip = _alert_status(notif_row)
|
||||
has_running = _has_running_application(uid)
|
||||
map_points = []
|
||||
for item in flats_view:
|
||||
|
|
@ -411,6 +423,7 @@ def _wohnungen_context(user) -> dict:
|
|||
return {
|
||||
"flats": flats_view,
|
||||
"map_points": map_points,
|
||||
"has_filters": _has_filters(filters_row),
|
||||
"alert_label": alert_label,
|
||||
"alert_chip": alert_chip,
|
||||
"filter_summary": _filter_summary(filters_row),
|
||||
|
|
@ -521,6 +534,20 @@ async def action_apply(
|
|||
return _wohnungen_partial_or_redirect(request, user)
|
||||
|
||||
|
||||
@app.post("/actions/reject")
|
||||
async def action_reject(
|
||||
request: Request,
|
||||
flat_id: str = Form(...),
|
||||
csrf: str = Form(...),
|
||||
user=Depends(require_user),
|
||||
):
|
||||
require_csrf(user["id"], csrf)
|
||||
db.reject_flat(user["id"], flat_id)
|
||||
db.log_audit(user["username"], "flat.rejected", f"flat_id={flat_id}",
|
||||
user_id=user["id"], ip=client_ip(request))
|
||||
return _wohnungen_partial_or_redirect(request, user)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tab: Bewerbungen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
37
web/db.py
37
web/db.py
|
|
@ -185,6 +185,16 @@ MIGRATIONS: list[str] = [
|
|||
ALTER TABLE flats ADD COLUMN lat REAL;
|
||||
ALTER TABLE flats ADD COLUMN lng REAL;
|
||||
""",
|
||||
# 0004: per-user rejections — flats the user doesn't want in the list anymore
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS flat_rejections (
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
flat_id TEXT NOT NULL REFERENCES flats(id),
|
||||
rejected_at TEXT NOT NULL,
|
||||
PRIMARY KEY (user_id, flat_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_rejections_user ON flat_rejections(user_id);
|
||||
""",
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -471,6 +481,33 @@ def recent_applications(user_id: Optional[int], limit: int = 50) -> list[sqlite3
|
|||
).fetchall())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rejections (flats a user doesn't want to see anymore)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def reject_flat(user_id: int, flat_id: str) -> None:
|
||||
with _lock:
|
||||
_conn.execute(
|
||||
"INSERT OR IGNORE INTO flat_rejections(user_id, flat_id, rejected_at) VALUES (?, ?, ?)",
|
||||
(user_id, flat_id, now_iso()),
|
||||
)
|
||||
|
||||
|
||||
def unreject_flat(user_id: int, flat_id: str) -> None:
|
||||
with _lock:
|
||||
_conn.execute(
|
||||
"DELETE FROM flat_rejections WHERE user_id = ? AND flat_id = ?",
|
||||
(user_id, flat_id),
|
||||
)
|
||||
|
||||
|
||||
def rejected_flat_ids(user_id: int) -> set[str]:
|
||||
rows = _conn.execute(
|
||||
"SELECT flat_id FROM flat_rejections WHERE user_id = ?", (user_id,)
|
||||
).fetchall()
|
||||
return {row["flat_id"] for row in rows}
|
||||
|
||||
|
||||
def last_application_for_flat(user_id: int, flat_id: str) -> Optional[sqlite3.Row]:
|
||||
return _conn.execute(
|
||||
"""SELECT * FROM applications
|
||||
|
|
|
|||
|
|
@ -14,7 +14,14 @@ function initFlatsMap() {
|
|||
try { mapInstance.remove(); } catch (e) {}
|
||||
mapInstance = null;
|
||||
}
|
||||
mapInstance = L.map(el).setView(BERLIN_CENTER, BERLIN_ZOOM);
|
||||
mapInstance = L.map(el, {
|
||||
zoomControl: false,
|
||||
scrollWheelZoom: false,
|
||||
doubleClickZoom: false,
|
||||
boxZoom: false,
|
||||
touchZoom: false,
|
||||
keyboard: false,
|
||||
}).setView(BERLIN_CENTER, BERLIN_ZOOM);
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: "© OpenStreetMap",
|
||||
maxZoom: 18,
|
||||
|
|
|
|||
|
|
@ -29,4 +29,9 @@
|
|||
<main class="max-w-6xl mx-auto px-6 py-6 space-y-6">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
{% if user.username == 'annika' %}
|
||||
<footer class="text-center text-xs text-slate-500 py-6">
|
||||
Programmiert für Annika ♥
|
||||
</footer>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -100,12 +100,6 @@
|
|||
<section class="view-map">
|
||||
<div class="card p-3">
|
||||
<div id="flats-map" data-flats='{{ map_points | tojson }}'></div>
|
||||
{% if not map_points %}
|
||||
<p class="mt-3 text-xs text-slate-500">
|
||||
Keine Koordinaten für passende Wohnungen vorhanden —
|
||||
entweder sind noch keine neuen Flats geocoded worden oder die Filter lassen noch nichts durch.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -153,11 +147,20 @@
|
|||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="/actions/reject"
|
||||
hx-post="/actions/reject" hx-target="#wohnungen-body" hx-swap="outerHTML">
|
||||
<input type="hidden" name="csrf" value="{{ csrf }}">
|
||||
<input type="hidden" name="flat_id" value="{{ f.id }}">
|
||||
<button class="btn btn-ghost text-sm" type="submit"
|
||||
hx-confirm="Ablehnen und aus der Liste entfernen?">
|
||||
Ablehnen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="px-4 py-8 text-center text-slate-500">
|
||||
{% if alert_label == 'nicht eingerichtet' %}
|
||||
{% if not has_filters %}
|
||||
Bitte zuerst Filter einstellen, damit passende Wohnungen angezeigt werden.
|
||||
{% else %}
|
||||
Aktuell keine Wohnung, die alle Filter erfüllt.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue