ui: WBS dropdown, decimal-room filters, segmented toggle, 'Final absenden'

* Einstellungen → Profil: WBS-Typ jetzt <select> mit WBS 100/140/160/180/220
* Einstellungen → Filter: Zimmer min/max als number-Feld mit step=0.5
  (2.5-Zimmer-Wohnungen sauber eingebbar)
* Wohnungen-Top-Leiste: Segmented-Toggle (ein zusammenhängender Kippschalter)
  für die beiden Schalter, keine einzelnen Radio-Pills mehr
* Trockenmodus umbenannt in 'Final absenden' (positive Polarität: An=echt
  senden). Bestätigungsdialog beim Einschalten.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Moritz 2026-04-21 11:50:50 +02:00
parent 120d14e918
commit d9468f6814
4 changed files with 73 additions and 42 deletions

View file

@ -8,11 +8,15 @@
<input type="hidden" name="csrf" value="{{ csrf }}"> <input type="hidden" name="csrf" value="{{ csrf }}">
<div> <div>
<label class="block text-xs uppercase tracking-wide text-slate-500 mb-1">Zimmer min</label> <label class="block text-xs uppercase tracking-wide text-slate-500 mb-1">Zimmer min</label>
<input class="input" name="rooms_min" value="{{ filters.rooms_min if filters.rooms_min is not none else '' }}"> <input class="input" type="number" step="0.5" min="0" max="20"
name="rooms_min" value="{{ filters.rooms_min if filters.rooms_min is not none else '' }}"
placeholder="z.B. 2 oder 2.5">
</div> </div>
<div> <div>
<label class="block text-xs uppercase tracking-wide text-slate-500 mb-1">Zimmer max</label> <label class="block text-xs uppercase tracking-wide text-slate-500 mb-1">Zimmer max</label>
<input class="input" name="rooms_max" value="{{ filters.rooms_max if filters.rooms_max is not none else '' }}"> <input class="input" type="number" step="0.5" min="0" max="20"
name="rooms_max" value="{{ filters.rooms_max if filters.rooms_max is not none else '' }}"
placeholder="z.B. 3.5">
</div> </div>
<div> <div>
<label class="block text-xs uppercase tracking-wide text-slate-500 mb-1">max Miete (€)</label> <label class="block text-xs uppercase tracking-wide text-slate-500 mb-1">max Miete (€)</label>

View file

@ -58,7 +58,12 @@
</label> </label>
<div> <div>
<label class="block text-xs uppercase text-slate-500 mb-1">WBS-Typ</label> <label class="block text-xs uppercase text-slate-500 mb-1">WBS-Typ</label>
<input class="input" name="wbs_type" value="{{ profile.wbs_type }}" placeholder="z.B. 180"> <select class="input" name="wbs_type">
<option value="0" {% if not profile.wbs_type or profile.wbs_type == '0' %}selected{% endif %}></option>
{% for t in ['100','140','160','180','220'] %}
<option value="{{ t }}" {% if profile.wbs_type|string == t %}selected{% endif %}>WBS {{ t }}</option>
{% endfor %}
</select>
</div> </div>
<div> <div>
<label class="block text-xs uppercase text-slate-500 mb-1">gültig bis</label> <label class="block text-xs uppercase text-slate-500 mb-1">gültig bis</label>
@ -102,10 +107,10 @@
</form> </form>
<hr class="my-6 border-soft"> <hr class="my-6 border-soft">
<h3 class="font-semibold mb-2">Trockenmodus</h3> <h3 class="font-semibold mb-2">Final absenden</h3>
<p class="text-sm text-slate-600 mb-3"> <p class="text-sm text-slate-600 mb-3">
<span class="chip chip-warn">experimentell</span> <span class="chip chip-warn">experimentell</span>
Im Trockenmodus wird das Formular ausgefüllt, aber nicht abgesendet. Erst deaktivieren, Solange „Final absenden" aus ist, füllt die Automation das Formular aus, klickt aber
wenn du jeden Anbieter einmal im Trockenmodus erfolgreich getestet hast — den Schalter nicht auf „Senden". Erst einschalten, wenn du jeden Anbieter einmal durchgetestet hast.
findest du auch oben auf der Wohnungen-Seite. Der Schalter liegt auch oben auf der Wohnungen-Seite.
</p> </p>

View file

@ -19,51 +19,68 @@
</a> </a>
</section> </section>
<!-- Reihe 2: Schalter Automatisch bewerben + Trockenmodus (Radio-Gruppen) --> <!-- Reihe 2: Schalter Automatisch bewerben + Final absenden -->
<section class="grid grid-cols-1 md:grid-cols-2 gap-3"> <section class="grid grid-cols-1 md:grid-cols-2 gap-3">
<!-- Automatisch bewerben --> <!-- Automatisch bewerben -->
<form class="card p-4" <form class="card p-4 flex items-center justify-between gap-3">
hx-post="/actions/auto-apply"
hx-trigger="change"
hx-target="#wohnungen-body"
hx-swap="outerHTML">
<input type="hidden" name="csrf" value="{{ csrf }}"> <input type="hidden" name="csrf" value="{{ csrf }}">
<div class="text-[11px] uppercase tracking-wide text-slate-500 mb-2">Automatisch bewerben</div> <div class="flex flex-col gap-0.5">
<div class="flex gap-4"> <div class="text-[11px] uppercase tracking-wide text-slate-500">Automatisch bewerben</div>
<label class="radio-opt"> <div class="text-xs text-slate-500">bei Match ohne Nachfrage bewerben</div>
</div>
<div class="toggle warn">
<label>
<input type="radio" name="value" value="off" <input type="radio" name="value" value="off"
hx-post="/actions/auto-apply"
hx-trigger="change"
hx-include="closest form"
hx-target="#wohnungen-body"
hx-swap="outerHTML"
{% if not auto_apply_enabled %}checked{% endif %}> {% if not auto_apply_enabled %}checked{% endif %}>
<span>Aus</span> Aus
</label> </label>
<label class="radio-opt" <label>
{% if not auto_apply_enabled %}data-hx-confirm="Automatisches Bewerben einschalten? Bei jedem passenden Flat wird automatisch beworben."{% endif %}>
<input type="radio" name="value" value="on" <input type="radio" name="value" value="on"
hx-post="/actions/auto-apply"
hx-trigger="change"
hx-include="closest form"
hx-target="#wohnungen-body"
hx-swap="outerHTML"
hx-confirm="Automatisches Bewerben einschalten? Bei jedem passenden Flat wird automatisch beworben." hx-confirm="Automatisches Bewerben einschalten? Bei jedem passenden Flat wird automatisch beworben."
{% if auto_apply_enabled %}checked{% endif %}> {% if auto_apply_enabled %}checked{% endif %}>
<span>An</span> An
</label> </label>
</div> </div>
</form> </form>
<!-- Trockenmodus --> <!-- Final absenden (inverse of submit_forms: on=real, off=trocken) -->
<form class="card p-4" <form class="card p-4 flex items-center justify-between gap-3">
hx-post="/actions/submit-forms"
hx-trigger="change"
hx-target="#wohnungen-body"
hx-swap="outerHTML">
<input type="hidden" name="csrf" value="{{ csrf }}"> <input type="hidden" name="csrf" value="{{ csrf }}">
<div class="text-[11px] uppercase tracking-wide text-slate-500 mb-2">Trockenmodus</div> <div class="flex flex-col gap-0.5">
<div class="flex gap-4"> <div class="text-[11px] uppercase tracking-wide text-slate-500">Final absenden</div>
<label class="radio-opt"> <div class="text-xs text-slate-500">aus = Formular ausfüllen, nicht abschicken</div>
<input type="radio" name="value" value="on" </div>
{% if not submit_forms %}checked{% endif %}> <div class="toggle warn">
<span>An <span class="text-xs text-slate-500">(Formular ausfüllen, nicht absenden)</span></span> <label>
</label>
<label class="radio-opt">
<input type="radio" name="value" value="off" <input type="radio" name="value" value="off"
hx-confirm="Trockenmodus ausschalten? Formulare werden dann WIRKLICH abgesendet!" hx-post="/actions/submit-forms"
hx-trigger="change"
hx-include="closest form"
hx-target="#wohnungen-body"
hx-swap="outerHTML"
{% if not submit_forms %}checked{% endif %}>
Aus
</label>
<label>
<input type="radio" name="value" value="on"
hx-post="/actions/submit-forms"
hx-trigger="change"
hx-include="closest form"
hx-target="#wohnungen-body"
hx-swap="outerHTML"
hx-confirm="Final absenden einschalten? Formulare werden dann WIRKLICH abgeschickt!"
{% if submit_forms %}checked{% endif %}> {% if submit_forms %}checked{% endif %}>
<span>Aus <span class="text-xs text-[#b8404e]">(echt senden)</span></span> An
</label> </label>
</div> </div>
</form> </form>

View file

@ -47,12 +47,17 @@
.chip-warn { background: #fff4dd; color: #a36a1f; border: 1px solid #f5d48b; } .chip-warn { background: #fff4dd; color: #a36a1f; border: 1px solid #f5d48b; }
.chip-bad { background: #fde6e9; color: #b8404e; border: 1px solid #f5b5bf; } .chip-bad { background: #fde6e9; color: #b8404e; border: 1px solid #f5b5bf; }
.chip-info { background: #e3effc; color: #1f5f99; border: 1px solid #b6d4f0; } .chip-info { background: #e3effc; color: #1f5f99; border: 1px solid #b6d4f0; }
.radio-opt { display: inline-flex; align-items: center; gap: 0.45rem; cursor: pointer; padding: 0.35rem 0.65rem; /* Segmented toggle (An/Aus Kippschalter) */
border: 1px solid var(--border); border-radius: 10px; background: var(--surface); .toggle { display: inline-flex; border: 1px solid var(--border); border-radius: 999px;
transition: border-color .15s, background .15s; user-select: none; } overflow: hidden; background: var(--surface); font-size: 0.85rem; font-weight: 500; }
.radio-opt:has(input:checked) { border-color: var(--primary); background: #ecf4fc; box-shadow: 0 0 0 1px var(--primary) inset; } .toggle label { padding: 0.45rem 1.1rem; cursor: pointer; user-select: none;
.radio-opt:hover { background: var(--ghost); } color: var(--muted); transition: background .15s, color .15s; }
.radio-opt input[type="radio"] { accent-color: var(--primary); } .toggle label + label { border-left: 1px solid var(--border); }
.toggle label input[type="radio"] { position: absolute; opacity: 0; pointer-events: none;
width: 0; height: 0; }
.toggle label:hover { color: var(--text); background: var(--ghost); }
.toggle label:has(input:checked) { background: var(--primary); color: #fff; }
.toggle.warn label:has(input[value="on"]:checked) { background: var(--danger); }
.brand-dot { .brand-dot {
width: 2rem; height: 2rem; border-radius: 10px; width: 2rem; height: 2rem; border-radius: 10px;
background: linear-gradient(135deg, #66b7f2 0%, #2f8ae0 60%, #fbd76b 100%); background: linear-gradient(135deg, #66b7f2 0%, #2f8ae0 60%, #fbd76b 100%);