Vier Anzeigefehler aus dem Gebrauch
"NaN Glaeser" auf der Karte "Artikel im Blick". Die Karte reichte einen
ARTIKEL an articlePrimary() weiter, das fuer CHARGEN geschrieben ist und
die Menge unter dem Namen `quantity` sucht. Ein Artikel nennt sie
`stock` - also undefined / 190 = NaN. Neu bestandPrimary() und
bestandSecondary() in units.js als die eine Stelle, an der zwischen den
beiden Namen uebersetzt wird; sie delegieren an die vorhandenen
Formatierer, damit es nicht zwei Fassungen gibt. Steckte seit 8959814
drin.
Dazu gibt fmt() fuer nicht-endliche Zahlen jetzt einen Strich aus statt
"NaN". Dem Benutzer "NaN" hinzuschreiben ist nie richtig - der eigentliche
Fehler wird trotzdem an der Quelle behoben, nicht hier.
Die Spalte "Verbrauch" schnitt ab. Zwei beschriftete Knoepfe brauchen
rund 230 px, die Spalte hatte 190, und .datatable ist table-layout:fixed
mit overflow:hidden. Weil die Zelle rechtsbuendig ist, fiel der ANFANG
weg - sichtbar stand dort "rbrauchen". Jetzt 240 px, dazu flex-wrap in
der Zelle, damit die Knoepfe umbrechen statt abzuschneiden, wenn jemand
die Spalte von Hand schmaler zieht.
Die LETZTE Spalte liess sich nicht verbreitern. Ihr Anfasser sitzt exakt
am rechten Rand des Rollbereichs (gemessen: 0 px Luft), und nach rechts
ist dort Schluss - der Zeiger kann nicht weiter, also kam nie ein
positiver Abstand zum Startpunkt zustande. Der Resize rechnet jetzt mit
der BEWEGUNG seit dem letzten Schritt statt mit dem Abstand zum Start,
und in der Randzone waechst die Spalte von selbst weiter, solange die
Taste haelt; ein Taktgeber treibt das an, denn ein stehender Zeiger
meldet keine Bewegung. Die Tabelle rollt dabei mit, damit der Anfasser
unter dem Zeiger bleibt.
"3 Glas" statt "3 Glaeser" in der Chargenliste, waehrend die Uebersicht
"3 Glaeser" schrieb. Die Mehrzahlformen liegen in einer Modulvariablen,
weil auch Nicht-Komponenten sie brauchen - eine Aenderung daran loest
aber kein Neuzeichnen aus. Wer seine Liste zeichnete, bevor
/package-types zurueck war, blieb bei der Einzahl. Ein Zaehler im
Einstellungs-Kontext zieht die Formatierer jetzt nach.
Von Hand geprueft: Karte zeigt "3 Glaeser / 570 g"; beide Knoepfe stehen
vollstaendig in der Spalte; die letzte Spalte waechst beim Halten am
Rand von 240 auf ueber 470 und bleibt beim Loslassen stehen, waehrend
eine mittlere Spalte weiterhin exakt der Ziehstrecke folgt (170 -> 230);
Chargenliste schreibt "3 Glaeser".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4972feef09
commit
98db3339a2
@@ -265,34 +265,77 @@ export default function DataTable({
|
||||
}
|
||||
|
||||
// --- Resize (Pointer) ---
|
||||
//
|
||||
// Gerechnet wird mit der BEWEGUNG seit dem letzten Schritt, nicht mit dem
|
||||
// Abstand zum Startpunkt. Nur so laesst sich die letzte Spalte verbreitern:
|
||||
// Ihr Anfasser sitzt am rechten Rand, und dort ist fuer den Zeiger Schluss -
|
||||
// nach rechts gibt es kein Fenster mehr. Steht er in der Randzone, waechst die
|
||||
// Spalte deshalb von selbst weiter, Schritt fuer Schritt, solange die Taste
|
||||
// haelt. Ein Taktgeber treibt das an, denn ein stehender Zeiger meldet keine
|
||||
// Bewegung mehr.
|
||||
const resizing = useRef(null);
|
||||
const rollTimer = useRef(null);
|
||||
const RAND = 36; // Breite der Randzone
|
||||
const SCHRITT = 14; // Pixel je Takt in der Randzone
|
||||
|
||||
function startResize(e, c) {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
const startW = widthOf(c);
|
||||
// Die flexible (grow-)Spalte auf ihre aktuelle Pixelbreite einfrieren. Sonst
|
||||
// "klaut" das Breiterziehen einer Spalte der grow-Spalte den Platz – dann
|
||||
// "klaut" das Breiterziehen einer Spalte der grow-Spalte den Platz - dann
|
||||
// schrumpft die Nachbarspalte und alles wandert nach links. Danach verhalten
|
||||
// sich die Spalten wie in Excel (unabhängig, notfalls scrollt die Tabelle).
|
||||
// sich die Spalten wie in Excel (unabhaengig, notfalls scrollt die Tabelle).
|
||||
setWidths((old) => {
|
||||
const next = { ...old };
|
||||
for (const gc of growCols) if (next[gc.key] == null) next[gc.key] = Math.round(widthOf(gc));
|
||||
return next;
|
||||
});
|
||||
resizing.current = { key: c.key, startX: e.clientX, startW };
|
||||
resizing.current = { key: c.key, breite: widthOf(c), zeigerX: e.clientX, letzterX: e.clientX };
|
||||
rollTimer.current = window.setInterval(anwenden, 30);
|
||||
window.addEventListener("pointermove", onResizeMove);
|
||||
window.addEventListener("pointerup", endResize);
|
||||
}
|
||||
|
||||
function anwenden() {
|
||||
const r = resizing.current; if (!r) return;
|
||||
let delta = r.zeigerX - r.letzterX;
|
||||
r.letzterX = r.zeigerX;
|
||||
|
||||
const el = wrapRef.current;
|
||||
let amRand = false;
|
||||
if (el) {
|
||||
const kasten = el.getBoundingClientRect();
|
||||
if (r.zeigerX > kasten.right - RAND) { delta += SCHRITT; amRand = true; }
|
||||
else if (r.zeigerX < kasten.left + RAND) delta -= SCHRITT;
|
||||
}
|
||||
if (!delta) return;
|
||||
|
||||
const neu = Math.max(60, r.breite + delta);
|
||||
if (neu === r.breite) return;
|
||||
r.breite = neu;
|
||||
setWidths((old) => ({ ...old, [r.key]: Math.round(neu) }));
|
||||
// Mitlaufen, damit der Anfasser unter dem Zeiger bleibt, waehrend die
|
||||
// Tabelle nach rechts waechst.
|
||||
if (amRand && el) requestAnimationFrame(() => { el.scrollLeft = el.scrollWidth; });
|
||||
}
|
||||
|
||||
function onResizeMove(e) {
|
||||
const r = resizing.current; if (!r) return;
|
||||
const w = Math.max(60, Math.round(r.startW + (e.clientX - r.startX)));
|
||||
setWidths((old) => ({ ...old, [r.key]: w }));
|
||||
r.zeigerX = e.clientX;
|
||||
anwenden();
|
||||
}
|
||||
|
||||
function endResize() {
|
||||
resizing.current = null;
|
||||
if (rollTimer.current) { window.clearInterval(rollTimer.current); rollTimer.current = null; }
|
||||
window.removeEventListener("pointermove", onResizeMove);
|
||||
window.removeEventListener("pointerup", endResize);
|
||||
}
|
||||
|
||||
// Beim Verlassen der Seite mitten im Ziehen bliebe sonst ein Taktgeber laufen.
|
||||
useEffect(() => () => {
|
||||
if (rollTimer.current) window.clearInterval(rollTimer.current);
|
||||
}, []);
|
||||
|
||||
// --- Reorder (HTML5 Drag) ---
|
||||
const dragKey = useRef(null);
|
||||
const [dropKey, setDropKey] = useState(null);
|
||||
|
||||
@@ -8,7 +8,7 @@ import BewegungIcon from "../components/BewegungIcon";
|
||||
import { FristBadge, verleihRowClass } from "../components/Verleih";
|
||||
import { useSettings } from "../settings";
|
||||
import {
|
||||
articlePrimary, articleSecondary, fmt, relativeExpiry,
|
||||
articlePrimary, articleSecondary, bestandPrimary, bestandSecondary, fmt, relativeExpiry,
|
||||
} from "../units";
|
||||
import Bars from "./charts/Bars";
|
||||
import Donut from "./charts/Donut";
|
||||
@@ -192,9 +192,12 @@ function KarteArtikel({ props: karteProps }) {
|
||||
<div className="card-stack">
|
||||
<div className="kpi">
|
||||
<div className="kpi-label"><Icon name="package" size={14} />{daten.name}</div>
|
||||
<div className="kpi-value">{articlePrimary(daten)}</div>
|
||||
{/* Artikel, nicht Charge: Der Bestand heisst hier ``stock``.
|
||||
``articlePrimary`` sucht ``quantity`` und lieferte deshalb
|
||||
"NaN Glaeser". */}
|
||||
<div className="kpi-value">{bestandPrimary(daten)}</div>
|
||||
</div>
|
||||
<div className="muted small">{articleSecondary(daten)}</div>
|
||||
<div className="muted small">{bestandSecondary(daten)}</div>
|
||||
</div>
|
||||
)}
|
||||
</Zustand>
|
||||
|
||||
@@ -254,9 +254,14 @@ export default function Charges() {
|
||||
{ key: "created", header: "Eingelagert", width: 130,
|
||||
sortValue: (l) => new Date(l.created_at).getTime(),
|
||||
render: (l) => <span className="muted">{new Date(l.created_at).toLocaleDateString("de-DE")}</span> },
|
||||
{ key: "verbrauch", header: "Verbrauch", label: "Verbrauch", width: 190, align: "num",
|
||||
// 240 statt 190: Zwei beschriftete Knoepfe brauchen den Platz, und die
|
||||
// Tabelle schneidet ab statt umzubrechen (.datatable ist table-layout:fixed
|
||||
// mit overflow:hidden). Bei rechtsbuendigen Zellen faellt dabei der ANFANG
|
||||
// weg - aus "Verbrauchen" wurde sichtbar "rbrauchen". Das Umbrechen unten
|
||||
// faengt ab, wenn jemand die Spalte von Hand schmaler zieht.
|
||||
{ key: "verbrauch", header: "Verbrauch", label: "Verbrauch", width: 240, align: "num",
|
||||
render: (l) => (
|
||||
<span className="cell-row">
|
||||
<span className="cell-row" style={{ flexWrap: "wrap", justifyContent: "flex-end" }}>
|
||||
<button type="button" className="btn sm ghost" title="Teilmenge verbrauchen"
|
||||
onClick={() => setVerbrauchen(l)}>
|
||||
<Icon name="checkout" size={15} />Verbrauchen
|
||||
|
||||
@@ -17,6 +17,8 @@ const SettingsContext = createContext({
|
||||
export function SettingsProvider({ children }) {
|
||||
const { user } = useAuth();
|
||||
const [dateFormat, setDateFormat] = useState(DEFAULT_FORMAT);
|
||||
// Zaehler, der sich erhoeht, sobald die Mehrzahlformen da sind. Siehe unten.
|
||||
const [gebindeStand, setGebindeStand] = useState(0);
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
try {
|
||||
@@ -30,6 +32,13 @@ export function SettingsProvider({ children }) {
|
||||
// Mehrzahlformen der Gebinde. Sie landen in einer Modulvariablen, weil
|
||||
// auch Nicht-Komponenten (die Formatierer in units.js) sie brauchen.
|
||||
setGebindeFormen(await api.listPackageTypes());
|
||||
// Die Formen liegen in einer Modulvariablen – eine Aenderung daran loest
|
||||
// von sich aus kein Neuzeichnen aus. Wer seine Liste zeichnete, BEVOR die
|
||||
// Antwort da war, blieb deshalb bei der Einzahl stehen: In der
|
||||
// Chargenliste stand "3 Glas", auf der Uebersicht "3 Glaeser", je
|
||||
// nachdem welche Abfrage zuerst zurueckkam. Dieser Zaehler haengt im
|
||||
// Kontext und zieht alle nach, die ueber useSettings() formatieren.
|
||||
setGebindeStand((n) => n + 1);
|
||||
} catch {
|
||||
/* Ohne Liste bleibt die Einzahl stehen – unschoen, aber nicht falsch. */
|
||||
}
|
||||
@@ -41,6 +50,7 @@ export function SettingsProvider({ children }) {
|
||||
|
||||
const value = {
|
||||
dateFormat,
|
||||
gebindeStand,
|
||||
formatDate: (iso) => formatDate(iso, dateFormat),
|
||||
// MHD: Monatsangaben erscheinen als "09/2026", Tagesdaten im gewaehlten Format.
|
||||
formatBestBefore: (iso, precision) => formatBestBefore(iso, precision, dateFormat),
|
||||
|
||||
+23
-1
@@ -149,7 +149,12 @@ export function baseUnitToInput(baseUnit) {
|
||||
|
||||
export function fmt(n) {
|
||||
if (n == null) return "–";
|
||||
return Number(n).toLocaleString("de-DE", { maximumFractionDigits: 2 });
|
||||
const zahl = Number(n);
|
||||
// NaN entsteht, wenn irgendwo ein Feld fehlt (z.B. ein Artikel dort, wo eine
|
||||
// Charge erwartet wird). Dem Benutzer "NaN" hinzuschreiben ist nie richtig -
|
||||
// ein Strich sagt wenigstens die Wahrheit: hier steht keine Zahl.
|
||||
if (!Number.isFinite(zahl)) return "–";
|
||||
return zahl.toLocaleString("de-DE", { maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
// Tage bis zum MHD (negativ = überfällig, null = kein MHD).
|
||||
@@ -308,6 +313,23 @@ export function articlePrimary(item) {
|
||||
return `${fmt(item.quantity / (item.unit_factor || 1))} ${item.unit_name || unitShort(item.base_unit)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestand eines ARTIKELS als Leitangabe: "3 Gläser".
|
||||
*
|
||||
* Die Brücke zwischen zwei Namen für dieselbe Sache: Eine Charge nennt ihre
|
||||
* Menge ``quantity``, ein Artikel nennt sie ``stock``. ``articlePrimary`` kennt
|
||||
* nur ``quantity`` – wer ihm einen Artikel hinhielt, bekam "NaN Gläser", weil
|
||||
* ``undefined / package_size`` eben NaN ist. Diese beiden Funktionen sind die
|
||||
* Stelle, an der das übersetzt wird, statt es an jeder Aufrufstelle zu tun.
|
||||
*/
|
||||
export function bestandPrimary(product) {
|
||||
return articlePrimary({ ...product, quantity: product?.stock || 0 });
|
||||
}
|
||||
|
||||
export function bestandSecondary(product) {
|
||||
return articleSecondary({ ...product, quantity: product?.stock || 0 });
|
||||
}
|
||||
|
||||
// Untermenge in der Basiseinheit – nur wenn sie sich von der Leitangabe unterscheidet.
|
||||
export function articleSecondary(item) {
|
||||
if (item.package_size > 0 || (item.unit_factor || 1) !== 1) {
|
||||
|
||||
Reference in New Issue
Block a user