EAN-Codes einer Kategorie nachvollziehbar gemacht
Eine Kategorie mit Artikeln zeigte "0 EANs", und der Versuch, den Code eines eigenen Artikels einzutragen, scheiterte mit "Dieser Code ist bereits vergeben". Beides war fachlich richtig, aber nirgends erklaert. Hintergrund: Die Code-Liste einer Kategorie ist eine Vorratsliste fuer Artikel, die es noch nicht gibt. Sie greift nur, wenn beim Einlagern ein unbekannter Code gescannt wird - dann landet der neu angelegte Artikel in dieser Kategorie (routers/products.lookup). Haengt ein Code bereits an einem Artikel, findet die Suche immer zuerst den Artikel; ein gleichlautender Kategorie-Eintrag koennte nie wirken. Die Spalte zaehlte bisher ausschliesslich diese Vorratscodes, nie die Codes der enthaltenen Artikel - daher die irritierende 0. Die Kategorie liefert jetzt zusaetzlich die Codes ihrer Artikel mit (product_barcodes, rein informativ). Beide Herkuenfte stehen in einer Liste: Artikel-Codes sind als solche gekennzeichnet, nennen den Artikel und lassen sich hier nicht loeschen, weil sie am Artikel haengen. Bewusst ohne Dublette in der Datenbank - ein zweiter Datensatz koennte nie greifen und beim Loeschen des Artikels verwaisen. Die Spalte zaehlt beide Herkuenfte. Die Fehlermeldung sagt jetzt, wem ein Code gehoert: beim Artikel mit Namen und dem Hinweis, dass Artikel-Codes hier nicht eingetragen werden muessen; bei einer anderen Kategorie mit deren Namen. Signal beim Einlagern: Wird ein Code erkannt, der einer Kategorie zugeordnet ist, steht jetzt deutlich sichtbar "Wird automatisch der Kategorie X zugeordnet" samt Begruendung - sowohl im Web als auch in der App, und in beiden Faellen (Open-Food-Facts-Treffer und voellig unbekannter Code). Nach dem Anlegen meldet die Web-Oberflaeche zurueck, welche Kategorie es geworden ist. Vorher stand die Zuordnung nur als Nebensatz in grauer Kleinschrift. Getestet: Gegen die laufende API geprueft, dass eine Kategorie die Codes ihrer Artikel meldet (Haupt-Barcode und zusaetzliche Alias-Codes), dass das Eintragen eines Artikel-Codes und eines fremden Kategorie-Codes mit der jeweils richtigen Begruendung abgewiesen wird und dass ein echter Vorratscode weiterhin angelegt werden kann. iOS-Geraetebuild und Web-Build fehlerfrei, 40 pytest-Tests gruen. Dabei zwei Uebersetzungsfehler durch deutsche Anfuehrungszeichen gefunden: Das schliessende Zeichen war ein gerades ", das den String vorzeitig beendete. Korrigiert und die uebrigen Vorkommen im Projekt gleich mit vereinheitlicht. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,14 @@ from sqlalchemy.orm import Session
|
|||||||
from ..database import get_db
|
from ..database import get_db
|
||||||
from ..deps import get_current_user, require_admin
|
from ..deps import get_current_user, require_admin
|
||||||
from ..models import Barcode, Group, Product, User
|
from ..models import Barcode, Group, Product, User
|
||||||
from ..schemas import BarcodeCreate, BarcodeOut, GroupCreate, GroupOut, GroupUpdate
|
from ..schemas import (
|
||||||
|
BarcodeCreate,
|
||||||
|
BarcodeOut,
|
||||||
|
GroupCreate,
|
||||||
|
GroupOut,
|
||||||
|
GroupUpdate,
|
||||||
|
ProductBarcodeOut,
|
||||||
|
)
|
||||||
from ..services.conversion import BASE_OF_KIND
|
from ..services.conversion import BASE_OF_KIND
|
||||||
from ..services.stock import current_stock
|
from ..services.stock import current_stock
|
||||||
|
|
||||||
@@ -19,6 +26,24 @@ def _group_to_out(db: Session, group: Group) -> GroupOut:
|
|||||||
BarcodeOut.model_validate(b)
|
BarcodeOut.model_validate(b)
|
||||||
for b in db.query(Barcode).filter(Barcode.group_id == group.id).order_by(Barcode.id).all()
|
for b in db.query(Barcode).filter(Barcode.group_id == group.id).order_by(Barcode.id).all()
|
||||||
]
|
]
|
||||||
|
# Codes der Artikel in dieser Gruppe mitliefern. Sie waren bisher nirgends
|
||||||
|
# sichtbar, wodurch eine Kategorie mit Artikeln "0 EANs" anzeigte.
|
||||||
|
product_codes: list[ProductBarcodeOut] = []
|
||||||
|
for product in products:
|
||||||
|
if product.barcode:
|
||||||
|
product_codes.append(
|
||||||
|
ProductBarcodeOut(
|
||||||
|
code=product.barcode, product_id=product.id, product_name=product.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for alias in db.query(Barcode).filter(Barcode.product_id == product.id).order_by(Barcode.id):
|
||||||
|
product_codes.append(
|
||||||
|
ProductBarcodeOut(
|
||||||
|
code=alias.code, product_id=product.id, product_name=product.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
out.product_barcodes = product_codes
|
||||||
|
|
||||||
unit = group.min_stock_unit
|
unit = group.min_stock_unit
|
||||||
if unit is not None:
|
if unit is not None:
|
||||||
# Bestand nur über Produkte gleicher Art, in der Gruppen-Einheit ausgedrückt.
|
# Bestand nur über Produkte gleicher Art, in der Gruppen-Einheit ausgedrückt.
|
||||||
@@ -86,6 +111,28 @@ def update_group(
|
|||||||
return _group_to_out(db, group)
|
return _group_to_out(db, group)
|
||||||
|
|
||||||
|
|
||||||
|
def _describe_conflict(db: Session, code: str) -> str | None:
|
||||||
|
"""Sagt, wem ein Code schon gehört – "bereits vergeben" allein half nicht weiter."""
|
||||||
|
product = db.query(Product).filter(Product.barcode == code).first()
|
||||||
|
existing = db.query(Barcode).filter(Barcode.code == code).first()
|
||||||
|
|
||||||
|
if existing is not None and existing.product_id:
|
||||||
|
product = db.get(Product, existing.product_id) or product
|
||||||
|
if product is not None:
|
||||||
|
return (
|
||||||
|
f"Dieser Code gehört bereits zum Artikel \"{product.name}\". "
|
||||||
|
"Artikel-Codes müssen hier nicht eingetragen werden – beim Scannen "
|
||||||
|
"wird immer zuerst der Artikel gefunden, und dessen Kategorie zählt."
|
||||||
|
)
|
||||||
|
if existing is not None and existing.group_id:
|
||||||
|
other = db.get(Group, existing.group_id)
|
||||||
|
name = other.name if other else "einer anderen Kategorie"
|
||||||
|
return f"Dieser Code ist bereits der Kategorie \"{name}\" zugeordnet."
|
||||||
|
if existing is not None:
|
||||||
|
return "Dieser Code ist bereits vergeben."
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{group_id}/barcodes", response_model=GroupOut, status_code=status.HTTP_201_CREATED)
|
@router.post("/{group_id}/barcodes", response_model=GroupOut, status_code=status.HTTP_201_CREATED)
|
||||||
def add_group_barcode(
|
def add_group_barcode(
|
||||||
group_id: int,
|
group_id: int,
|
||||||
@@ -98,10 +145,9 @@ def add_group_barcode(
|
|||||||
if group is None:
|
if group is None:
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Gruppe nicht gefunden")
|
raise HTTPException(status.HTTP_404_NOT_FOUND, "Gruppe nicht gefunden")
|
||||||
code = payload.code.strip()
|
code = payload.code.strip()
|
||||||
if db.query(Barcode).filter(Barcode.code == code).first() or (
|
conflict = _describe_conflict(db, code)
|
||||||
db.query(Product).filter(Product.barcode == code).first()
|
if conflict:
|
||||||
):
|
raise HTTPException(status.HTTP_409_CONFLICT, conflict)
|
||||||
raise HTTPException(status.HTTP_409_CONFLICT, "Dieser Code ist bereits vergeben")
|
|
||||||
db.add(Barcode(code=code, note=(payload.note or None), group_id=group.id))
|
db.add(Barcode(code=code, note=(payload.note or None), group_id=group.id))
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(group)
|
db.refresh(group)
|
||||||
|
|||||||
@@ -48,6 +48,18 @@ class BarcodeOut(BaseModel):
|
|||||||
note: str | None = None
|
note: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ProductBarcodeOut(BaseModel):
|
||||||
|
"""Code, der über einen Artikel dieser Kategorie angehört.
|
||||||
|
|
||||||
|
Rein informativ: Er steht am Artikel, nicht an der Kategorie. Beim Scannen
|
||||||
|
findet die Suche immer zuerst den Artikel (siehe routers/products.lookup),
|
||||||
|
weshalb ein gleichlautender Kategorie-Eintrag nie greifen könnte.
|
||||||
|
"""
|
||||||
|
code: str
|
||||||
|
product_id: int
|
||||||
|
product_name: str
|
||||||
|
|
||||||
|
|
||||||
# ---- Auth / Users ----
|
# ---- Auth / Users ----
|
||||||
class Token(BaseModel):
|
class Token(BaseModel):
|
||||||
access_token: str
|
access_token: str
|
||||||
@@ -88,6 +100,8 @@ class GroupOut(BaseModel):
|
|||||||
min_stock_unit_name: str | None = None
|
min_stock_unit_name: str | None = None
|
||||||
kind: str | None = None # Art der Mindestbestand-Einheit
|
kind: str | None = None # Art der Mindestbestand-Einheit
|
||||||
barcodes: list[BarcodeOut] = [] # EANs, die dieser Gruppe zugeordnet sind
|
barcodes: list[BarcodeOut] = [] # EANs, die dieser Gruppe zugeordnet sind
|
||||||
|
# EANs der Artikel in dieser Gruppe - nur zur Anzeige, nicht bearbeitbar.
|
||||||
|
product_barcodes: list[ProductBarcodeOut] = []
|
||||||
|
|
||||||
|
|
||||||
class GroupCreate(BaseModel):
|
class GroupCreate(BaseModel):
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ struct CheckInView: View {
|
|||||||
@State private var product: Product?
|
@State private var product: Product?
|
||||||
@State private var suggestion: LookupResult.Suggestion?
|
@State private var suggestion: LookupResult.Suggestion?
|
||||||
@State private var suggestedGroupId: Int?
|
@State private var suggestedGroupId: Int?
|
||||||
|
@State private var suggestedGroupName: String?
|
||||||
@State private var unknownCode: String?
|
@State private var unknownCode: String?
|
||||||
@State private var manualCodeShown = false
|
@State private var manualCodeShown = false
|
||||||
@State private var manualCode = ""
|
@State private var manualCode = ""
|
||||||
@@ -110,6 +111,25 @@ struct CheckInView: View {
|
|||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sagt vor dem Anlegen, welche Kategorie der Artikel bekommt und warum.
|
||||||
|
@ViewBuilder
|
||||||
|
private func categoryNote() -> some View {
|
||||||
|
if let name = suggestedGroupName {
|
||||||
|
HStack(alignment: .top, spacing: 6) {
|
||||||
|
Image(systemName: "tag")
|
||||||
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
|
Text("Wird der Kategorie „\(name)“ zugeordnet").font(.caption).bold()
|
||||||
|
Text("Dieser EAN-Code ist dort hinterlegt.")
|
||||||
|
.font(.caption2).foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(8)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.background(Color.accentColor.opacity(0.15))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func suggestionCard(_ item: LookupResult.Suggestion) -> some View {
|
private func suggestionCard(_ item: LookupResult.Suggestion) -> some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text(item.name).font(.headline)
|
Text(item.name).font(.headline)
|
||||||
@@ -117,6 +137,7 @@ struct CheckInView: View {
|
|||||||
.font(.caption).foregroundStyle(.secondary)
|
.font(.caption).foregroundStyle(.secondary)
|
||||||
Text("Bei Open Food Facts gefunden, noch nicht im Katalog.")
|
Text("Bei Open Food Facts gefunden, noch nicht im Katalog.")
|
||||||
.font(.caption2).foregroundStyle(.secondary)
|
.font(.caption2).foregroundStyle(.secondary)
|
||||||
|
categoryNote()
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
ProductFormView(prefillBarcode: item.barcode, groupId: suggestedGroupId,
|
ProductFormView(prefillBarcode: item.barcode, groupId: suggestedGroupId,
|
||||||
suggestion: item) { created in
|
suggestion: item) { created in
|
||||||
@@ -140,6 +161,7 @@ struct CheckInView: View {
|
|||||||
Text("Unbekannter Code \(code)").font(.headline)
|
Text("Unbekannter Code \(code)").font(.headline)
|
||||||
Text("Weder im Katalog noch bei Open Food Facts.")
|
Text("Weder im Katalog noch bei Open Food Facts.")
|
||||||
.font(.caption).foregroundStyle(.secondary)
|
.font(.caption).foregroundStyle(.secondary)
|
||||||
|
categoryNote()
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
ProductFormView(prefillBarcode: code, groupId: suggestedGroupId) { created in
|
ProductFormView(prefillBarcode: code, groupId: suggestedGroupId) { created in
|
||||||
unknownCode = nil
|
unknownCode = nil
|
||||||
@@ -168,6 +190,7 @@ struct CheckInView: View {
|
|||||||
do {
|
do {
|
||||||
let result = try await APIClient.shared.lookup(barcode: code)
|
let result = try await APIClient.shared.lookup(barcode: code)
|
||||||
suggestedGroupId = result.groupId
|
suggestedGroupId = result.groupId
|
||||||
|
suggestedGroupName = result.groupName
|
||||||
if let existing = result.existingProduct {
|
if let existing = result.existingProduct {
|
||||||
product = existing
|
product = existing
|
||||||
} else if let hint = result.suggestion {
|
} else if let hint = result.suggestion {
|
||||||
|
|||||||
@@ -2,10 +2,18 @@ import { useState } from "react";
|
|||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Liste zusätzlicher EAN-Codes mit Notiz ("Mehl bei Aldi").
|
* Liste der EAN-Codes.
|
||||||
|
*
|
||||||
|
* Zwei Herkünfte in einer Liste: Codes, die über einen Artikel dazugehören
|
||||||
|
* (nur Anzeige – sie stehen am Artikel, nicht hier), und selbst hinterlegte
|
||||||
|
* Codes, die noch keinem Artikel gehören. Vorher waren nur letztere sichtbar,
|
||||||
|
* weshalb eine Kategorie mit Artikeln „0 EANs“ anzeigte.
|
||||||
|
*
|
||||||
* onAdd({ code, note }) und onDelete(code) werden vom Aufrufer bereitgestellt.
|
* onAdd({ code, note }) und onDelete(code) werden vom Aufrufer bereitgestellt.
|
||||||
*/
|
*/
|
||||||
export default function BarcodeList({ barcodes = [], onAdd, onDelete, disabled = false, hint }) {
|
export default function BarcodeList({
|
||||||
|
barcodes = [], productBarcodes = [], onAdd, onDelete, disabled = false, hint,
|
||||||
|
}) {
|
||||||
const [code, setCode] = useState("");
|
const [code, setCode] = useState("");
|
||||||
const [note, setNote] = useState("");
|
const [note, setNote] = useState("");
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
@@ -26,6 +34,15 @@ export default function BarcodeList({ barcodes = [], onAdd, onDelete, disabled =
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ul className="simple-list">
|
<ul className="simple-list">
|
||||||
|
{productBarcodes.map((b) => (
|
||||||
|
<li key={`p-${b.code}`}>
|
||||||
|
<span style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
|
||||||
|
<code className="strong">{b.code}</code>
|
||||||
|
<span className="badge">Artikel</span>
|
||||||
|
<span className="muted small">{b.product_name}</span>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
{barcodes.map((b) => (
|
{barcodes.map((b) => (
|
||||||
<li key={b.id ?? b.code}>
|
<li key={b.id ?? b.code}>
|
||||||
<span style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
|
<span style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
|
||||||
@@ -40,7 +57,9 @@ export default function BarcodeList({ barcodes = [], onAdd, onDelete, disabled =
|
|||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
{barcodes.length === 0 && <li className="muted small">Noch keine zusätzlichen Codes.</li>}
|
{barcodes.length === 0 && productBarcodes.length === 0 && (
|
||||||
|
<li className="muted small">Noch keine Codes.</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{!disabled && (
|
{!disabled && (
|
||||||
|
|||||||
@@ -85,7 +85,11 @@ export default function CheckIn() {
|
|||||||
const payload = suggestionToProduct(suggestion, groupId);
|
const payload = suggestionToProduct(suggestion, groupId);
|
||||||
const created = await api.createProduct(payload);
|
const created = await api.createProduct(payload);
|
||||||
selectProduct(created);
|
selectProduct(created);
|
||||||
setInfo(`Produkt "${created.name}" angelegt.`);
|
const kategorie = groups.find((g) => String(g.id) === String(created.group_id));
|
||||||
|
setInfo(
|
||||||
|
`Produkt "${created.name}" angelegt` +
|
||||||
|
(kategorie ? ` und der Kategorie "${kategorie.name}" zugeordnet.` : " (ohne Kategorie).")
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -183,10 +187,14 @@ export default function CheckIn() {
|
|||||||
{suggestion.brand ? `${suggestion.brand} · ` : ""}auf Open Food Facts gefunden
|
{suggestion.brand ? `${suggestion.brand} · ` : ""}auf Open Food Facts gefunden
|
||||||
{suggestion.quantity_text ? ` · ${suggestion.quantity_text}` : ""}
|
{suggestion.quantity_text ? ` · ${suggestion.quantity_text}` : ""}
|
||||||
</div>
|
</div>
|
||||||
<div className="muted small">
|
<div className="muted small">Noch nicht im Katalog.</div>
|
||||||
Noch nicht im Katalog.
|
{suggestionGroup.name && (
|
||||||
{suggestionGroup.name && ` Kategorie: ${suggestionGroup.name}`}
|
<div className="assign-note">
|
||||||
</div>
|
<Icon name="tag" size={14} />
|
||||||
|
Wird automatisch der Kategorie <strong>{suggestionGroup.name}</strong> zugeordnet
|
||||||
|
<span className="muted small"> – weil dieser EAN-Code dort hinterlegt ist.</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isAdmin ? (
|
{isAdmin ? (
|
||||||
<button className="btn primary" onClick={createFromSuggestion} disabled={busy}>
|
<button className="btn primary" onClick={createFromSuggestion} disabled={busy}>
|
||||||
@@ -207,6 +215,13 @@ export default function CheckIn() {
|
|||||||
{isAdmin
|
{isAdmin
|
||||||
? <Link to={`/products/new?barcode=${encodeURIComponent(unknownBarcode)}`}>Manuell anlegen</Link>
|
? <Link to={`/products/new?barcode=${encodeURIComponent(unknownBarcode)}`}>Manuell anlegen</Link>
|
||||||
: "Bitte einen Administrator bitten, es anzulegen."}
|
: "Bitte einen Administrator bitten, es anzulegen."}
|
||||||
|
{suggestionGroup.name && (
|
||||||
|
<div className="assign-note">
|
||||||
|
<Icon name="tag" size={14} />
|
||||||
|
Wird automatisch der Kategorie <strong>{suggestionGroup.name}</strong> zugeordnet
|
||||||
|
<span className="muted small"> – weil dieser EAN-Code dort hinterlegt ist.</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export default function Groups() {
|
|||||||
<th className="num">Bestand</th>
|
<th className="num">Bestand</th>
|
||||||
<th>Mindestbestand</th>
|
<th>Mindestbestand</th>
|
||||||
<th>Einheit</th>
|
<th>Einheit</th>
|
||||||
<th className="num">EANs</th>
|
<th className="num">EAN-Codes</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -133,7 +133,9 @@ export default function Groups() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="num">
|
<td className="num">
|
||||||
<button className="link-btn" onClick={() => setSelectedId(g.id)}>
|
<button className="link-btn" onClick={() => setSelectedId(g.id)}>
|
||||||
{g.barcodes?.length || 0} verwalten
|
{/* Beide Herkünfte zählen, sonst steht bei einer Kategorie
|
||||||
|
mit Artikeln irreführend eine 0. */}
|
||||||
|
{(g.product_barcodes?.length || 0) + (g.barcodes?.length || 0)} verwalten
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td className="num">
|
<td className="num">
|
||||||
@@ -161,8 +163,12 @@ export default function Groups() {
|
|||||||
</div>
|
</div>
|
||||||
<BarcodeList
|
<BarcodeList
|
||||||
barcodes={selected.barcodes || []}
|
barcodes={selected.barcodes || []}
|
||||||
|
productBarcodes={selected.product_barcodes || []}
|
||||||
disabled={!isAdmin}
|
disabled={!isAdmin}
|
||||||
hint="Scannst du einen dieser Codes beim Einlagern, wird das neue Produkt automatisch dieser Kategorie zugeordnet."
|
hint={"Mit „Artikel“ gekennzeichnete Codes gehören einem Artikel dieser Kategorie " +
|
||||||
|
"und lassen sich hier nicht ändern. Selbst hinterlegte Codes wirken nur für " +
|
||||||
|
"noch unbekannte Artikel: Wird so ein Code beim Einlagern gescannt, landet " +
|
||||||
|
"der neu angelegte Artikel automatisch in dieser Kategorie."}
|
||||||
onAdd={async (body) => {
|
onAdd={async (body) => {
|
||||||
try {
|
try {
|
||||||
await api.addGroupBarcode(selected.id, body);
|
await api.addGroupBarcode(selected.id, body);
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function Settings() {
|
|||||||
</label>
|
</label>
|
||||||
<p className="muted small mt-0">
|
<p className="muted small mt-0">
|
||||||
Chargen, deren Mindesthaltbarkeitsdatum innerhalb dieser Frist liegt (oder
|
Chargen, deren Mindesthaltbarkeitsdatum innerhalb dieser Frist liegt (oder
|
||||||
bereits überschritten ist), erscheinen auf der Übersicht unter „Bald ablaufend".
|
bereits überschritten ist), erscheinen auf der Übersicht unter „Bald ablaufend“.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="card-head" style={{ marginTop: "var(--sp-5)" }}>
|
<div className="card-head" style={{ marginTop: "var(--sp-5)" }}>
|
||||||
|
|||||||
@@ -304,6 +304,15 @@ td select { width: auto; min-width: 132px; max-width: 100%; }
|
|||||||
/* Umschalter Tagesdatum / Monat im Kopf der Chargenliste */
|
/* Umschalter Tagesdatum / Monat im Kopf der Chargenliste */
|
||||||
.precision-pick { display: flex; align-items: center; gap: var(--sp-2); margin: 0; font-weight: 400; }
|
.precision-pick { display: flex; align-items: center; gap: var(--sp-2); margin: 0; font-weight: 400; }
|
||||||
.precision-pick select { margin: 0; width: auto; padding-top: 3px; padding-bottom: 3px; font-size: 0.8rem; }
|
.precision-pick select { margin: 0; width: auto; padding-top: 3px; padding-bottom: 3px; font-size: 0.8rem; }
|
||||||
|
/* Hinweis beim Einlagern: welche Kategorie automatisch zugeordnet wird */
|
||||||
|
.assign-note {
|
||||||
|
display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
|
||||||
|
margin-top: 4px; padding: 4px 8px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
.input-danger { border-color: var(--danger) !important; }
|
.input-danger { border-color: var(--danger) !important; }
|
||||||
.hint-danger { display: inline-flex; align-items: center; gap: 4px; color: var(--danger); font-size: 0.72rem; margin-top: 3px; }
|
.hint-danger { display: inline-flex; align-items: center; gap: 4px; color: var(--danger); font-size: 0.72rem; margin-top: 3px; }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user