Commit Graph
135 Commits
Author SHA1 Message Date
ScarriffleandClaude Opus 5 c741df5963 Etiketten-Vorlagen: gespeicherte Exporte mit frei waehlbaren Spalten
Der Etikettenexport konnte genau eine Sache, und zwar unveraenderlich: sechs
feste Spalten ueber Einzelstuecke. Was fehlte, war genau das, was ein Etikett
brauchbar macht - die eigenen Felder. Eine Kabellaenge oder die Kapazitaet einer
Powerbank steht laengst am Artikel, kam aber in keiner CSV vor. Und jede
Zusammenstellung war beim naechsten Mal wieder weg: Kategorien im React-State,
Trennzeichen im localStorage, Spalten hartkodiert.

Jetzt gibt es benannte Vorlagen mit eigener Spaltenwahl und Reihenfolge, fuer
Einzelstuecke, Artikel und Lagerorte. Eine Tabelle nach dem Vorbild von
DashboardLayout: benannt, Nutzlast als JSON-Text. Ohne user_id - eine Vorlage
gehoert allen, nicht einem Konto.

Spaltenschluessel fuer eigene Felder ist die ID, nicht der key: FieldDefinition.key
ist nur JE KATEGORIE eindeutig, zwei Kategorien duerfen beide "kapazitaet" fuehren.

Die CSV entsteht jetzt im Server statt im Browser. Mit waehlbaren Spalten,
eigenen Feldern und drei Bezugsarten gehoert das an eine Stelle - und zwar
dorthin, wo es sich pruefen laesst. Formel-Injektionsschutz, BOM und CRLF sind
aus der Browser-Fassung mitgenommen. Der Grund, aus dem der QR-Inhalt bisher im
Browser entstand ("nur die Oberflaeche kennt die oeffentliche Adresse"), gilt
allein fuer Lagerorte; dafuer reicht sie ihre Herkunft als Parameter durch.

Die beiden alten Abschnitte weichen den Vorlagen. Beim ersten Start entstehen
zwei mitgelieferte, die genau die bisherigen Spalten fuehren - wer nichts
einstellt, bekommt dasselbe Ergebnis wie vorher.

Dazu laesst sich eine P-touch-Datei hinterlegen. Geprueft wird ueber Endung und
ZIP-Inhalt (label.xml), nicht ueber den Content-Type: Fuer .lbx meldet der
Browser meist application/octet-stream. Eine .lbx merkt sich den Pfad zu ihrer
Datenquelle - auf einem fremden Rechner muss die CSV in P-touch einmal neu
verknuepft werden. Das laesst sich von hier aus nicht loesen; die Datei
dabeizuhaben erspart trotzdem das Suchen.

Sicherung auf Version 4: Vorlagen kommen mit, Kategorien als Pfad und eigene
Felder als Label, weil IDs zwischen zwei Instanzen nicht dieselben sind. Die
.lbx als Base64 - das erste Binaerformat in dieser Sicherung, vertretbar bei
wenigen Kilobyte und einer Vorlage, die ohne ihre Datei nur eine halbe waere.

23 neue Tests (343 gesamt), dazu im Browser durchgespielt: Kabel-Kategorie mit
eigenem Feld, Spalte hinzugefuegt, CSV mit "Länge (m)" und dem Wert, .lbx
hoch- und wieder heruntergeladen (Pruefsumme identisch), PNG und Nicht-ZIP
sauber abgelehnt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 16:07:49 +02:00
ScarriffleandClaude Opus 5 1553e36b78 Artikelformular: Typ und Verwaltung als Auswahlliste
Zwei Umschalter mit zusammen fuenf Knoepfen beanspruchten zwei volle Zeilen im
Formular - der dreifache ("Menge je Lagerort / Einzelstuecke /
Verbrauchsgegenstand") brauchte dafuer sogar eine eigene Umbruch-Regel. Als
Auswahllisten stehen sie kompakt untereinander und sehen aus wie die
Kategorie-Auswahl direkt dazwischen.

Am Verhalten aendert sich nichts: changeModus und setObjMode bekommen denselben
Wert, nur aus onChange statt onClick.

.seg-wrap wird dadurch nirgends mehr gebraucht und faellt weg. .segmented und
.seg-label bleiben - die nutzen Mindestbestaende, Produkte und Kategorien
weiterhin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 14:16:01 +02:00
ScarriffleandClaude Opus 5 a3fd9819c4 Rueckgabe sichtbar machen, Zubehoerteile am Einzelstueck
RUECKGABE
Der Knopf war da, nur nicht zu finden: ein 16-px-Symbol in einer Spalte ohne
Ueberschrift, 52 px breit, ganz rechts in einer horizontal scrollenden Tabelle.
Beschriftet war er allein ueber title= - ein Tooltip, den auf einem Touchgeraet
nie jemand sieht.

Jetzt traegt er Text ("Zuruecknehmen"), die Spalte heisst "Verleih" und ist
breit genug, und beide Knoepfe haben ein aria-label. Dazu eine neue Seite
"Verliehen" in der Seitenleiste: alles Draussene an einem Ort, Rueckgabe direkt
aus der Liste. Bisher gab es diese Uebersicht nur als zwei Dashboard-Karten, und
die stehen nicht im Standard-Layout - wer sie nicht selbst hinzufuegte, hatte
gar keine.

In der App fuehrte der einzige Weg ueber das Bearbeiten-Formular des Stuecks;
jetzt geht es per Wischgeste und Kontextmenue direkt aus der Liste. Der
Verleih-Filter dort sass hinter einem nackten Symbol und ist beschriftet.

Zurueckgenommen wird nur noch nach Rueckfrage, in Web und App.

ZUBEHOER
Ein Netzteil gehoert zu einem Laptop: items.parent_item_id, genau ein Geraet je
Teil, beliebig viele Teile je Geraet. Eine Spalte genuegt dafuer, es braucht
keine Verknuepfungstabelle. ON DELETE SET NULL, damit das Netzteil den Laptop
ueberlebt statt mitgeloescht zu werden - und weil SQLite Fremdschluessel nicht
erzwingt, loest der Router die Zuordnung beim Loeschen zusaetzlich selbst.

Ringe (A traegt B, B traegt A) weist die Zuordnung mit 409 ab; die Elternkette
wird dafuer hochgelaufen, wie beim Kategoriebaum.

Die Beziehung laeuft ueber eigene Endpunkte und bewusst NICHT ueber ItemUpdate:
dessen Schema sendet immer alle Felder mit, auch leere, damit sich Felder
loeschen lassen. Genau so gingen schon einmal Kaufpreis und Waehrung verloren,
als ein Aufrufer ein Feld vergass.

Beim Verleihen fragt die Oberflaeche, welche Teile mitgehen (vorausgewaehlt:
alles, was im Haus ist), beim Zuruecknehmen kommen sie zusammen zurueck. Ohne
das Spiegelbild kaeme das Geraet zurueck und sein Netzteil bliebe ewig
"verliehen", weil niemand mehr danach sucht. Ungefragt nimmt der Server nie
etwas mit.

In der App sind zubehoer und gehoert_zu optional dekodiert: Die App spricht mit
mehreren Serverprofilen, und ein aelterer Server schickt die Felder nicht - ein
Vorgabewert reicht dafuer nicht, Swift nimmt ihn beim Dekodieren nicht her.

12 neue Tests (320 gesamt). Der Umlauf ist ausserdem gegen ein lokales Backend
im Browser durchgespielt: verleihen mit Zubehoer, Rueckfrage, beide zusammen
zurueck.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 14:08:11 +02:00
ScarriffleandClaude Opus 5 1332dd86fa Web: Einzelstuecke verleihen, auch direkt nach dem Scannen
Verleihen und Zuruecknehmen in der Einzelstueck-Tabelle eines Artikels und
in der Liste aller Einzelstuecke, dort als Spalten "Verliehen an" und
"Zurueck bis" (filter- und sortierbar). Ueberfaellig ist rot, demnaechst
faellig gelb - dieselben Zustandsfarben wie beim MHD.

Die Scan-Zielseite /i/<uid> leitete vorher nur zum Artikel weiter. Fuer
"scannen und verleihen" braucht die Aktion aber genau dort ihren Platz:
Die Seite zeigt jetzt, was man in der Hand haelt - Stueck, Artikel,
Lagerort, Verleihzustand - und bietet Verleihen bzw. Zuruecknehmen an.
Gegenstueck zur Lagerort-Seite /l/<code>, die es fuers Einlagern genauso
macht.

Anzeige und Aktionen liegen einmal in components/Verleih.jsx, weil alle
drei Stellen dasselbe brauchen. Der Knopf haengt nicht an isAdmin:
Verleihen ist eine Buchung wie Ein- und Auslagern.

Dazu zwei Dashboard-Karten: alles Verliehene und, getrennt, nur das
Ueberfaellige. Stuecke ohne Rueckgabedatum sind nie faellig und tauchen in
der zweiten Karte nicht auf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 09:30:14 +02:00
ScarriffleandClaude Opus 5 9be772557e Sitzung haengt an der Benutzer-ID, Umbenennen wird moeglich
Das Sitzungs-Token benannte den Benutzer ueber seinen Namen (sub = username).
Damit war der Name die Kennung der Sitzung - ein Umbenennen haette jede laufende
Anmeldung dieses Benutzers ungueltig gemacht, und deshalb war es gar nicht erst
vorgesehen. Beim Anmeldedienst geaenderte Namen liessen sich in Vorrania
folglich nirgends nachziehen.

Neue Token tragen die ID (sub = "<id>", zusaetzlich uid), und deps.py loest die
Sitzung darueber auf. Alles andere haengt ohnehin schon an der ID: Bewegungen,
Dashboards, API-Tokens. Der Name ist damit reine Anzeige.

Benutzer lassen sich jetzt unter "Benutzer" umbenennen - auch sich selbst, ohne
aus der Sitzung zu fliegen. Der Anmeldeweg selbst sucht weiterhin ueber den
Namen; das ist das Feld im Formular und soll so bleiben.

Bestandsschutz: Token ohne uid werden weiter ueber den Namen aufgeloest, damit
beim Update niemand abgemeldet wird. Der Zweig darf nach Ablauf der
Token-Lebensdauer weg. Das zusaetzliche uid unterscheidet alt von neu eindeutig -
ein Benutzer koennte schlicht "42" heissen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 22:34:22 +02:00
ScarriffleandClaude Opus 5 eb29143f97 Erstanmeldung fragt nach, statt still ein zweites Konto anzulegen
Wer sich zum ersten Mal ueber den Anmeldedienst anmeldet und hier noch kein
Konto hat, bekam bisher wortlos ein neues - mit Rolle "Benutzer" und ohne
Verlauf. Wer schon ein Konto hatte, wollte aber fast immer genau das verknuepfen
und stand dann mit zwei Konten da.

Jetzt zeigt der Rueckweg eine Entscheidungsseite: bestehendes Konto verknuepfen
oder ein neues anlegen. Das Verknuepfen verlangt einmalig Benutzername und
Passwort des bestehenden Kontos. Genau dieser Nachweis ist der Grund, warum der
Anmeldeweg selbst nie fragen darf, welches Konto gemeint ist - ohne ihn koennte
sich jeder mit einem beliebigen Konto beim Anmeldedienst an ein fremdes
Vorrania-Konto haengen.

Die geprueften Angaben liegen dafuer kurzlebig und einmalig in
oidc_pending_links; vom Code steht dort nur der SHA-256-Hash, wie bei den
API-Tokens. Ein falsch getipptes Passwort verbraucht den Vorgang nicht, und
unbekannter Benutzername und falsches Passwort antworten gleich - sonst liessen
sich vorhandene Konten abzaehlen.

AUTO_CREATE=false blendet den Knopf zum Anlegen aus und laesst sich auch ueber
den Dialog nicht umgehen. Der App-Weg bleibt unveraendert, weil er keinen
Zwischenschritt anzeigen kann.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 22:28:38 +02:00
ScarriffleandClaude Opus 5 3d0ff54bf5 Anmeldung wahlweise ueber OpenID Connect (Authentik)
Neben dem Passwort-Login gibt es jetzt einen zweiten, gleichberechtigten Weg:
OpenID Connect gegen einen selbst gehosteten Anmeldedienst. Ohne Konfiguration
aendert sich nichts - keine SSO-Knoepfe, Passwort-Login wie bisher.

Der Anmeldedienst ist NUR fuer die Anmeldung zustaendig. Danach stellt Vorrania
sein eigenes Token aus wie beim Passwort-Login, deshalb bleiben deps.py, die
Rollenpruefung und die langlebigen API-Tokens unveraendert.

Backend
- Anbieter kommen aus der Umgebung (OIDC_PROVIDERS + OIDC_<KUERZEL>_*). Ein
  zweiter Anbieter ist damit reine Konfiguration, ohne Codeaenderung.
- Browser-Weg (vertraulicher Client) und App-Weg (oeffentlicher Client, PKCE)
  enden beide im selben Token-Schema.
- state/nonce/PKCE-Verifier liegen in einer kurzlebigen Tabelle statt in einem
  Cookie: Starlettes CORS-Schicht spiegelt bei allow_origins=["*"] zusammen mit
  allow_credentials die Herkunft zurueck, sobald ein Request ein Cookie traegt.
  Ohne Cookie entsteht diese Wechselwirkung erst gar nicht - und der Ablauf
  bleibt mit der db-Fixture pruefbar.
- Zuordnung ueber die sub des Anbieters; die Erstverknuepfung laeuft ueber die
  BESTAETIGTE E-Mail-Adresse. Rollen kommen nie aus dem Anbieter, und ein
  gleichlautender preferred_username uebernimmt nie ein bestehendes Konto.
- users.password_hash darf jetzt leer sein (reine SSO-Konten). verify_password
  faengt das ab, sonst antwortete der Login mit 500 statt 401.

Verknuepfen bestehender Konten
- Neue Seite "Mein Konto" (fuer alle Benutzer, nicht nur Admins): verknuepfen
  und wieder loesen. Die Berechtigung stammt aus der laufenden Sitzung, es
  braucht also weder eine hinterlegte Adresse noch Admin-Rechte.
- Der Start laeuft ueber POST, weil eine Seitennavigation das Bearer-Token
  nicht mitschicken kann.
- Die letzte Anmeldemoeglichkeit eines Kontos laesst sich nicht loesen.

iOS
- AppAuth (erste Paketabhaengigkeit) mit PKCE im System-Browser, nie in einem
  eingebetteten WebView, und ohne Client-Secret.
- offline_access nur im App-Weg: der Browser-Weg braucht kein Refresh-Token und
  bekaeme sonst einen ueberfluessigen Zustimmungsdialog.
- Anmeldezustand je Serverprofil im Keychain; abgelaufene Token erneuert die App
  still im Hintergrund. Gleichzeitige 401er teilen sich eine Erneuerung.

Deployment
- docker-compose reicht die ganze .env durch. Einzeln aufzuzaehlen ginge nur mit
  fest verdrahteten Kuerzeln - genau das soll ein weiterer Anbieter nicht
  erfordern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 21:55:38 +02:00
ScarriffleandClaude Opus 5 8d4954610b Bilder blockieren nicht mehr die halbe Seite
Warum eine angeklickte Artikelseite nur zur Haelfte lud: Die Bild-Route wird je
Tabellenzeile einmal aufgerufen, und ein Browser haelt je Host nur rund sechs
Verbindungen offen. Was diese Route aufhaelt, legt die ganze Oberflaeche lahm.
Sie hielt gleich dreifach auf:

1. Fehlte die lokale Kopie, holte ``images.ensure`` das Bild MITTEN in der
   Anfrage von der fremden Adresse - bis zu 15 Sekunden, je Zeile, und bei
   jedem Aufruf erneut, weil ein Fehlschlag nirgends vermerkt wurde. Artikel
   aus einem Backup-Import haben genau diese Ausgangslage. Gemessen: 2,01 s
   je Aufruf, dreimal hintereinander. Jetzt: 0,01 s, das Nachholen laeuft nach
   der Antwort im Hintergrund und ein toter Link wird eine Stunde gesperrt.

2. Ein Vorschaubild ist 34x34 Pixel gross - ausgeliefert wurde das Original.
   Gemessen: 2530 KB je Briefmarke, bei 40 Zeilen 99 MB fuer eine Liste. Neu
   erzeugt die Bild-Route (Pillow) eine Vorschau und legt sie daneben ab;
   ``?thumb=1`` liefert sie aus. Gemessen: 0,6 KB.

3. If-None-Match wurde ignoriert. Nach den 5 Minuten Cache-Frist lud der
   Browser jedes Bild komplett neu - das erklaert, warum es "alle 20 Minuten"
   wieder losging. Jetzt 304 ohne Daten.

Dazu im Web: laufende Bildabrufe werden beim Seitenwechsel abgebrochen (sonst
steht die neue Seite hinter Bildern Schlange, die niemand mehr sieht), und wo
``image_version`` bekannt ist, entfaellt die Anfrage fuer bildlose Artikel ganz.

Und der Verbindungsvorrat der Datenbank: 5 (+10) gegen 40 Arbeits-Threads von
FastAPI. Ab der 16. gleichzeitigen Anfrage wartete eine Route stillschweigend
30 Sekunden - von aussen ein haengender Server. Jetzt 10 (+20) mit 10 Sekunden
Frist: lieber ein Fehler als eine halbe Minute Stille.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:14:43 +02:00
ScarriffleandClaude Opus 5 ad3100462b Verlauf: klarere Bewegungs-Icons, Spalte nach vorn
Die beiden Kisten unterschieden sich nur in einem kleinen Pfeil. Bei 18 px war
das nicht zu trennen - erkannt hat man sie allein an der Farbe, und die war
gerade wegen der Wertung (gruen rein, orange raus) rausgeflogen. Ohne Farbe
blieb nichts uebrig, woran man sie auseinanderhaelt.

Jetzt die Strich-Icons checkin/checkout/edit: grosser Pfeil auf eine Linie zu
bzw. von ihr weg, Stift fuer die Korrektur. Dieselben Zeichen wie auf den
Ein-/Auslagern-Knoepfen, also schon vertraut, und auch klein eindeutig - die
Richtung liegt im Zeichen statt in der Farbe.

Auf der Verlaufsseite steht die Spalte jetzt an erster Stelle; sie sagt in
einem Blick worum es geht und braucht als Zeichen kaum Breite.

Der gefuellte Icon-Zweig in Icon.jsx wird damit nicht mehr gebraucht und ist
raus. Die SVG-Quellen bleiben unter brand/icons/ liegen - das README sagt jetzt
ausdruecklich, dass sie ungenutzt sind, warum, und wie man sie zurueckholt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 12:48:25 +02:00
ScarriffleandClaude Opus 5 9a2dc3a482 Verlauf: Bewegungsart als Sinnbild statt als Wort
"Eingelagert"/"Ausgelagert"/"Korrektur" standen als Textbadge in einer eigenen
Spalte - in der schmalen Ansicht als dritte beschriftete Zeile je Eintrag. Jetzt
eine getoente Kachel mit Icon: Kiste mit Pfeil hinein, Kiste mit Pfeil heraus,
Stapel mit Stift (aus open-box (1).svg, open-box.svg, edit-server.svg). Die
Farbe traegt dieselbe Bedeutung wie vorher das Badge - gruen rein, orange raus,
neutral Korrektur; Titel und aria-label behalten das Wort.

Die Dashboard-Karte bekommt dabei das Layout aus der Skizze: Kachel links,
Zeitpunkt ueber Produkt, ohne Tabelle. Die Verlaufs-SEITE bleibt eine Tabelle -
dort stehen noch Menge und Benutzer -, dort weicht nur das Wort der Kachel.

Die Icon-Komponente kann jetzt auch GEFUELLTE Grafiken mit eigenem viewBox; die
vorhandenen sind Strichzeichnungen (fill:none, stroke:currentColor) und
vertragen sich damit nicht. Die Pfaddaten sind per Skript aus den SVGs
uebernommen, nicht abgetippt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 12:38:05 +02:00
ScarriffleandClaude Opus 5 eddbb3710e Web: abgelaufene Anmeldung erkennen statt leere Seiten zeigen
Zwei Defekte, die zusammen das gemeldete Muster ergeben.

1) Es gab keine zentrale Behandlung von HTTP 401. AuthProvider prueft die
Anmeldung EINMAL beim Start; laeuft das Token danach ab, bleibt der Zustand
"angemeldet" stehen, waehrend jede Abfrage scheitert. Seiten mit
.catch(() => {}) (18 Stellen) zeigen dann schlicht nichts, andere eine
englische Servermeldung - genau das "mal gar nichts, mal die Haelfte".
Jetzt raeumt der API-Client bei 401 die Sitzung auf und der Login sagt, dass
sie abgelaufen ist. Beim Anmelden selbst greift das nicht, dort heisst 401
"Passwort falsch".

2) Der Start hatte ein blankes `catch`, das das Token bei JEDEM Fehler loeschte -
auch bei 502 oder weggebrochenem Netz. Wer waehrend eines Server-Neustarts F5
drueckte, war deshalb abgemeldet, obwohl die Sitzung voellig in Ordnung war.
Das duerfte der haeufigere Ausloeser gewesen sein, weil in letzter Zeit oft neu
deployt wurde. Jetzt wird nur bei 401/403 abgemeldet; bei allem anderen bleibt
die Anmeldung liegen und es erscheint "Server nicht erreichbar" mit einem
Knopf zum erneuten Versuchen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 11:07:55 +02:00
ScarriffleandClaude Opus 5 01c93d8b2c Web: Mindestbestaende in festen Spalten, Detail-Spalten verschiebbar
Die Eingabefelder auf der Mindestbestaende-Seite standen je Zeile woanders. Die
Zeile war ein .field-inline, und dort waechst sowohl der Name als auch das
Eingabefeld (.field-inline > input { flex: 1 1 auto }) - beide teilten sich den
Restplatz, also verschob ein laengerer Name das Feld nach rechts und machte es
schmaler. Jetzt ist die Zeile ein Raster mit festen Spalten (Name, Menge,
Einheit, Bestand, Ort, Loeschen); ohne Adminrechte bleiben die letzten beiden
Zellen leer, damit die Zeilen trotzdem fluchten. Schmale Fenster stellen den
Namen ueber die volle Breite.

Die "Details"-Spalte liess sich nicht verschieben: sie war fixed und hatte
zusaetzlich einen leeren Kopf - es gab also nichts zu greifen. Sie ist jetzt
frei und traegt einen sichtbaren Titel. Dasselbe fuer die gleichartigen
Link-Spalten in der Einzelstueck- und der Chargenliste ("Artikel",
"Aufteilen"). Sortieren bleibt dort aus, weil toggleSort ohne sortValue
ohnehin aussteigt. Bild- und Auswahlspalten bleiben fest - die haben keinen
sinnvollen Titel und gehoeren an den Anfang.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 10:36:48 +02:00
ScarriffleandClaude Opus 5 e00eef275c Gruppen hierarchisch: in allen Auswahlfeldern und in der Artikelliste
Die Gruppen-Dropdowns waren flache, alphabetische Listen - man sah nicht, dass
"Grillwurst" unter "Wurst" haengt. Ein gemeinsamer Helfer (gruppenOptionen)
rollt den Graphen zum Baum aus und rueckt ein; eine Gruppe mit mehreren
Obergruppen erscheint unter jeder, denn genau das ist ihr Sinn, und ausgewaehlt
wird ohnehin dieselbe ID.

Eingerueckt wird mit GESCHUETZTEN Leerzeichen - normale fasst der Browser in
einem <option> zusammen, die Einrueckung waere sonst wirkungslos.

Betroffen: Gruppen-Auswahl im Artikelformular (beide Zweige), beim Einlagern,
die Ziel-Auswahl der Mindestbestaende und die Obergruppen-Mehrfachauswahl (die
beim Suchen bewusst abflacht, weil eine Einrueckung ohne die Eltern in die Irre
fuehrt). In der App dasselbe in beiden Gruppen-Pickern.

Die Gruppen-Spalte der Artikelliste zeigt jetzt den Weg von oben statt eines
Etiketts - "Wurst → Grillwurst", mit gedaempften Vorfahren wie bei der
Kategorie. Gefiltert wird ueber ALLE Wege: wer auf "Wurst" filtert, sieht auch
die Artikel aus den Untergruppen. Dafuer gibt es gruppenInfoMap als Gegenstueck
zu categoryInfoMap, das mit mehreren Wegen je Gruppe umgehen kann.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 09:59:32 +02:00
ScarriffleandClaude Opus 5 302e7625e5 Web: Gruppentabelle auf volle Breite, Karten darunter
Die Seite hatte die Tabelle links und eine feste rechte Spalte (24% der
Breite). Seit die Tabelle Obergruppen, Bestand mit Einheit und die
Gebinde-Erlaeuterung traegt, war die rechte Spalte teurer als sie half - und sie
stand auch dann da, wenn gar keine Gruppe ausgewaehlt war.

Jetzt bekommt die Tabelle die volle Breite. "Neue Gruppe" steht darunter, die
Karten zur ausgewaehlten Gruppe (EAN-Codes, Mindestbestand) erscheinen beim
"verwalten" ebenfalls darunter, nebeneinander. Kein Springen des Layouts beim
Auswaehlen, weil die Tabelle ihre Breite nie aendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 09:44:29 +02:00
ScarriffleandClaude Opus 5 3de168b598 Gruppen: Bestand in der richtigen Einheit, Baumansicht, Gruppen-Spalte
Anzeigefehler: die Gruppenliste klebte immer min_stock_unit_name hinter den
Bestand - auch wenn die Gruppe laengst im Gruppen-Gebinde zaehlt. Aus 790 g bei
einem 190-g-Glas wurde so "4,16 Gramm" statt "4,16 Glaeser". Gerechnet war
richtig, beschriftet falsch.

Ursache war doppelte Logik: MinStock.jsx hatte grpPkgMode/grpFactor/grpUnit
lokal, Groups.jsx gar nicht. Die drei stehen jetzt in units.js und werden von
beiden benutzt. Die Einheiten-Spalte sagt ausserdem, wenn eine Gruppe im
Gebinde zaehlt ("zaehlt in Glaeser a 190 g") und bietet den Weg zurueck an.

Gruppenliste als Baum: eine Gruppe mit zwei Obergruppen erscheint unter beiden -
das ist die ehrliche Darstellung eines Graphen. Jede Zeile traegt ihren PFAD als
Schluessel, damit die vorhandene Baumlogik der DataTable unveraendert zurecht
kommt; die kennt nur einen Elternteil je Zeile, und ein Pfad hat genau einen.
Beim Sortieren/Filtern flacht sie wie gewohnt ab.

"1 Untergruppen" heisst jetzt "1 Untergruppe" - an allen vier Stellen ueber
einen gemeinsamen Helfer (anzahlWort), Web und App.

Lebensmittelliste bekommt eine Spalte "Gruppe" mit Filter. Dafuer liefert
ProductOut jetzt group_name, symmetrisch zu category_name, mit joinedload gegen
N+1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 09:40:09 +02:00
ScarriffleandClaude Opus 5 37bc7663af Web: Einheiten-Felder als zusammenhaengender Block
Im Grillwurst-Fall stand dieselbe Zahl zweimal im Formular, mit zwei
Bedeutungen und in identischer Aufmachung: "Packungsgroesse 3" (1 Packung = 3
Stueck) und "Zweiteinheit 3 Stk ≙ 250 g" (3 Stueck wiegen 250 g). Dazu sass der
Zweiteinheit-Block zwischen Gebinde/MHD und Mindestbestand/Gruppe, hiess nach
einem Fachbegriff und benutzte ≙ statt eines Satzes.

Jetzt steht alles zur Einheit unter einer Ueberschrift und liest sich als Satz:
"Gezaehlt wird in Stueck", "Gebinde Packung", "1 Packung sind 3 Stueck". Das
Gebinde steht dafuer vor der Packungsgroesse - die Zeile darunter nennt es beim
Namen.

Die Umrechnung heisst nach dem, was sie liefert (Gewicht / Volumen / Stueckzahl)
und beginnt mit einer einzigen Auswahl; erst danach erscheint der Satz "3 Stk
wiegen 250 g" mit passendem Verb und Ein-/Mehrzahl. Die linke Zahl wird beim
Waehlen der Zielart EINMALIG mit der Packungsgroesse vorbelegt - zu tippen
bleibt nur die 250. Bewusst keine dauerhafte Kopplung: wer die Packung spaeter
auf 4 aendert, hat dieselbe Wurst, und ein mitgezogener Wert wuerde
stillschweigend behaupten, sie sei leichter geworden.

MHD-Angabe wandert aus dem Einheiten-Block zur Gruppen-Zeile; sie stand nur
zufaellig neben dem Gebinde.

Reine Oberflaeche - Datenmodell, API und Backend-Tests unveraendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 09:19:08 +02:00
ScarriffleandClaude Opus 5 788ef2caf2 Web: Zweiteinheit erfassen und anzeigen
Im Artikel-Formular eine Zeile "3 [Stk] ≙ 250 [Gramm]" mit ausgerechneter
Kontrolle darunter ("1 Stk ≈ 83,33 g"). Links steht die Basiseinheit des
Artikels fest, rechts stehen nur die anderen Arten zur Wahl - eine Bruecke auf
die eigene Art haette nichts umzurechnen. Nur vollstaendig ausgefuellt wird sie
gespeichert; Gegenstaende ohne Charge tragen keine.

buildUnitOptions laesst bei hinterlegter Bruecke auch die Einheiten der Gegenart
zu - damit erben Ein- und Auslagern die Faehigkeit ohne eigene Logik. Beim
Auslagern steht der Bestand jetzt in beiden Lesarten ("6 Stueck · 500 g").

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 00:40:33 +02:00
ScarriffleandClaude Opus 5 0537a2b53f Web: Obergruppen zuordnen, Mindestbestaende nach Lagerort
Mindestbestaende-Seite ist jetzt nach Lagerort gegliedert statt nach Artikel -
"Ueberall (egal wo)" zuerst, danach die Lagerorte. Es gibt keine automatische
Leerzeile je Artikel mehr; angelegt wird ueber "+ hinzufuegen" im jeweiligen
Abschnitt, dann steht der Ort schon fest und es fehlen nur Ziel und Menge.
Das Zeilenmodell faellt damit von vier Faellen auf zwei, und weil der Server
einheitlich Basiseinheiten liefert, bleibt genau eine Umrechnung uebrig statt
der drei gegenlaeufigen von vorher.

Gruppen: neue Spalte "Obergruppen" mit Mehrfachauswahl (GroupParentSelect).
Bewusst kein Baum - CategorySelect und die tree-Prop der DataTable koennen nur
einen Elternteil und wuerden eine Gruppe mit zwei Obergruppen doppelt anzeigen.
Die eigene Gruppe und ihre Untergruppen sind in der Auswahl gesperrt, damit die
Ringregel sichtbar wird, bevor der Server sie ablehnt. Produktzahl zeigt
transitiv und direkt ("12 (3 direkt)"), dazu Badges fuer Untergruppen und eine
Warnung, wenn eine Gruppe mit Untergruppen keine Einheit hat - sonst summiert
sie Gramm und Stueck zu einer sinnlosen Zahl.

Einkaufsliste: "Gesamt" heisst "Ueberall", Gruppen zeigen ihre mitgezaehlten
Untergruppen, und eine Fussnote benennt die eine Stelle, die sich nicht
verrechnen laesst (zwei Obergruppen mit gemeinsamer Untergruppe).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 23:56:18 +02:00
ScarriffleandClaude Opus 4.8 86e0edace8 Mindestbestände: Gruppen in Packungen erfassen (Gruppen-Gebinde)
Gruppen mit verwalteter Einheit lassen sich in der Spalte 'Einheit' auf ein
Gruppen-Gebinde umschalten. Über 'Gebinde festlegen…' definiert man einmalig
'1 Packung ≈ X g/ml' (Richtwert, da Produkte unterschiedlich große Packungen
haben können). Danach werden Mindestbestand UND Bestand der Gruppe in
Packungen angezeigt/erfasst; das Backend rechnet vorhandene Werte beim
Umschalten um.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 18:42:15 +02:00
ScarriffleandClaude Opus 4.8 28785b0a10 Mindestbestände: Einheit je Produkt umschaltbar (g/ml <-> Gebinde)
Produkte mit Packungsgröße lassen sich auf der Mindestbestandsseite nun
wahlweise in ihrer Gebinde-Einheit (Glas/Dose) statt nur in g/ml erfassen
- Auswahl in der Spalte 'Einheit'. Mindestbestand UND Bestand werden dann
in Packungen angezeigt; der Hinzufügen-Dialog hat den Umschalter ebenfalls.
Gespeicherte Werte bleiben physisch gleich (nur Anzeige/Erfassung wechselt),
das Backend-Flag min_stock_in_packages steuert die Umrechnung.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 18:31:30 +02:00
ScarriffleandClaude Opus 4.8 f2c657ff31 Einkaufsliste: Fehlmenge als Gebinde (Glaeser/Dosen), Basiseinheit klein
Statt 'fehlt 500 Gramm' jetzt die Produktgroesse als Leitangabe:
'fehlt 3 Glaeser (500 g)'. Auf ganze Gebinde aufgerundet, weil man nur
ganze Glaeser/Dosen kauft; Basiseinheit als kleiner Hinweis.

Backend: neues ShoppingNeed-Objekt an jeder Einkaufslisten-Zeile (Produkte
+ Gruppen, gesamt + je Ort) - damit die Angabe auch direkt ueber die API
kommt ('x Glaeser (y g)'), inkl. Pluralisierung aus der Gebinde-Tabelle.
Gruppen leiten das Gebinde ab, wenn alle passenden Produkte dasselbe haben,
sonst bleibt es bei der Basiseinheit. Bestehende Felder unveraendert.

Frontend: gemeinsame Komponente ShoppingNeedText fuer Karte und volle Seite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 18:03:37 +02:00
ScarriffleandClaude Opus 4.8 9cc3e325bb Web: Kassenzettel-Trefferschwelle in den Einstellungen
Prozent-Feld fuer receipt_match_threshold (Default 45) analog zur Ablaufwarnung;
speichert ueber den generischen Settings-Endpunkt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 18:26:20 +02:00
ScarriffleandClaude Opus 4.8 3e10e254c8 Web: Dashboard-Karten wie HomeAssistant konfigurieren + Einkaufsliste je Lagerort
Karten-Optionen (Artikel, Lagerort, Zeitraum) sassen als einzelne Dropdowns im
Kartenkopf. Jetzt: deklaratives config-Schema je Kartenart und ein Zahnrad-Dialog
(KartenConfig) im Anordnen-Modus - der Kopf zeigt sonst nur noch Titel/Papierkorb.
Felder: product, location, zeitraum (erweiterbar: select, boolean). Werte bleiben
in props/tage, keine Migration.

Einkaufsliste-Karte laedt zusaetzlich shoppingByLocation und zeigt Gesamt + Bedarfe
je Lagerort. Ueber die Karten-Konfig waehlbarer Lagerort: leer = alle Orte, gesetzt
= Gesamt + nur dieser Ort.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 14:29:09 +02:00
ScarriffleandClaude Opus 4.8 f3bab15362 Mindestbestaende: Bestand je Lagerort anzeigen (nicht nur Gesamt)
Per-Lagerort-Zeilen (Produkt und Gruppe) zeigten '-' beim Bestand, weil die Seite
nur den Gesamtbestand kannte. Jetzt liefert LocationMinStockOut.stock den Bestand
AN DEM Ort (inkl. Unterorte) mit - bei Produkten in Basiseinheiten, bei Gruppen in
der Gruppen-Einheit (analog zu ProductOut.stock/GroupOut.stock). Berechnet ueber
location_subtree_stock_base; die Weboberflaeche zeigt ihn wie beim Gesamt-Bestand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 13:57:40 +02:00
ScarriffleandClaude Opus 4.8 4ed33e221f Web: Chargen-Seite faerbt Zeilen nach MHD (gelb = bald ablaufend, rot = abgelaufen)
Wiederverwendung von expiryRowClass + .row-warn/.row-danger wie auf der
Artikelseite. Warnfrist kommt aus der Einstellung expiry_warning_days (Default 7).
Die MHD-Faerbung bleibt auch bei ausgewaehlter/gehoverter Zeile sichtbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 13:45:19 +02:00
ScarriffleandClaude Opus 4.8 f981f41d94 Einzelstueck-QR ueber Custom-Scheme statt Universal Link
Universal Links (Associated Domains) sind an eine feste Domain gebunden - fuer
einen selbstgehosteten Dienst untauglich (jede Instanz hat eine andere URL) und
die Domain stand im offenen Quellcode (Leak). Jetzt tragen Einzelstueck-QRs das
Custom-Scheme vorrania://i/<UID>: Die normale Kamera-App oeffnet die App weiterhin,
aber ohne Domain-Bindung und ohne Server im Code. applinks-Eintrag entfernt.

Web: itemDeepLink()-Helfer, genutzt in Einzelstuecke-Liste/-Etiketten und im
P-touch-Export. Lagerort-QRs bleiben unveraendert (https).
iOS: Router erkennt vorrania://i/<UID> und oeffnet das Einzelstueck am aktiven Server.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 11:34:37 +02:00
ScarriffleandClaude Opus 4.8 48d47f1d39 Web: Mindestbestand-Ort per Dropdown verschieben + OFF-Panel-Regression behoben
MinStock: die ORT-Spalte ist jetzt ein Dropdown (Gesamt/Lagerort). Auswahl
verschiebt den Bedarf (moveRow) statt ihn zu duplizieren - mit Rueckfrage, falls
der Zielort schon belegt ist. Produktwerte werden dabei umgerechnet, Gruppen nicht.

OFF-Panel: meine schmalere Formularspalte hatte den Vergleichs-Button abgeschnitten
und die OFF-Tabelle aus der Karte laufen lassen. Tabelle jetzt in .table-wrap
(scrollt/stapelt), Button-Text darf umbrechen, Formularspalte etwas breiter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 09:32:45 +02:00
ScarriffleandClaude Opus 4.8 231d86895f Charge aufteilen: Teilmenge mit gleichem MHD an anderen Lagerort umlagern
Bisher liess sich nur die ganze Charge umlagern. Neu: POST /lots/{id}/split zweigt
eine Teilmenge (Basiseinheiten) als neue Charge mit gleichem MHD an einen anderen
Lagerort ab; der Rest bleibt. Reine Umbuchung ohne Bewegungseintrag, Gesamtbestand
unveraendert. Schutz gegen zu grosse Menge und gleichen Zielort.

Web: gemeinsamer SplitLotDialog (Menge in Artikeleinheit + Zielort, zeigt Rest),
Aufteilen-Knopf auf der Chargen-Seite und in der Chargen-Tabelle der Artikelseite.
Neues Split-Icon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 08:43:16 +02:00
ScarriffleandClaude Opus 4.8 8cf7cef6e1 Web: Artikelseite nutzt volle Breite, Chargen-Tabelle bekommt mehr Platz
Die Bearbeiten-Seite war auf 1400px gedeckelt (viel Rand links/rechts) und teilte
Formular und Chargen 50/50. Jetzt nutzt sie die volle Breite (wie die Listen) und
das Grid ist gespiegelt zu .wide-aside: links das schmale Formular (clamp bis
640px), rechts die Chargen-/Bestandstabelle mit dem Restplatz - so passen
Menge/MHD/Lagerort ohne Seitwaerts-Scrollen. Neuanlage bleibt einspaltig.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 18:52:30 +02:00
ScarriffleandClaude Opus 4.8 e28b6a5e38 Web: Mindestbestaende per Loesch-Knopf entfernbar
Loeschen ging bisher nur uebers Leeren des Zahlenfeldes - das war weder
auffindbar noch (bei Gesamt-Werten, deren Zeile bestehen bleibt) sichtbar.
Jetzt hat jede gesetzte Zeile einen Papierkorb-Knopf: Gesamt-Wert -> NULL,
je-Lagerort-Eintrag faellt raus. Mit Rueckfrage ueber den App-Dialog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 17:12:57 +02:00
ScarriffleandClaude Opus 4.8 bfc52997dc Web: Mindestbestand je Lagerort laesst sich anlegen (mergeLoc ergaenzt neue Orte)
Der Endpunkt ersetzt die komplette Lagerort-Liste; mergeLoc iterierte aber nur
ueber bestehende Eintraege und liess neue Orte weg. Dadurch entstand nie ein
neuer je-Lagerort-Mindestbestand, wenn nur ein Gesamt-Wert (leere Ortsliste)
oder ein Eintrag an einem anderen Ort existierte. Jetzt wird ein fehlender Ort
ergaenzt und der Abgleich per String() unabhaengig vom id-Typ gemacht.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 17:04:14 +02:00
ScarriffleandClaude Opus 4.8 18c07eaf3a Web: Chargenseite - Spalte "Art" (Lebensmittel/Gegenstand)
Neue sortier-/filterbare Spalte zeigt je Charge, ob es ein Lebensmittel oder ein
Gegenstand (Verbrauchsgegenstand) ist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 14:13:22 +02:00
ScarriffleandClaude Opus 4.8 5550897319 Web: Chargen per Klick auf die ganze Zeile auswaehlen
Nicht nur die kleine Checkbox schaltet die Auswahl, sondern ein Klick irgendwo
in die Zeile (ausser auf Bedienelemente wie Lagerort-Dropdown/Link). Ausgewaehlte
Zeilen werden hervorgehoben. Neuer DataTable-Prop onRowClick.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 14:08:12 +02:00
ScarriffleandClaude Opus 4.8 51c02091ed Web: Chargen auswaehlen und Lagerort der Auswahl setzen
Chargenliste bekommt Auswahl-Checkboxen. Das obere Panel wirkt jetzt auf die
Auswahl: "N ausgewaehlt -> Lagerort setzen" (leer = Ort entfernen, z.B. beim
Aufloesen eines Orts). Schnellwahl "alle sichtbaren" (respektiert Filter) und
"alle ohne Lagerort". DataTable meldet dafuer die sichtbaren Zeilen
(onVisibleRows).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 14:04:19 +02:00
ScarriffleandClaude Opus 4.8 826f4df4bd Web: Chargenliste (mit Sammel-Lagerort) + Ladezustand auf allen Tabellen
Neue Seite "Chargen": alle Chargen ueber alle Artikel (Artikel, Menge, MHD,
Lagerort, eingelagert). Lagerort je Zeile direkt aenderbar; alle Chargen ohne
Ort per Klick zuweisbar (bulk-location) - gut, wenn der Lagerort erst spaeter
angelegt wurde.

DataTable bekommt einen loading-Prop: waehrend des Ladens steht "Wird geladen..."
statt der Leer-Meldung. Auf ALLEN Tabellen angewendet (Produkte, Mindestbestaende,
Kategorien, Gruppen, Einheiten, Gebinde, Einzelstuecke, Chargen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 13:58:07 +02:00
ScarriffleandClaude Opus 4.8 227143d13f Web: Chargen zeigen ihren Lagerort (und lassen ihn aendern)
Die Chargen-Tabelle hatte keine Lagerort-Spalte - dadurch wirkte es, als koenne
man Chargen keinem Ort zuordnen (geht laengst ueber den Lagerort beim Einlagern).
Neue Spalte "Lagerort" zeigt den Ort je Charge; beim Bearbeiten laesst er sich
per Dropdown aendern (z.B. 2 Glaeser Pesto in Schrank A, 2 in Schrank B).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 13:41:19 +02:00
ScarriffleandClaude Opus 4.8 d448754d2a Web: Lagerort-Inhalt ansehen + von dort einlagern
Der Lagerort-QR (/l/<code>) zeigt jetzt, was am Ort (inkl. Unterorte) liegt -
Artikel mit Bestand, jeweils verlinkt - statt nur den Namen. "Hier einlagern"
springt ins Einlagern mit vorbelegtem Lagerort (?location=). In der Lagerorte-
Verwaltung fuehrt ein neues Lupensymbol zur Inhaltsansicht.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 13:37:34 +02:00
ScarriffleandClaude Opus 4.8 4034b97cf3 Web: Produktlisten zeigen "wird geladen" statt "keine Produkte" beim Laden
Bisher stand sofort "Keine Produkte gefunden." da, obwohl noch geladen wurde.
Jetzt ein Ladezustand: Kopf zeigt "Wird geladen...", Tabelle "Produkte werden
geladen...", und die Leer-Meldung kommt erst, wenn der Server wirklich nichts
liefert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 12:07:12 +02:00
ScarriffleandClaude Opus 4.8 f96286b3fa Web: Gegenstaende-Liste - Spalte/Filter "Verwaltung"
Neue Spalte "Verwaltung" (nur in der Gegenstaende-Liste): Menge je Lagerort /
Einzelstuecke / Verbrauchsgegenstand - sortier- und filterbar wie die anderen
Spalten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 15:40:21 +02:00
ScarriffleandClaude Opus 4.8 31137cd884 Web: Spalte "Zuletzt geaendert" in den Produktlisten
Neue sortier- und filterbare Spalte (Datum + Uhrzeit) in Lebensmittel- und
Gegenstaende-Liste, basierend auf Product.updated_at. Nuetzlich, um zuletzt
angelegte/geaenderte Artikel zu finden (z.B. mit fehlender Kategorie). Erscheint
automatisch, laesst sich per Spaltenauswahl aus-/einblenden und umsortieren.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 11:36:47 +02:00
ScarriffleandClaude Opus 4.8 4141781ffb Web: Foto schon beim Anlegen eines Produkts hinterlegen
Bisher ging ein Foto erst nach dem Speichern (Produkt-ID noetig). Jetzt kann man
beim Neuanlegen direkt "Foto machen"/"Galerie" nutzen - das Bild wird vorgemerkt,
als Vorschau gezeigt und nach dem Anlegen hochgeladen. Nuetzlich, wenn Open Food/
Products Facts kein Bild liefert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 10:28:56 +02:00
ScarriffleandClaude Opus 4.8 3ff7d2a35a Web: Mindestbestand je Lagerort im Formular in echter Einheit
Der je-Lagerort-Editor (foodLike) zeigt und speichert jetzt in der Anzeigeeinheit
(Gramm/Milliliter/Stueck) statt in Packungen - konsistent zur Mindestbestaende-
Seite. Umrechnung Artikeleinheit<->Anzeigeeinheit an der Formular-Grenze.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 09:02:10 +02:00
ScarriffleandClaude Opus 4.8 49a96d7fdb Web: Mindestbestand nur fuer Charge-Artikel (Lebensmittel + Verbrauchsgegenstand)
Menge je Lagerort und Einzelstuecke haben keinen Mindestbestand mehr: die
"Mindestbestand (Stueck)"-Felder und der je-Lagerort-Block fallen im Formular
weg (Gruppe fuer Menge bleibt), Alt-Werte werden beim Speichern geleert. Die
Mindestbestaende-Seite und der "+ hinzufuegen"-Dialog zeigen nur noch
Charge-Artikel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 08:54:50 +02:00
ScarriffleandClaude Opus 4.8 275611160b Web: Mindestbestaende in echter Einheit statt Packung
"1 Packung" ist nichtssagend (kann 500 g, 1 kg oder 25 ml sein). Die Seite zeigt
und bearbeitet Produkt-Mindestbestaende jetzt in der Anzeigeeinheit des Artikels
(Gramm/Milliliter/Stueck): global (Basiseinheiten) und je Lagerort (Artikel-
einheiten) werden dafuer umgerechnet. Rigatoni zeigt jetzt z.B. "500 Gramm".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 08:48:32 +02:00
ScarriffleandClaude Opus 4.8 f0c64466e2 Web: Mindestbestaende-Bestand zeigt richtige Einheit ("1 Packung" statt "1 Gramm")
articleLabel fiel bei leerem Gebinde-Namen auf die Basiseinheit zurueck, waehrend
articleUnit durch die Packungsgroesse teilt. Dadurch stand z.B. bei Rigatoni
"1 Gramm" statt "1 Packung" (500 g). Label ist jetzt an die Packungsgroesse
gekoppelt und passt zur Zahl.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 08:39:03 +02:00
ScarriffleandClaude Opus 4.8 24ea55c396 Web: Barcode-Feld nicht mehr von den Knoepfen zusammengequetscht
Barcode-Eingabe behaelt eine Mindestbreite; die Knoepfe (Nachschlagen /
Vergleichen) brechen bei Platznot in die naechste Zeile um.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 21:27:16 +02:00
ScarriffleandClaude Opus 4.8 46f9c69a39 Web: "Neue Kategorie"-Button auf Hoehe des Auswahlfelds
Eigene unsichtbare Beschriftungszeile ueber dem Knopf, damit er auf gleicher
Hoehe wie das Kategorie-Auswahlfeld beginnt - statt am oberen oder unteren
Zeilenrand zu kleben.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 21:23:38 +02:00
ScarriffleandClaude Opus 4.8 8d34e2e8d8 Web: "Neue Kategorie"-Button an das Kategorie-Feld ausrichten
Button per align-self unten buendig zum Auswahlfeld (stand vorher am oberen
Zeilenrand). Gruppen-Tooltip auf "mehrerer Artikel" verallgemeinert, da jetzt
auch Gegenstaende gruppierbar sind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 21:19:18 +02:00
ScarriffleandClaude Opus 4.8 f287f8f768 Web: zentrale Mindestbestaende per "+ hinzufuegen" anlegen
Neuer Dialog auf der Mindestbestaende-Seite: Ziel (Produkt/Gegenstand oder
Gruppe) -> Geltung (Gesamt oder Lagerort) -> Menge in der Einheit des Ziels.
Speichert ueber die vorhandenen Endpunkte (updateProduct/updateGroup bzw.
set*LocationMinStock).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 20:41:57 +02:00
ScarriffleandClaude Opus 4.8 4087277bb0 Web: Verbrauchsgegenstand als dritte Gegenstands-Art + Typ-Position + Thumbnail
ProductForm: Typ/Kategorie an feste Position (nach Marke, vor den typ-
spezifischen Feldern) - der Umschalter springt nicht mehr je nach Auswahl.
Gegenstaende bekommen eine 3-Wege-Verwaltung: Menge je Lagerort /
Einzelstuecke / Verbrauchsgegenstand. Verbrauchsgegenstaende werden wie
Lebensmittel als Charge (Einheit, Packung, MHD, Mindestbestand mit Einheit,
Gruppe, Chargen-Ansicht) gefuehrt (foodLike = kein Gegenstand ODER bulk).
Menge-Gegenstaende koennen jetzt ebenfalls einer Gruppe zugeordnet werden.

Thumbnail: .thumb display:block, damit die Zeilenhoehe beim Nachladen nicht
mehr minimal springt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 20:39:15 +02:00