Darstellung der App nachgebessert, Gruppen heissen jetzt Kategorien
Die App zeigte an mehreren Stellen Rohwerte statt lesbarer Angaben, und die
Bedienelemente sahen nicht nach Bedienelementen aus.
Deutsche Lokalisierung: Im Chargen-Abschnitt stand "10. Dec 2027" - deutsches
Format mit englischem Monatsnamen. Die Ursache lag nicht in der Formatierung,
sondern darin, dass die App als englische App gebaut wurde
(CFBundleDevelopmentRegion = $(DEVELOPMENT_LANGUAGE), also "en"). Jetzt ist
Deutsch als Sprache gesetzt; damit zeichnet iOS DatePicker und Monatsnamen von
selbst richtig. Der MonthYearPicker legt seinen Kalender zusaetzlich fest auf
de_DE, damit die Monatsnamen auch bei anderssprachigem Geraet stimmen.
Datumsformat: Die Anzeige folgt jetzt der Servereinstellung (Einstellungen ->
Darstellung), wie die Web-Oberflaeche. Neu DisplaySettings mit den fuenf
Formaten, den deutschen Beschriftungen der Basiseinheiten und der Mengenangabe
in der Leitangabe. Die doppelt ausprogrammierte MHD-Formatierung in
ListViews und CheckOutView ist damit weg.
Chargen-Abschnitt: Jede Charge steht jetzt in einem eigenen Abschnitt mit
Kopfzeile statt gestapelt in einer einzigen Formularzeile - das war der
gequetschte Eindruck. Loeschen sitzt in der Kopfzeile.
Mengenfelder haben einen sichtbaren Rahmen. Ohne ihn las sich das
rechtsbuendige Feld wie fester Text, und es war nicht erkennbar, dass sich die
Menge ueberhaupt aendern laesst.
Kamera: Der Sucher fuellte per ZStack und ignoresSafeArea den ganzen Bildschirm
und wirkte dadurch erdrueckend. Er sitzt jetzt als 4:3-Feld oben, darunter
Hinweis, Meldungen und Knoepfe in gewohnter Form. Am ScannerViewController
musste nichts geaendert werden.
Einkaufsliste: Artikel und Kategorien tragen ein Symbol, Kategoriezeilen
zusaetzlich eine Kennzeichnung. Den Artikelzeilen fehlte bisher jede Einheit
("fehlt 2 - Bestand 7 von 10"); sie nennen jetzt Packungen und Basismenge.
Produktdetails: Die Felder waren blanke TextFields - sobald ein Wert drinstand,
verschwand der Platzhalter und man las nur noch "Bratbutter" / "M-Classic" /
"450". Jetzt mit Beschriftung links, Einheit hinter der Packungsgroesse (vorher
stand dort der rohe API-Wert "gram") und einem Abschnitt "Erkennung" mit
Barcode. Dasselbe im Formular zum Anlegen.
Gruppen heissen in der Oberflaeche jetzt Kategorien - in Web und App. Tabelle,
Feld group_id und die Endpunkte behalten ihren Namen, damit bestehende Zugriffe
wie Home Assistant weiterlaufen; der Unterschied ist rein sprachlich. Die
Kategorie laesst sich nun auch in der App zuweisen, was vorher gar nicht ging.
Dabei ein Fehler gefunden und behoben: Swift laesst nil-Optionals beim Kodieren
weg, das Backend wertet nur mitgeschickte Felder aus. "Keine Kategorie" oder
eine geloeschte Marke waeren damit stumm verpufft. ProductUpdateRequest kodiert
diese Felder jetzt ausdruecklich. Der Mindestbestand steht bewusst nicht mehr
darin, weil die App ihn nicht bearbeitet und ein mitgesendetes null ihn
geloescht haette.
Getestet: iOS-Geraetebuild fehler- und warnungsfrei, Web-Build laeuft durch,
40 pytest-Tests gruen. Die Datumsformatierung wurde eigenstaendig uebersetzt
und gegen elf Faelle geprueft (alle fuenf Formate, Monatsangabe, fehlendes und
unlesbares Datum) - alle korrekt. Im gebauten Bundle ist belegt, dass
CFBundleDevelopmentRegion auf "de" steht. Gegen die laufende API geprueft:
/settings und /groups liefern die erwarteten Felder, das Leeren der Kategorie
wirkt jetzt und der Mindestbestand bleibt dabei erhalten.
Die Oberflaeche selbst habe ich nicht auf dem Geraet bedient.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -47,7 +47,7 @@ function Sidebar() {
|
||||
<NavItem to="/checkin" icon="checkin" label="Einlagern" />
|
||||
<NavItem to="/checkout" icon="checkout" label="Auslagern" />
|
||||
<NavItem to="/products" icon="package" label="Produkte" />
|
||||
<NavItem to="/groups" icon="tag" label="Gruppen" />
|
||||
<NavItem to="/groups" icon="tag" label="Kategorien" />
|
||||
<NavItem to="/shopping" icon="cart" label="Einkaufsliste" />
|
||||
<NavItem to="/history" icon="history" label="Verlauf" />
|
||||
{isAdmin && (
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function CheckIn() {
|
||||
const [barcode, setBarcode] = useState("");
|
||||
const [product, setProduct] = useState(null);
|
||||
const [suggestion, setSuggestion] = useState(null);
|
||||
// Gruppe, die diesem Code zugeordnet ist (z.B. alle Mehl-Marken in "Mehl").
|
||||
// Kategorie, die diesem Code zugeordnet ist (heisst in der API weiterhin "group").
|
||||
const [suggestionGroup, setSuggestionGroup] = useState({ id: "", name: "" });
|
||||
const [unknownBarcode, setUnknownBarcode] = useState(null);
|
||||
const [results, setResults] = useState([]);
|
||||
@@ -80,7 +80,7 @@ export default function CheckIn() {
|
||||
async function createFromSuggestion() {
|
||||
setError(null); setBusy(true);
|
||||
try {
|
||||
// Ist der Code einer Gruppe zugeordnet, hat das Vorrang vor dem Kategorie-Rateversuch.
|
||||
// Ist der Code einer Kategorie zugeordnet, hat das Vorrang vor dem Rateversuch aus OFF.
|
||||
const groupId = suggestionGroup.id || guessGroup(groups, suggestion);
|
||||
const payload = suggestionToProduct(suggestion, groupId);
|
||||
const created = await api.createProduct(payload);
|
||||
@@ -185,7 +185,7 @@ export default function CheckIn() {
|
||||
</div>
|
||||
<div className="muted small">
|
||||
Noch nicht im Katalog.
|
||||
{suggestionGroup.name && ` Gruppe: ${suggestionGroup.name}`}
|
||||
{suggestionGroup.name && ` Kategorie: ${suggestionGroup.name}`}
|
||||
</div>
|
||||
</div>
|
||||
{isAdmin ? (
|
||||
|
||||
@@ -120,7 +120,7 @@ export default function Dashboard() {
|
||||
<tbody>
|
||||
{groupShopping.map((it) => (
|
||||
<tr key={`g${it.group_id}`}>
|
||||
<td><span className="badge accent">Gruppe</span> {it.name}</td>
|
||||
<td><span className="badge accent">Kategorie</span> {it.name}</td>
|
||||
<td className="num">{fmt(it.stock)} {it.unit_name}</td>
|
||||
<td className="num strong">{fmt(it.deficit)} {it.unit_name}</td>
|
||||
</tr>
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function Groups() {
|
||||
}
|
||||
|
||||
async function remove(group) {
|
||||
if (!confirm(`Gruppe "${group.name}" löschen? Produkte bleiben erhalten, verlieren aber die Gruppenzuordnung.`)) return;
|
||||
if (!confirm(`Kategorie "${group.name}" löschen? Produkte bleiben erhalten, verlieren aber die Zuordnung.`)) return;
|
||||
try {
|
||||
await api.deleteGroup(group.id);
|
||||
if (selectedId === group.id) setSelectedId(null);
|
||||
@@ -68,9 +68,9 @@ export default function Groups() {
|
||||
<div>
|
||||
<div className="page-head">
|
||||
<div>
|
||||
<h1>Gruppen</h1>
|
||||
<h1>Kategorien</h1>
|
||||
<div className="sub">
|
||||
Produkte zusammenfassen, Gruppen-Mindestbestand setzen und EAN-Codes hinterlegen
|
||||
Produkte zusammenfassen, Mindestbestand je Kategorie setzen und EAN-Codes hinterlegen
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,7 +82,7 @@ export default function Groups() {
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Gruppe</th>
|
||||
<th>Kategorie</th>
|
||||
<th className="num">Produkte</th>
|
||||
<th className="num">Bestand</th>
|
||||
<th>Mindestbestand</th>
|
||||
@@ -146,7 +146,7 @@ export default function Groups() {
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{groups.length === 0 && <tr><td colSpan={7} className="empty">Noch keine Gruppen.</td></tr>}
|
||||
{groups.length === 0 && <tr><td colSpan={7} className="empty">Noch keine Kategorien.</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -162,7 +162,7 @@ export default function Groups() {
|
||||
<BarcodeList
|
||||
barcodes={selected.barcodes || []}
|
||||
disabled={!isAdmin}
|
||||
hint="Scannst du einen dieser Codes beim Einlagern, wird das neue Produkt automatisch dieser Gruppe zugeordnet."
|
||||
hint="Scannst du einen dieser Codes beim Einlagern, wird das neue Produkt automatisch dieser Kategorie zugeordnet."
|
||||
onAdd={async (body) => {
|
||||
try {
|
||||
await api.addGroupBarcode(selected.id, body);
|
||||
@@ -182,7 +182,7 @@ export default function Groups() {
|
||||
|
||||
{isAdmin && (
|
||||
<form className="card" onSubmit={add}>
|
||||
<div className="card-head"><Icon name="tag" /><h2>Neue Gruppe</h2></div>
|
||||
<div className="card-head"><Icon name="tag" /><h2>Neue Kategorie</h2></div>
|
||||
<label>
|
||||
Name
|
||||
<input placeholder="z.B. Mehl" value={form.name}
|
||||
@@ -205,7 +205,7 @@ export default function Groups() {
|
||||
</div>
|
||||
<button className="btn primary"><Icon name="plus" size={16} />Anlegen</button>
|
||||
<p className="muted small">
|
||||
Der Gruppen-Bestand summiert nur Produkte, die zur gewählten Einheit passen.
|
||||
Der Bestand der Kategorie summiert nur Produkte, die zur gewählten Einheit passen.
|
||||
Namen lassen sich in der Tabelle direkt ändern.
|
||||
</p>
|
||||
</form>
|
||||
|
||||
@@ -92,7 +92,7 @@ export default function ProductForm() {
|
||||
setInfo(
|
||||
"Daten von Open Food Facts übernommen." +
|
||||
(s.package_size != null ? "" : " (Füllmenge nicht hinterlegt – bitte Packungsgröße prüfen.)") +
|
||||
(groupGuess ? " Passende Gruppe vorgeschlagen." : "")
|
||||
(groupGuess ? " Passende Kategorie vorgeschlagen." : "")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -327,7 +327,7 @@ export default function ProductForm() {
|
||||
</div>
|
||||
</label>
|
||||
<label className="grow">
|
||||
Gruppe
|
||||
Kategorie
|
||||
<select value={form.group_id} onChange={(e) => set("group_id", e.target.value)} disabled={readOnly}>
|
||||
<option value="">– keine –</option>
|
||||
{groups.map((g) => <option key={g.id} value={g.id}>{g.name}</option>)}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function ShoppingList() {
|
||||
<div className="page-head">
|
||||
<div>
|
||||
<h1>Einkaufsliste</h1>
|
||||
<div className="sub">Produkte und Gruppen unter ihrem Mindestbestand</div>
|
||||
<div className="sub">Produkte und Kategorien unter ihrem Mindestbestand</div>
|
||||
</div>
|
||||
</div>
|
||||
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
|
||||
@@ -42,7 +42,7 @@ export default function ShoppingList() {
|
||||
<li key={key} className={checked[key] ? "done" : ""}>
|
||||
<label>
|
||||
<input type="checkbox" checked={!!checked[key]} onChange={(e) => toggle(key, e.target.checked)} />
|
||||
<span className="badge accent">Gruppe</span>
|
||||
<span className="badge accent">Kategorie</span>
|
||||
<span className="item-name">{it.name}</span>
|
||||
</label>
|
||||
<span className="muted small">
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function Transfer() {
|
||||
<p className="muted small mt-0">
|
||||
<strong>CSV</strong> enthält eine Zeile je Charge und lässt sich in Excel oder
|
||||
LibreOffice bearbeiten. <strong>JSON</strong> ist ein vollständiges Backup inklusive
|
||||
Einheiten, Gruppen und Lagerorten.
|
||||
Einheiten, Kategorien und Lagerorten.
|
||||
</p>
|
||||
<div className="field-inline">
|
||||
<button className="btn primary" onClick={() => download("csv")}>
|
||||
@@ -84,7 +84,7 @@ export default function Transfer() {
|
||||
<section className="card">
|
||||
<div className="card-head"><Icon name="checkin" /><h2>Import</h2></div>
|
||||
<p className="muted small mt-0">
|
||||
CSV oder JSON auswählen. Unbekannte Produkte, Gruppen und Lagerorte werden immer
|
||||
CSV oder JSON auswählen. Unbekannte Produkte, Kategorien und Lagerorte werden immer
|
||||
angelegt – was mit vorhandenen Beständen passiert, bestimmt der Modus.
|
||||
</p>
|
||||
<form onSubmit={doImport}>
|
||||
|
||||
Reference in New Issue
Block a user