Files
Vorrania/web/src/pages/Groups.jsx
Scarriffle 32d49b47bd Tabellen entzerrt, EAN-Codes mit Marke und Artikelname, Codes-Block nach unten
EAN-Codes:
- Das Kennzeichen "Artikel" ist weg; stattdessen stehen Marke und Artikelname
  zusammen ("Zweifel - Moutarde Original Chips"). Die Marke allein reichte nicht,
  um einen Code wiederzuerkennen. Hinweistext entsprechend angepasst.

Produktseite:
- "Weitere EAN-Codes" steht jetzt unterhalb von Formular und Chargen statt als
  dritte Spalte dazwischen. Die Codes braucht man selten und sie schoben die
  beiden wichtigen Bloecke auseinander.

Tabellen:
- "overflow-wrap: anywhere" galt fuer ALLE Zellen und zerriss Woerter mitten
  drin ("PROD UKTE", "555 Gram m"). Jetzt nur noch gezielt per Klasse.
- Kopfzeilen und Zahl-plus-Einheit brechen nicht mehr um.
- Eingaben in Tabellen sind kompakter (kleinere Polsterung, Zahlenfelder 8ch),
  Auswahlfelder richten sich nach ihrem Text statt 132px Mindestbreite -
  dadurch sind die Zeilen deutlich schlanker.
- Kennzeichen wie "niedrig" oder "abgelaufen" sitzen ueber .cell-row in einer
  Zeile mit dem Wert und rutschen nicht mehr je nach Zeilenhoehe herum.

Geprueft: "npm run build" laeuft durch. Backend unveraendert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 07:50:42 +02:00

242 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from "react";
import { api } from "../api";
import { useConfirm } from "../confirm";
import { useAuth } from "../auth";
import BarcodeList from "../components/BarcodeList";
import Icon from "../components/Icon";
import { fmt } from "../units";
export default function Groups() {
const confirm = useConfirm();
const { isAdmin } = useAuth();
const [groups, setGroups] = useState([]);
const [units, setUnits] = useState([]);
const [form, setForm] = useState({ name: "", min_stock: "", min_stock_unit_id: "" });
const [selectedId, setSelectedId] = useState(null);
const [error, setError] = useState(null);
async function load() {
try {
const [gs, us] = await Promise.all([api.listGroups(), api.listUnits()]);
setGroups(gs);
setUnits(us);
} catch (err) {
setError(err.message);
}
}
useEffect(() => { load(); }, []);
async function add(e) {
e.preventDefault();
setError(null);
try {
await api.createGroup({
name: form.name,
min_stock: form.min_stock === "" ? null : Number(form.min_stock),
min_stock_unit_id: form.min_stock_unit_id === "" ? null : Number(form.min_stock_unit_id),
});
setForm({ name: "", min_stock: "", min_stock_unit_id: form.min_stock_unit_id });
load();
} catch (err) {
setError(err.message);
}
}
async function patch(group, body) {
setError(null);
try {
await api.updateGroup(group.id, body);
load();
} catch (err) {
setError(err.message);
}
}
async function remove(group) {
const ok = await confirm({
title: `Gruppe „${group.name}“ löschen?`,
message: "Die Produkte bleiben erhalten, verlieren aber ihre Zuordnung zu dieser Gruppe.",
confirmLabel: "Löschen",
danger: true,
});
if (!ok) return;
try {
await api.deleteGroup(group.id);
if (selectedId === group.id) setSelectedId(null);
load();
} catch (err) {
setError(err.message);
}
}
const selected = groups.find((g) => g.id === selectedId) || null;
return (
<div>
<div className="page-head">
<div>
<h1>Gruppen</h1>
<div className="sub">
Bestände mehrerer Marken zusammenzählen z.B. Mehl von Rewe, Aldi und Migros als 5 kg Mehl. Mit gemeinsamem Mindestbestand; die EAN-Codes der zugeordneten Artikel erscheinen automatisch hier.
</div>
</div>
</div>
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
<div className="grid-2 wide-aside">
<div className="card" style={{ padding: 0 }}>
<div className="table-wrap">
<table className="table">
<thead>
<tr>
<th>Gruppe</th>
<th className="num">Produkte</th>
<th className="num">Bestand</th>
<th>Mindestbestand</th>
<th>Einheit</th>
<th className="num">EAN-Codes</th>
<th></th>
</tr>
</thead>
<tbody>
{groups.map((g) => {
const low = g.min_stock != null && g.stock < g.min_stock;
return (
<tr key={g.id}>
<td data-label="Gruppe">
<span className="cell-row">
{isAdmin ? (
<input defaultValue={g.name} style={{ marginTop: 0, minWidth: 120 }}
onBlur={(e) => {
const v = e.target.value.trim();
if (v && v !== g.name) patch(g, { name: v });
}} />
) : <span className="strong">{g.name}</span>}
{low && <span className="badge warn">niedrig</span>}
</span>
</td>
<td data-label="Produkte" className="num muted">{g.product_count}</td>
<td data-label="Bestand" className="num">{fmt(g.stock)} {g.min_stock_unit_name || ""}</td>
<td data-label="Mindestbestand">
{isAdmin ? (
<input type="number" step="any" defaultValue={g.min_stock ?? ""}
style={{ marginTop: 0, minWidth: 90 }}
onBlur={(e) => {
const v = e.target.value;
if (v !== String(g.min_stock ?? "")) {
patch(g, { min_stock: v === "" ? null : Number(v) });
}
}} />
) : (g.min_stock != null ? fmt(g.min_stock) : "")}
</td>
<td data-label="Einheit">
{isAdmin ? (
<select value={g.min_stock_unit_id ?? ""} style={{ marginTop: 0 }}
onChange={(e) => patch(g, {
min_stock_unit_id: e.target.value === "" ? null : Number(e.target.value),
})}>
<option value=""> Basiseinheit </option>
{units.map((u) => <option key={u.id} value={u.id}>{u.name}</option>)}
</select>
) : (g.min_stock_unit_name || "")}
</td>
<td data-label="EAN-Codes" className="num">
<button className="link-btn" onClick={() => setSelectedId(g.id)}>
{/* Beide Herkünfte zählen, sonst steht bei einer Gruppe
mit Artikeln irreführend eine 0. */}
{(g.product_barcodes?.length || 0) + (g.barcodes?.length || 0)} verwalten
</button>
</td>
<td className="num">
{isAdmin && (
<button className="btn-icon danger" onClick={() => remove(g)} title="Löschen">
<Icon name="trash" size={16} />
</button>
)}
</td>
</tr>
);
})}
{groups.length === 0 && <tr><td colSpan={7} className="empty">Noch keine Gruppen.</td></tr>}
</tbody>
</table>
</div>
</div>
<div>
{selected && (
<section className="card">
<div className="card-head">
<Icon name="search" />
<h2>EAN-Codes der Gruppe: {selected.name}</h2>
</div>
<BarcodeList
barcodes={selected.barcodes || []}
productBarcodes={selected.product_barcodes || []}
disabled={!isAdmin}
hint={"Codes der zugeordneten Artikel stehen automatisch hier, mit Marke und " +
"Artikelnamen; entfernen lassen sie sich nur, indem der Artikel die Gruppe " +
"wechselt. Zusätzlich sind Codes möglich, die noch zu keinem Artikel " +
"gehören für die gibt es eine Notiz, weil ihnen der Artikelname fehlt. " +
"Wird so ein Code beim Einlagern gescannt, landet der neu angelegte Artikel " +
"automatisch hier."}
onAdd={async (body) => {
try {
await api.addGroupBarcode(selected.id, body);
await load();
} catch (err) { setError(err.message); }
}}
onEditNote={async (code, note) => {
try {
await api.updateGroupBarcodeNote(selected.id, code, { note });
await load();
} catch (err) { setError(err.message); }
}}
onDelete={async (code) => {
try {
await api.deleteGroupBarcode(selected.id, code);
await load();
} catch (err) { setError(err.message); }
}}
/>
<button className="btn ghost" onClick={() => setSelectedId(null)}>Schließen</button>
</section>
)}
{isAdmin && (
<form className="card" onSubmit={add}>
<div className="card-head"><Icon name="tag" /><h2>Neue Gruppe</h2></div>
<label>
Name
<input placeholder="z.B. Mehl" value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })} required />
</label>
<div className="row">
<label className="grow">
Mindestbestand (optional)
<input type="number" step="any" value={form.min_stock}
onChange={(e) => setForm({ ...form, min_stock: e.target.value })} placeholder="z.B. 2" />
</label>
<label className="grow">
Einheit
<select value={form.min_stock_unit_id}
onChange={(e) => setForm({ ...form, min_stock_unit_id: e.target.value })}>
<option value=""> Basiseinheit </option>
{units.map((u) => <option key={u.id} value={u.id}>{u.name}</option>)}
</select>
</label>
</div>
<button className="btn primary"><Icon name="plus" size={16} />Anlegen</button>
<p className="muted small">
Der Bestand der Gruppe summiert nur Produkte, die zur gewählten Einheit passen.
Namen lassen sich in der Tabelle direkt ändern.
</p>
</form>
)}
</div>
</div>
</div>
);
}