Exportbedingungen: Vergleichsart je Feldart statt nacktem Textfeld
Bisher tippte man den Wert immer von Hand ein - auch bei einem Haken, wo man wissen musste, dass intern "true" steht, und bei einer Auswahl, wo ein Vertipper stillschweigend nichts fand. Das ist der unangenehmste Fehlschlag: Die CSV kommt einfach leer heraus, ohne Hinweis warum. Jetzt bringt jede Bedingung die passende Eingabe mit: Ja/Nein ein Haken Auswahl "ist gleich" / "ist nicht gleich" plus das Dropdown der Optionen Zahl "ist groesser =" / "ist =" / "ist kleiner =" plus Zahlenfeld Text wie bisher freie Eingabe, leer = ohne Wert Damit die Oberflaeche das kann, traegt /etiketten/spalten jetzt Art und Auswahlwerte je Feld mit - vorher gab es nur Schluessel und Titel. Der Filter bekommt dafuer ein op (=, !=, >=, <=), Vorgabe "=", damit bestehende Vorlagen unveraendert weiterlaufen. Groesser/kleiner vergleicht als Zahl und nimmt Komma wie Punkt; Zeilen ohne Wert treffen dabei NIE - "leer" ist nicht kleiner als drei, es ist unvergleichbar. Eine unbekannte Vergleichsart faellt auf "=" zurueck statt den Export zu zerreissen. Die Sicherung traegt das op mit, wie schon das Feld selbst. Sieben neue Tests, 547 gruen. Diesmal zusaetzlich in der laufenden Oberflaeche durchgespielt: Alle vier Feldarten zeigen die richtige Eingabe, der Filter wird gespeichert, und die CSV enthaelt bei "Laenge >= 5" genau die Artikel mit 5 und 12 - nicht den mit 2 und nicht den ohne Wert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
dd57d3356a
commit
8de222072c
@@ -502,7 +502,11 @@ def _vorlagen_exportieren(db: Session) -> list[dict]:
|
|||||||
if fd is None:
|
if fd is None:
|
||||||
continue
|
continue
|
||||||
pfad = _category_path(db, db.get(Category, fd.category_id))
|
pfad = _category_path(db, db.get(Category, fd.category_id))
|
||||||
feldfilter.append({"feld": f"{pfad}::{fd.label}", "wert": bedingung["wert"]})
|
feldfilter.append({
|
||||||
|
"feld": f"{pfad}::{fd.label}",
|
||||||
|
"op": bedingung.get("op", "="),
|
||||||
|
"wert": bedingung["wert"],
|
||||||
|
})
|
||||||
|
|
||||||
zeilen.append({
|
zeilen.append({
|
||||||
"name": v.name,
|
"name": v.name,
|
||||||
@@ -573,7 +577,11 @@ def _vorlagen_importieren(db: Session, zeilen: list, errors: list[str]) -> int:
|
|||||||
# Wie bei den Spalten: fehlt das Feld hier, faellt die Bedingung
|
# Wie bei den Spalten: fehlt das Feld hier, faellt die Bedingung
|
||||||
# weg statt die ganze Vorlage scheitern zu lassen.
|
# weg statt die ganze Vorlage scheitern zu lassen.
|
||||||
if fd is not None:
|
if fd is not None:
|
||||||
feldfilter.append({"feld_id": fd.id, "wert": bedingung.get("wert", "")})
|
feldfilter.append({
|
||||||
|
"feld_id": fd.id,
|
||||||
|
"op": bedingung.get("op", "="),
|
||||||
|
"wert": bedingung.get("wert", ""),
|
||||||
|
})
|
||||||
|
|
||||||
kategorie_ids = [
|
kategorie_ids = [
|
||||||
pfad_zu_id[p] for p in entry.get("kategorien", []) if p in pfad_zu_id
|
pfad_zu_id[p] for p in entry.get("kategorien", []) if p in pfad_zu_id
|
||||||
|
|||||||
+15
-3
@@ -1124,17 +1124,29 @@ class SpalteOut(BaseModel):
|
|||||||
|
|
||||||
schluessel: str
|
schluessel: str
|
||||||
titel: str
|
titel: str
|
||||||
|
# Nur bei eigenen Feldern gefuellt. Die Oberflaeche braucht beides, um beim
|
||||||
|
# Wertefilter die passende Eingabe anzubieten (Checkbox, Auswahl, Zahl).
|
||||||
|
typ: str = ""
|
||||||
|
optionen: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
class FeldFilter(BaseModel):
|
class FeldFilter(BaseModel):
|
||||||
"""Eine Bedingung auf einem eigenen Feld.
|
"""Eine Bedingung auf einem eigenen Feld.
|
||||||
|
|
||||||
Mehrere Bedingungen muessen alle zutreffen. Verglichen wird genau, aber
|
Mehrere Bedingungen muessen alle zutreffen.
|
||||||
ohne Ruecksicht auf Gross-/Kleinschreibung. Ein leerer ``wert`` trifft die
|
|
||||||
Zeilen OHNE Wert in diesem Feld.
|
``op`` ist die Vergleichsart. Vorgabe ist ``=``, damit aeltere Vorlagen
|
||||||
|
ohne Angabe unveraendert weiterlaufen:
|
||||||
|
|
||||||
|
* ``=`` / ``!=`` – genau gleich bzw. ungleich, ohne Ruecksicht auf
|
||||||
|
Gross-/Kleinschreibung. Ein leerer ``wert`` trifft bei ``=`` die Zeilen
|
||||||
|
OHNE Wert in diesem Feld.
|
||||||
|
* ``>=`` / ``<=`` – nur bei Zahlenfeldern sinnvoll; Zeilen ohne Wert oder
|
||||||
|
mit unlesbarer Zahl treffen nie.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
feld_id: int
|
feld_id: int
|
||||||
|
op: str = Field(default="=", pattern=r"^(=|!=|>=|<=)$")
|
||||||
wert: str = ""
|
wert: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from typing import Any, Callable
|
|||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from .fields import options_list
|
||||||
from ..models import (
|
from ..models import (
|
||||||
Category,
|
Category,
|
||||||
EtikettenVorlage,
|
EtikettenVorlage,
|
||||||
@@ -192,7 +193,15 @@ def feld_spalten(db: Session, art: str, kategorie_ids: list[int]) -> list[dict]:
|
|||||||
continue
|
continue
|
||||||
gesehen.add(fd.id)
|
gesehen.add(fd.id)
|
||||||
titel = f"{fd.label} ({fd.unit})" if fd.unit else fd.label
|
titel = f"{fd.label} ({fd.unit})" if fd.unit else fd.label
|
||||||
spalten.append({"schluessel": f"{FELD_PRAEFIX}{fd.id}", "titel": titel})
|
# Art und Auswahlwerte kommen mit: Erst damit kann die Oberflaeche beim
|
||||||
|
# Wertefilter eine Checkbox, ein Dropdown oder ein Zahlenfeld anbieten
|
||||||
|
# statt eines nackten Textfelds, in das man sich vertippt.
|
||||||
|
spalten.append({
|
||||||
|
"schluessel": f"{FELD_PRAEFIX}{fd.id}",
|
||||||
|
"titel": titel,
|
||||||
|
"typ": fd.field_type,
|
||||||
|
"optionen": options_list(fd),
|
||||||
|
})
|
||||||
return spalten
|
return spalten
|
||||||
|
|
||||||
|
|
||||||
@@ -242,7 +251,14 @@ def konfiguration_lesen(vorlage: EtikettenVorlage) -> dict:
|
|||||||
if not str(fid).lstrip("-").isdigit():
|
if not str(fid).lstrip("-").isdigit():
|
||||||
continue
|
continue
|
||||||
wert = eintrag.get("wert", "")
|
wert = eintrag.get("wert", "")
|
||||||
feldfilter.append({"feld_id": int(fid), "wert": str(wert) if wert is not None else ""})
|
op = eintrag.get("op", "=")
|
||||||
|
if op not in ("=", "!=", ">=", "<="):
|
||||||
|
op = "="
|
||||||
|
feldfilter.append({
|
||||||
|
"feld_id": int(fid),
|
||||||
|
"op": op,
|
||||||
|
"wert": str(wert) if wert is not None else "",
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
"spalten": spalten,
|
"spalten": spalten,
|
||||||
"kategorie_ids": kategorie_ids,
|
"kategorie_ids": kategorie_ids,
|
||||||
@@ -274,7 +290,15 @@ def konfiguration_schreiben(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _feld_passt(fd: FieldDefinition, bedingung: str, ist: str | None) -> bool:
|
def _als_zahl(text: str | None) -> float | None:
|
||||||
|
"""Zahl aus dem gespeicherten Text – Komma gilt wie Punkt."""
|
||||||
|
try:
|
||||||
|
return float(str(text).replace(",", "."))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _feld_passt(fd: FieldDefinition, op: str, bedingung: str, ist: str | None) -> bool:
|
||||||
"""Trifft der gespeicherte Wert die Bedingung?
|
"""Trifft der gespeicherte Wert die Bedingung?
|
||||||
|
|
||||||
Ja/Nein ist der heikle Fall: Nimmt man den Haken weg, loescht
|
Ja/Nein ist der heikle Fall: Nimmt man den Haken weg, loescht
|
||||||
@@ -289,8 +313,20 @@ def _feld_passt(fd: FieldDefinition, bedingung: str, ist: str | None) -> bool:
|
|||||||
if fd.field_type == FieldType.boolean.value:
|
if fd.field_type == FieldType.boolean.value:
|
||||||
from .fields import coerce_value
|
from .fields import coerce_value
|
||||||
|
|
||||||
return (coerce_value(fd, ist) or "false") == (coerce_value(fd, bedingung) or "false")
|
gleich = (coerce_value(fd, ist) or "false") == (coerce_value(fd, bedingung) or "false")
|
||||||
return (ist or "").casefold() == (bedingung or "").strip().casefold()
|
return not gleich if op == "!=" else gleich
|
||||||
|
|
||||||
|
if op in (">=", "<="):
|
||||||
|
# Groesser/kleiner gibt es nur auf Zahlen. Ohne Wert oder mit
|
||||||
|
# unlesbarem Text trifft die Bedingung nie - „leer" ist nicht kleiner
|
||||||
|
# als drei, es ist unvergleichbar.
|
||||||
|
links, rechts = _als_zahl(ist), _als_zahl(bedingung)
|
||||||
|
if links is None or rechts is None:
|
||||||
|
return False
|
||||||
|
return links >= rechts if op == ">=" else links <= rechts
|
||||||
|
|
||||||
|
gleich = (ist or "").casefold() == (bedingung or "").strip().casefold()
|
||||||
|
return not gleich if op == "!=" else gleich
|
||||||
|
|
||||||
|
|
||||||
def _nach_feldwerten(db: Session, art: str, zeilen: list, feldfilter: list[dict]) -> list:
|
def _nach_feldwerten(db: Session, art: str, zeilen: list, feldfilter: list[dict]) -> list:
|
||||||
@@ -307,7 +343,7 @@ def _nach_feldwerten(db: Session, art: str, zeilen: list, feldfilter: list[dict]
|
|||||||
for eintrag in feldfilter:
|
for eintrag in feldfilter:
|
||||||
fd = db.get(FieldDefinition, eintrag["feld_id"])
|
fd = db.get(FieldDefinition, eintrag["feld_id"])
|
||||||
if fd is not None:
|
if fd is not None:
|
||||||
bedingungen.append((fd, eintrag["wert"]))
|
bedingungen.append((fd, eintrag.get("op", "="), eintrag["wert"]))
|
||||||
if not bedingungen:
|
if not bedingungen:
|
||||||
return zeilen
|
return zeilen
|
||||||
|
|
||||||
@@ -316,7 +352,7 @@ def _nach_feldwerten(db: Session, art: str, zeilen: list, feldfilter: list[dict]
|
|||||||
for zeile in zeilen:
|
for zeile in zeilen:
|
||||||
produkt_id = zeile.product_id if art == "items" else getattr(zeile, "id", None)
|
produkt_id = zeile.product_id if art == "items" else getattr(zeile, "id", None)
|
||||||
eigene = werte.get(produkt_id, {})
|
eigene = werte.get(produkt_id, {})
|
||||||
if all(_feld_passt(fd, wert, eigene.get(fd.id)) for fd, wert in bedingungen):
|
if all(_feld_passt(fd, op, wert, eigene.get(fd.id)) for fd, op, wert in bedingungen):
|
||||||
passend.append(zeile)
|
passend.append(zeile)
|
||||||
return passend
|
return passend
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ from app.models import (
|
|||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from app.routers.etiketten import _pruefe_lbx, datei_herunterladen, spalten
|
from app.routers.etiketten import _pruefe_lbx, datei_herunterladen, spalten
|
||||||
from app.services.etiketten import csv_erzeugen, konfiguration_lesen
|
from app.services.etiketten import csv_erzeugen, konfiguration_lesen, verfuegbare_spalten
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
@@ -608,7 +608,9 @@ def test_feldfilter_uebersteht_die_sicherung(db, haken):
|
|||||||
inhalt = export_backup_json(db=db, _=person).body
|
inhalt = export_backup_json(db=db, _=person).body
|
||||||
gesichert = _json.loads(inhalt)["etiketten_vorlagen"][0]
|
gesichert = _json.loads(inhalt)["etiketten_vorlagen"][0]
|
||||||
# Nach Pfad und Label, nie nach ID.
|
# Nach Pfad und Label, nie nach ID.
|
||||||
assert gesichert["feldfilter"] == [{"feld": "Kleidung::Verliehen", "wert": "ja"}]
|
assert gesichert["feldfilter"] == [
|
||||||
|
{"feld": "Kleidung::Verliehen", "op": "=", "wert": "ja"}
|
||||||
|
]
|
||||||
|
|
||||||
from app.database import Base
|
from app.database import Base
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
@@ -635,6 +637,99 @@ def test_feldfilter_uebersteht_die_sicherung(db, haken):
|
|||||||
).one()
|
).one()
|
||||||
konf = konfiguration_lesen(neu)
|
konf = konfiguration_lesen(neu)
|
||||||
# Auf die ID der ZWEITEN Instanz umgeschrieben, Wert unveraendert.
|
# Auf die ID der ZWEITEN Instanz umgeschrieben, Wert unveraendert.
|
||||||
assert konf["feldfilter"] == [{"feld_id": neues_feld.id, "wert": "ja"}]
|
assert konf["feldfilter"] == [{"feld_id": neues_feld.id, "op": "=", "wert": "ja"}]
|
||||||
assert _namen(zweite, neu) == ["Jacke"]
|
assert _namen(zweite, neu) == ["Jacke"]
|
||||||
zweite.close()
|
zweite.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_zahlenfeld_groesser_gleich(db, kabel):
|
||||||
|
cat, fd = kabel
|
||||||
|
_artikel(db, cat, "Kurz", fd, "2")
|
||||||
|
_artikel(db, cat, "Mittel", fd, "5")
|
||||||
|
_artikel(db, cat, "Lang", fd, "10")
|
||||||
|
v = _vorlage(db, ["name"], art="products",
|
||||||
|
feldfilter=[{"feld_id": fd.id, "op": ">=", "wert": "5"}])
|
||||||
|
|
||||||
|
assert sorted(_namen(db, v)) == ["Lang", "Mittel"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_zahlenfeld_kleiner_gleich_mit_komma(db, kabel):
|
||||||
|
"""Gespeichert wird der getippte Text – „2,5" muss als Zahl gelten."""
|
||||||
|
cat, fd = kabel
|
||||||
|
_artikel(db, cat, "Kurz", fd, "2,5")
|
||||||
|
_artikel(db, cat, "Lang", fd, "10")
|
||||||
|
v = _vorlage(db, ["name"], art="products",
|
||||||
|
feldfilter=[{"feld_id": fd.id, "op": "<=", "wert": "3"}])
|
||||||
|
|
||||||
|
assert _namen(db, v) == ["Kurz"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_groesser_gleich_trifft_artikel_ohne_wert_nicht(db, kabel):
|
||||||
|
"""„leer" ist nicht kleiner als drei – es ist unvergleichbar."""
|
||||||
|
cat, fd = kabel
|
||||||
|
_artikel(db, cat, "Mit Wert", fd, "9")
|
||||||
|
_artikel(db, cat, "Ohne Wert")
|
||||||
|
for op in (">=", "<="):
|
||||||
|
v = _vorlage(db, ["name"], art="products",
|
||||||
|
feldfilter=[{"feld_id": fd.id, "op": op, "wert": "3"}])
|
||||||
|
assert "Ohne Wert" not in _namen(db, v)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ungleich_bei_auswahlfeld(db):
|
||||||
|
cat = Category(name="Kleidung", tracking=CategoryTracking.object.value)
|
||||||
|
db.add(cat)
|
||||||
|
db.flush()
|
||||||
|
fd = FieldDefinition(
|
||||||
|
category_id=cat.id, label="Größe", key="groesse", field_type="select",
|
||||||
|
options=json.dumps(["S", "M", "L"]),
|
||||||
|
)
|
||||||
|
db.add(fd)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(cat)
|
||||||
|
db.refresh(fd)
|
||||||
|
|
||||||
|
_artikel(db, cat, "Klein", fd, "S")
|
||||||
|
_artikel(db, cat, "Mittel", fd, "M")
|
||||||
|
v = _vorlage(db, ["name"], art="products",
|
||||||
|
feldfilter=[{"feld_id": fd.id, "op": "!=", "wert": "S"}])
|
||||||
|
|
||||||
|
assert _namen(db, v) == ["Mittel"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_ungleich_bei_ja_nein(db, haken):
|
||||||
|
cat, fd = haken
|
||||||
|
_artikel(db, cat, "Jacke", fd, "true")
|
||||||
|
_artikel(db, cat, "Hose", fd, "false")
|
||||||
|
v = _vorlage(db, ["name"], art="products",
|
||||||
|
feldfilter=[{"feld_id": fd.id, "op": "!=", "wert": "ja"}])
|
||||||
|
|
||||||
|
assert _namen(db, v) == ["Hose"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_unbekannte_vergleichsart_faellt_auf_gleich_zurueck(db, haken):
|
||||||
|
cat, fd = haken
|
||||||
|
v = EtikettenVorlage(name="Krumm", art="products", konfiguration=json.dumps({
|
||||||
|
"spalten": ["name"],
|
||||||
|
"feldfilter": [{"feld_id": fd.id, "op": "~~", "wert": "ja"}],
|
||||||
|
}))
|
||||||
|
db.add(v)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
assert konfiguration_lesen(v)["feldfilter"][0]["op"] == "="
|
||||||
|
|
||||||
|
|
||||||
|
def test_auswahlfeld_liefert_seine_optionen_zur_spalte(db):
|
||||||
|
"""Die Oberflaeche kann nur dann ein Dropdown anbieten."""
|
||||||
|
cat = Category(name="Kleidung", tracking=CategoryTracking.object.value)
|
||||||
|
db.add(cat)
|
||||||
|
db.flush()
|
||||||
|
db.add(FieldDefinition(
|
||||||
|
category_id=cat.id, label="Größe", key="groesse", field_type="select",
|
||||||
|
options=json.dumps(["S", "M"]),
|
||||||
|
))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
spalten = verfuegbare_spalten(db, "products", [cat.id])
|
||||||
|
feld = [s for s in spalten if s["schluessel"].startswith("feld:")][0]
|
||||||
|
assert feld["typ"] == "select"
|
||||||
|
assert feld["optionen"] == ["S", "M"]
|
||||||
|
|||||||
@@ -155,6 +155,65 @@ export default function EtikettenVorlagen({ onError }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Bearbeiten einer einzelnen Vorlage. */
|
/** Bearbeiten einer einzelnen Vorlage. */
|
||||||
|
/**
|
||||||
|
* Die Eingabe einer Filterbedingung, passend zur Art des Feldes.
|
||||||
|
*
|
||||||
|
* Ohne das waere jede Bedingung ein nacktes Textfeld: Man muesste wissen, dass
|
||||||
|
* ein Haken intern „true" heisst und wie eine Auswahl genau geschrieben ist.
|
||||||
|
* Ein Vertipper faende dann stillschweigend nichts – der unangenehmste
|
||||||
|
* Fehlschlag, weil die CSV einfach leer herauskommt.
|
||||||
|
*/
|
||||||
|
function FilterEingabe({ feld, bedingung, onAendern }) {
|
||||||
|
const typ = feld?.typ || "text";
|
||||||
|
|
||||||
|
if (typ === "boolean") {
|
||||||
|
return (
|
||||||
|
<label className="check-inline" style={{ margin: 0 }}>
|
||||||
|
<input type="checkbox" checked={bedingung.wert === "true"}
|
||||||
|
onChange={(e) => onAendern({ op: "=", wert: e.target.checked ? "true" : "false" })} />
|
||||||
|
<span className="muted small">{bedingung.wert === "true" ? "ja" : "nein"}</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typ === "select") {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<select value={bedingung.op} style={{ width: 110 }}
|
||||||
|
onChange={(e) => onAendern({ op: e.target.value })}>
|
||||||
|
<option value="=">ist gleich</option>
|
||||||
|
<option value="!=">ist nicht gleich</option>
|
||||||
|
</select>
|
||||||
|
<select value={bedingung.wert} style={{ width: 120 }}
|
||||||
|
onChange={(e) => onAendern({ wert: e.target.value })}>
|
||||||
|
{(feld.optionen || []).map((o) => <option key={o} value={o}>{o}</option>)}
|
||||||
|
</select>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typ === "number") {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<select value={bedingung.op} style={{ width: 110 }}
|
||||||
|
onChange={(e) => onAendern({ op: e.target.value })}>
|
||||||
|
<option value=">=">ist größer =</option>
|
||||||
|
<option value="=">ist =</option>
|
||||||
|
<option value="<=">ist kleiner =</option>
|
||||||
|
</select>
|
||||||
|
<input value={bedingung.wert} inputMode="decimal" style={{ width: 90 }}
|
||||||
|
onChange={(e) => onAendern({ wert: e.target.value })} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text, mehrzeiliger Text und Datum: freie Eingabe, leer = ohne Wert.
|
||||||
|
return (
|
||||||
|
<input value={bedingung.wert} placeholder="ohne Wert" style={{ width: 120 }}
|
||||||
|
onChange={(e) => onAendern({ wert: e.target.value })} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function VorlageEditor({ vorlage, onGespeichert, onError }) {
|
function VorlageEditor({ vorlage, onGespeichert, onError }) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [art, setArt] = useState(vorlage.art);
|
const [art, setArt] = useState(vorlage.art);
|
||||||
@@ -265,18 +324,21 @@ function VorlageEditor({ vorlage, onGespeichert, onError }) {
|
|||||||
Nur Zeilen mit bestimmten Feldwerten (alle Bedingungen müssen zutreffen)
|
Nur Zeilen mit bestimmten Feldwerten (alle Bedingungen müssen zutreffen)
|
||||||
</span>
|
</span>
|
||||||
<ul className="clean-list" style={{ marginTop: "var(--sp-2)" }}>
|
<ul className="clean-list" style={{ marginTop: "var(--sp-2)" }}>
|
||||||
{feldfilter.map((f, i) => (
|
{feldfilter.map((f, i) => {
|
||||||
<li key={f.feld_id}>
|
const feld = moeglich.find((m) => m.schluessel === `feld:${f.feld_id}`);
|
||||||
<span style={{ flex: 1, minWidth: 0 }}>{titelVon(`feld:${f.feld_id}`)}</span>
|
const aendern = (teil) => setFeldfilter(feldfilter.map((x, j) =>
|
||||||
<input value={f.wert} placeholder="ohne Wert" style={{ width: 120 }}
|
(j === i ? { ...x, ...teil } : x)));
|
||||||
onChange={(e) => setFeldfilter(feldfilter.map((x, j) =>
|
return (
|
||||||
(j === i ? { ...x, wert: e.target.value } : x)))} />
|
<li key={f.feld_id}>
|
||||||
<button type="button" className="btn-icon danger" title="Bedingung entfernen"
|
<span style={{ flex: 1, minWidth: 0 }}>{titelVon(`feld:${f.feld_id}`)}</span>
|
||||||
onClick={() => setFeldfilter(feldfilter.filter((_, j) => j !== i))}>
|
<FilterEingabe feld={feld} bedingung={f} onAendern={aendern} />
|
||||||
<Icon name="close" size={14} />
|
<button type="button" className="btn-icon danger" title="Bedingung entfernen"
|
||||||
</button>
|
onClick={() => setFeldfilter(feldfilter.filter((_, j) => j !== i))}>
|
||||||
</li>
|
<Icon name="close" size={14} />
|
||||||
))}
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
<select value="" style={{ marginTop: "var(--sp-1)" }}
|
<select value="" style={{ marginTop: "var(--sp-1)" }}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -284,8 +346,13 @@ function VorlageEditor({ vorlage, onGespeichert, onError }) {
|
|||||||
if (!id) return;
|
if (!id) return;
|
||||||
// Ein Feld nur einmal: zwei Bedingungen darauf koennten sich nie
|
// Ein Feld nur einmal: zwei Bedingungen darauf koennten sich nie
|
||||||
// beide erfuellen.
|
// beide erfuellen.
|
||||||
|
const feld = moeglich.find((m) => m.schluessel === e.target.value);
|
||||||
|
// Ja/Nein startet als gesetzter Haken, eine Auswahl auf ihrem
|
||||||
|
// ersten Wert - so steht nie eine Bedingung da, die nichts trifft.
|
||||||
|
const start = feld?.typ === "boolean" ? "true"
|
||||||
|
: feld?.typ === "select" ? (feld.optionen?.[0] || "") : "";
|
||||||
setFeldfilter([...feldfilter.filter((x) => x.feld_id !== id),
|
setFeldfilter([...feldfilter.filter((x) => x.feld_id !== id),
|
||||||
{ feld_id: id, wert: "" }]);
|
{ feld_id: id, op: "=", wert: start }]);
|
||||||
}}>
|
}}>
|
||||||
<option value="">+ Bedingung auf einem eigenen Feld …</option>
|
<option value="">+ Bedingung auf einem eigenen Feld …</option>
|
||||||
{moeglich.filter((m) => m.schluessel.startsWith("feld:")).map((m) => (
|
{moeglich.filter((m) => m.schluessel.startsWith("feld:")).map((m) => (
|
||||||
@@ -293,9 +360,10 @@ function VorlageEditor({ vorlage, onGespeichert, onError }) {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<p className="muted small">
|
<p className="muted small">
|
||||||
Wert genau so eingeben, wie er im Feld steht; bei Ja/Nein-Feldern
|
Bei Textfeldern genau so eingeben, wie es im Feld steht;{" "}
|
||||||
„ja" oder „nein". <strong>Leer heißt: ohne Wert</strong> – also alle,
|
<strong>leer heißt dort: ohne Wert</strong> – also alle, bei denen
|
||||||
bei denen das Feld nicht ausgefüllt ist.
|
das Feld nicht ausgefüllt ist. Größer/kleiner vergleicht als Zahl;
|
||||||
|
Zeilen ohne Wert treffen dabei nie.
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user