Zum Inhalt

Geführte Preiseinrichtung in V2

Kurzfassung. Die Preisverwaltung kann heute alles, was sie können muss — aber sie sagt niemandem, wie man sie benutzt. Wer die Preisliste öffnet, sieht ein leeres Grid und legt daraufhin naheliegenderweise eine Zeile je Gerätetyp an. Genau das ist der Weg, der bei 20.000 Typen nicht mehr pflegbar ist. Dieses Dokument beschreibt, wie der empfohlene Weg (Preisgruppen als Segmente, Bepreisung auf Gruppenebene statt je Typ, Ableitung per Duplizieren) in der Anwendung sichtbar, erklärt und messbar wird, und wie der Bestand ohne Bruch dorthin überführt wird.

Ergänzt konzept-preisverwaltung-v2.md (das Modell) um die Rollout- und Bedienseite. Modellentscheidungen stehen dort und in ADR-012, ADR-029 und ADR-032, nicht hier.


1. Ausgangslage

Der Ist-Stand wurde am Code erhoben, nicht angenommen. Was funktioniert:

  • PriceFinderService löst Gerätetyp × Preisgruppe × Gültigkeit (optional Komplexität) sauber und treiberunabhängig auf.
  • Preisliste (pages/price/index.vue), Matrix mit Stichtag und Abdeckungsanzeige (pages/price/matrix.vue), Preisgruppen samt Duplizieren-mit-Faktor (pages/price-category/) sind gebaut.
  • Der Preisinfos-Dialog je Auftragsposition (components/booking/BookingPositionPrices.vue) zeigt alle Gruppenpreise eines Typs und markiert die Kundengruppe.

Was fehlt, und zwar nicht als Funktion, sondern als Führung:

  1. Kein Einstieg, der die Strategie erklärt. /price ist ein flaches Grid. Nichts sagt, dass zuerst Preisgruppen und Kundenzuordnung kommen und dass auf Gruppenebene bepreist werden sollte.
  2. Die teuerste Regel ist unsichtbar. Heute gibt es keine Vererbung zwischen Preisgruppen: ein Kunde mit Gruppe sieht ausschließlich Zeilen seiner Gruppe, ein Kunde ohne Gruppe ausschließlich gruppenlose Zeilen (PriceFinderService.php:50-56). Mischbetrieb erzeugt still Positionen zu 0,00, und das steht nirgends in der Oberfläche. ADR-032 schafft diese Regel ab (Gruppen erben, prozentual oder mit fixem Aufschlag). Bis dahin muss die Oberfläche sie wenigstens benennen.
  3. Das Kopierproblem ist nicht messbar. Dass derselbe Betrag auf einem Dutzend Gerätetypen steht, sieht man nur, wenn man danach sucht. Es gibt keine Kennzahl, die den Pflegeaufwand sichtbar macht, bevor er weh tut.
  4. Preise hängen weiterhin am Gerätetyp. Das ist der Zustand, den ADR-032 mit der Preiskategorie als Container ablösen will. Gebaut ist davon nichts. Zwischen „heute" und „ADR-032 fertig" liegt eine Strecke, die überbrückt werden muss, ohne dass die Anwendung in der Zwischenzeit in die falsche Richtung erzieht.

Der Kern des Problems ist nicht fehlende Funktion, sondern eine fehlende Erzählung. Die Anwendung beantwortet „was kann ich hier tun?", aber nicht „was soll ich als Nächstes tun und warum?".


2. Leitgedanke: Statusseite statt Assistent

Naheliegend wäre ein Einrichtungsassistent (modaler Wizard, fünf Schritte, einmal durchlaufen). Dagegen sprechen drei Dinge:

  • Der Zustand ändert sich dauerhaft: neue Gerätetypen, neue Kunden, neue Preisgruppen, später Prozeduren. Ein einmal durchlaufener Assistent veraltet am Tag nach dem Durchlauf.
  • Labore richten über Wochen ein, nicht in einer Sitzung, oft mit mehreren Personen.
  • Ein Assistent, den man wegklicken kann, wird weggeklickt.

Deshalb: eine dauerhafte Übersichtsseite, die immer den Ist-Zustand und den nächsten sinnvollen Schritt zeigt. Sie ist nach der Einrichtung nicht wertlos, sondern wird zum Kontrollinstrument („sind alle Gruppen noch abgedeckt?").

Der Assistent-Charakter entsteht durch einen einzigen hervorgehobenen Nächster-Schritt-Block, dessen Inhalt sich aus dem Zustand ableitet — nicht durch Modalität.

Die Erklärebene wird bestückt, nicht erfunden

Das Projekt hat das Muster bereits, inklusive erklärtem Ziel („überall kontextuelle Hinweise", frontend-v2/components/README-admin-ui-kit.md). Drei Ebenen, alle vorhanden:

Ebene Träger Pflegeort
Feld DynamicForm zeigt FieldConfig.description automatisch Backend-Feldregister (field_configuration), per Migration seedbar
Abschnitt/Seite AdminPage description, AdminSection #info, InfoHint Frontend-Komponente
Zustand Abdeckungs-Tag, Lückenmarkierung (Matrix kann das schon) aus API-Kennzahlen

Daraus folgt eine Regel für die Umsetzung: Feldbezogene Erklärungen gehören ins Feldregister, nicht in Vue-Templates. Sie sind dann übersetzbar, ohne Frontend-Änderung pflegbar und erscheinen automatisch in jedem Formular, das prices rendert.


3. Die Kennzahlen

Führung ohne Messung ist Dekoration. Fünf Zahlen tragen die gesamte Seite; alle sind aus vorhandenen Daten berechenbar, ohne Schemaänderung.

Kennzahl Aussage Berechnung
Kunden ohne Preisgruppe Hauptursache stiller 0,00-Positionen customers mit leerem price_category_id
Abdeckung je Preisgruppe Wo landen Positionen bei 0,00? vorhandene Matrix-Logik (covered/total) je Gruppe
Kopiencluster verdeckter Pflegeaufwand prices je Gruppe nach price gruppieren; Gruppen mit ≥ 2 Gerätetypen sind Cluster
Zeilenanteil in Clustern wie stark der Bestand schon von Hand klassifiziert wurde Zeilen in Clustern ÷ Zeilen gesamt
Ausnahmeanteil (ab Stufe 3) trägt ADR-032? Typen mit eigener prices-Zeile trotz Standardprozedur

Die Kopiencluster sind der Dreh- und Angelpunkt. Sie machen das Problem aus ADR-029/031 sichtbar, ohne dass eine Preisklasse oder eine Prozedur existieren muss: Wenn 14 Gerätetypen in derselben Gruppe 180,00 tragen, ist das eine Preisklasse, die jemand von Hand kopiert hat. Dieselbe Auswertung liefert später den Migrationsvorschlag („diese 14 Typen → eine Preiskategorie"). Eine Auswertung, zwei Zwecke.

Der Ausnahmeanteil ist die Zahl, die ADR-032 als Vorbedingung nennt („eigentlich ja" mit unbeziffertem Ausnahmeanteil). Sie wird erst berechenbar, wenn Standardprozeduren gepflegt sind, gehört aber jetzt schon in den Entwurf, damit sie nicht nachträglich angeflanscht wird.


4. Stufenplan

Die Stufen sind so geschnitten, dass jede für sich einen Nutzen liefert und keine die nächste voraussetzt, um sinnvoll zu sein.

Stufe 0 — Sichtbarkeit (kein Schema, keine Modelländerung)

Umgesetzt am 2026-08-04. GET /prices/overview + PriceOverviewService, Seite unter /price/overview als erster Tab und Ziel des Menüeintrags. Abweichung vom Entwurf: /price zieht nicht um, sonst brächen Bookmarks und Deep-Links. Die Führungswirkung entsteht über den Menüeintrag, der auf die Übersicht zeigt.

Ziel: Die Anwendung sagt, wo man steht.

  • Backend: GET /prices/overview (Recht price_view) liefert die fünf Kennzahlen plus je Preisgruppe: Kundenzahl, Preiszeilen, Abdeckung. Die Cluster-Auswertung als eigener Block mit den größten Clustern (Betrag, Gruppe, Anzahl Typen, Beispieltypen). Neuer PriceOverviewService (app/Services/Pricing/), Wiederverwendung von PriceMatrixService für die Abdeckung. Query-Builder, kein DB::raw().
  • Frontend: /price bekommt RouteTabs mit Übersicht | Preisliste | Matrix | Preisgruppen. Die Übersicht wird die Standardroute. Jede Kennzahl sitzt in einer AdminSection mit #info-Slot: Zahl, Bedeutung, Aktion.

Nutzen ohne alles Weitere: Ein Labor sieht sofort, wie viele Kunden ohne Preisgruppe sind und wo die Matrix Löcher hat. Das ist heute nur durch Durchklicken zu erfahren.

Stufe 1 — Führung und Erklärung

Umgesetzt am 2026-08-04. Regelkette (alle sechs Regeln inklusive Leitgruppe), Leitgruppen-Ermittlung im PriceOverviewService, Einstellungen settings.price.price_lead_group und price_target_coverage im SettingsCatalog, die Erklärtexte als Werksvorgabe in default_field_definitions.json plus Nachtrags-Migration für Bestände, sowie Lücken-Filter (only_gaps) und Sammelanlage (POST /prices/bulk-create) in der Matrix.

Ziel: Die Anwendung sagt, was als Nächstes zu tun ist, und warum.

  • Nächster-Schritt-Block auf der Übersicht, deterministische Regelkette (erste zutreffende Regel gewinnt):
# Bedingung Empfehlung
1 keine Preisgruppe vorhanden Erste Preisgruppe anlegen
2 Kunden ohne Preisgruppe > 0 Diese Kunden zuordnen (Liste, Sammelaktion)
3 Leitgruppe unter Zielabdeckung Leitgruppe bepreisen (in die Matrix, auf Lücken gefiltert)
4 weitere Gruppe deutlich unter Leitgruppe Aus der Leitgruppe ableiten (Duplizieren mit Faktor)
5 Kopiencluster über Schwelle Cluster ansehen — Kandidaten für Gruppenbepreisung
6 sonst Nichts zu tun; die Übersicht bleibt Kontrollinstrument

Die Leitgruppe ist die Gruppe mit den meisten Kunden, überschreibbar über eine Einstellung im vorhandenen SettingsCatalog (kein Feld auf price_category — das ist eine von V1 genutzte Tabelle).

  • Feldregister-Seed (Migration, additiv, idempotent) für prices und price_category. Die Texte sind Teil dieser Planung, nicht Beiwerk:

  • price_cat_uID: „Kundensegment, für das dieser Preis gilt. Derzeit gibt es keine Vererbung zwischen Preisgruppen: Ein Kunde in einer Gruppe bekommt ausschließlich Preise dieser Gruppe, ein Kunde ohne Gruppe ausschließlich Preise ohne Gruppe. Jede Gruppe braucht deshalb bis auf Weiteres eigene Preise." (Mit AP 11b wird dieser Text auf die dann geltende Ableitung umgestellt.)

  • type_tID: „Gerätetyp, für den dieser Preis gilt — er gilt nur für diesen einen Typ. Bepreise gleichartige Geräte gemeinsam, statt denselben Betrag auf viele Typen zu kopieren."
  • start_date / end_date: „Gültigkeitszeitraum. Eine Preisänderung wird als neue Zeile mit Startdatum angelegt, nicht durch Überschreiben — nur so bleibt belegbar, welcher Preis im März galt. Leer heißt unbegrenzt."
  • complexity: „Optionale Unterscheidung, etwa nach Kalibrierart. Bleibt das Feld leer, gilt die Zeile für alle Komplexitäten."
  • quantity / unit: „Bezugsmenge des Preises, keine Auftragsmenge."

  • Lücken-Einstieg in der Matrix: Filter „nur Lücken" plus Sammelanlage für markierte Zellen. Die Matrix zeigt Lücken schon; was fehlt, ist der Weg vom Sehen zum Beheben.

Stufe 2 — Werkzeuge, die der Weg voraussetzt

Der empfohlene Weg ist ohne diese beiden nicht ehrlich empfehlbar, weil eine Preisrunde sonst Handarbeit je Zeile bleibt:

  • AP 6 Massenanpassung: prozentual oder absolut über eine Auswahl, mit Vorschau und neuem Startdatum statt Überschreiben.
  • AP 8 Import/Export: CSV-Roundtrip für Ersteinrichtung und Preisrunde.

Beide sind im Konzept bereits als Arbeitspakete geführt. Neu ist die Einordnung: Sie sind Vorbedingung der Empfehlung, nicht Komfort.

Stufe 3 — Preiskategorie als Container (AP 10 + AP 11 + AP 11b, ADR-032)

Hier verschwindet die Kopplung an den Gerätetyp:

  • price_classes + price_class_members, price_class_id auf service_prices, dreistufige Zugriffsfolge im PriceFinderService mit matched_by.
  • Preisgruppen erben (AP 11b): Aufschlagsart, Wert und Zeitraum an der Gruppe, einstufig vom Betrag der Kategorie abgeleitet. Das ist der Schritt, der die unter Punkt 2 benannte unsichtbare Regel abschafft, statt sie nur zu erklären. Der Zeitraum macht eine Rabattaktion zu einer befristeten Gruppe statt zu einer manuellen Umpreisung, die jemand zurücknehmen muss.
  • Die Herkunft wird sichtbar. Der Preisinfos-Dialog und die Lookup-Vorschau zeigen künftig nicht nur den Betrag, sondern die gewinnende Stufe: „Preis aus Preiskategorie ‚Druck 0–30 bar', geerbt über die Prozedur ‚Druckkalibrierung', Preisgruppe ‚Händler': 153,00 (Liste 180,00 minus 15 Prozent), gültig ab 01.01.2026." Bei Stufe 1: „Ausnahmepreis direkt am Gerätetyp, überschreibt die Kategorie." Mit der Vererbung ist diese Anzeige keine Zugabe mehr, sondern Bedingung dafür, dass ADR-012 („warum kostet das 82 Euro?") überhaupt erfüllbar bleibt.
  • Der Ausnahmeanteil wird ab hier auf der Übersicht geführt.

Stufe 4 — Geführte Überführung des Bestands

Ziel: Vom heutigen Zustand („Preise am Gerätetyp") in den Zielzustand, ohne Bruch und rückrollbar.

  1. Bericht (dry_run): Kopiencluster plus, wo vorhanden, die dreistufige Nummer aus dem Zusatzfeld „Artikelnummer" als zweite Quelle. Ergebnis: Wie viele Zeilen ließen sich zu wie vielen Kategorien zusammenfassen?
  2. Vorschlag je Cluster, nicht automatisch: „Diese 14 Typen tragen 180,00. Eine Preiskategorie daraus machen?" Ob die 14 Typen anschließend direkt in der Kategorie liegen oder über eine gemeinsame Prozedur, ist eine zweite, nachgelagerte Frage. Der Vorschlag funktioniert auch dort, wo noch keine Prozedur existiert.
  3. Übernahme: Kategorie mit Betrag anlegen, Mitglieder zuordnen, die Typzeilen stilllegen (Enddatum), nicht löschen. Sie hängen an Altaufträgen, und ein Enddatum ist zurücknehmbar.
  4. Abweichler bleiben, wo sie sind. Ein Typ, der trotz gleicher Kategorie anders kostet, behält seine prices-Zeile und gewinnt über Stufe 1. Das ist das „eigentlich" aus ADR-032, und es braucht keinen eigenen Mechanismus.

Solange nichts gelöscht wird, ist jeder Schritt umkehrbar.


5. Was das kostet, und was es nicht löst

Aufwand (Schätzung, zur Einordnung, nicht als Zusage):

Stufe Inhalt Schätzung
0 GET /prices/overview, PriceOverviewService, Übersichtsseite mit Tabs 3–5 PT
1 Nächster-Schritt-Regelkette, Feldregister-Seed, Lücken-Filter und Sammelanlage 3–4 PT
2 AP 6 + AP 8 (bereits im Konzept veranschlagt) 5–8 PT
3 AP 10 + AP 11 + AP 11b (bereits im Konzept veranschlagt) 18–27 PT
4 Bericht, Vorschlag je Cluster, Übernahme mit Stilllegung 4–6 PT

Ehrliche Grenzen:

  • Stufe 0 bis 2 ändern das Modell nicht. Preise hängen weiterhin am Gerätetyp. Was sich ändert: Der Bestand wird gruppenweise statt typweise gepflegt, das Kopieren wird sichtbar, und die Anwendung erzieht nicht mehr in die falsche Richtung. Die eigentliche Ablösung ist Stufe 3.
  • Die Übergangszeit hat einen unsauberen Zustand. Bis Stufe 3 sind die „Gruppenzeilen" technisch Gerätetyp-Zeilen (die heutigen Sammel-Typen wie 101 Federmanometer -1 bis 30 bar). Die Anwendung kann sie nicht von echten Gerätetypen unterscheiden. Eine Kennzeichnung wäre eine Spalte auf types — einer von V1 genutzten Tabelle — und damit eine Yii-Migration für eine Zwischenlösung, die Stufe 3 wieder einreißt. Bewusst nicht gemacht; stattdessen macht die Cluster-Kennzahl den Zustand messbar, ohne ihn zu zementieren.
  • Stufe 3 hängt nicht mehr am Prozedur-Rollout, ihr Nutzen schon. Die Preiskategorie nimmt Gerätetypen auch direkt auf, funktioniert also ohne eine einzige Prozedur. Ohne Prozeduren muss sie aber typweise befüllt werden, und dann ist die 20.000-Typen-Frage nur verschoben.
  • Die Gruppenvererbung ändert bestehende Preise. Wo heute still 0,00 entsteht, steht danach ein abgeleiteter Betrag. Das ist der Zweck, aber es gehört ein Vorher-Nachher-Bericht davor, sonst überrascht es im Angebot.
  • Der Ausnahmeanteil kann das Modell kippen. Ist er hoch, entstehen wieder viele Typzeilen und der heutige Zustand ist zurück. Deshalb steht die Zahl auf der Übersicht und nicht in einem Report, den niemand öffnet.
  • Nicht adressiert: die offene Frage, was „kundenspezifisch" bedeutet (offener Punkt 12 im Konzept). Sie betrifft das Modell, nicht die Führung, und bleibt vor AP 10 zu klären.

6. Alternativen

  • B — Nur Dokumentation. Handbuchkapitel „So richtest du Preise ein", keine Codeänderung. Billig, aber die Anwendung erzieht weiter in die falsche Richtung, und Handbücher werden beim Einrichten nicht gelesen. Als Ergänzung sinnvoll, als Ersatz nicht.
  • C — Modaler Einrichtungsassistent. Fühlt sich geführter an, veraltet aber am Tag nach dem Durchlauf und hilft beim späteren Kontrollieren nicht. Siehe Abschnitt 2.
  • D — Direkt Stufe 3 bauen und die Führung weglassen. Löst das Modellproblem, aber niemand weiß dann, wie der neue Weg zu benutzen ist, also dieselbe Lücke auf höherem Niveau. Mit dem Container ist D allerdings tragfähiger als in der Vorfassung, weil Stufe 3 nicht mehr auf den Prozedur-Rollout wartet.

Empfehlung: A, also Stufe 0 und 1 zuerst. Sie sind klein, hängen an nichts, verändern das Modell nicht und machen den Zustand messbar, bevor über die größere Umstellung entschieden wird. Die Cluster-Kennzahl aus Stufe 0 liefert außerdem die Zahlen, mit denen sich Stufe 3 begründen oder verwerfen lässt.


7. Referenzen

  • Modell und Arbeitspakete: konzept-preisverwaltung-v2.md
  • Preismatrix statt Preiskatalog: ADR-012
  • Preisklasse als Zuordnungsebene (in Teilen ersetzt): ADR-029
  • Preiskategorie als Container, Gruppenvererbung: ADR-032
  • Erklärmuster und Komponenten: frontend-v2/components/README-admin-ui-kit.md
  • Preisfindung (Verhalten ohne Vererbung, stille Lücke): laravel/app/Services/Pricing/PriceFinderService.php