Kleiner Anpassungen / Aktualisierung Dokumentation
This commit is contained in:
@@ -21,7 +21,7 @@ APP_SHORT_NAME=Einkauf
|
|||||||
# Einziger nach aussen veroeffentlichter Port. Dahinter liegt nginx,
|
# Einziger nach aussen veroeffentlichter Port. Dahinter liegt nginx,
|
||||||
# das sowohl die Oberflaeche als auch /api/ ausliefert. Der api-Container
|
# das sowohl die Oberflaeche als auch /api/ ausliefert. Der api-Container
|
||||||
# hat bewusst KEINEN eigenen Port nach aussen.
|
# hat bewusst KEINEN eigenen Port nach aussen.
|
||||||
HTTP_PORT=46600
|
HTTP_PORT=8080
|
||||||
|
|
||||||
# Mailpit-Weboberflaeche, nur mit "--profile dev". An 127.0.0.1 gebunden.
|
# Mailpit-Weboberflaeche, nur mit "--profile dev". An 127.0.0.1 gebunden.
|
||||||
MAILPIT_PORT=8025
|
MAILPIT_PORT=8025
|
||||||
@@ -29,14 +29,14 @@ MAILPIT_PORT=8025
|
|||||||
# Basis-URL, wie Nutzer die App im Browser erreichen - inklusive Port,
|
# Basis-URL, wie Nutzer die App im Browser erreichen - inklusive Port,
|
||||||
# falls kein vorgelagerter Reverse Proxy auf 80/443 steht.
|
# falls kein vorgelagerter Reverse Proxy auf 80/443 steht.
|
||||||
# Landet in Verifikations- und Einladungsmails, muss also stimmen.
|
# Landet in Verifikations- und Einladungsmails, muss also stimmen.
|
||||||
PUBLIC_BASE_URL=http://einkauf.example.de:46600
|
PUBLIC_BASE_URL=http://einkauf.example.de:8080
|
||||||
|
|
||||||
# Welchen Absendern von X-Forwarded-For die API glauben darf.
|
# Welchen Absendern von X-Forwarded-For die API glauben darf.
|
||||||
# "*" ist in Ordnung, solange api keinen oeffentlichen Port hat.
|
# "*" ist in Ordnung, solange api keinen oeffentlichen Port hat.
|
||||||
FORWARDED_ALLOW_IPS=*
|
FORWARDED_ALLOW_IPS=*
|
||||||
|
|
||||||
# Cookie nur über HTTPS ausliefern.
|
# Cookie nur über HTTPS ausliefern.
|
||||||
# false bei direktem HTTP-Zugriff auf Port 46600
|
# false bei direktem HTTP-Zugriff auf Port 8080
|
||||||
# true sobald ein Reverse Proxy mit TLS davorsteht (dann muss
|
# true sobald ein Reverse Proxy mit TLS davorsteht (dann muss
|
||||||
# PUBLIC_BASE_URL ebenfalls auf https:// zeigen)
|
# PUBLIC_BASE_URL ebenfalls auf https:// zeigen)
|
||||||
# Ein Secure-Cookie über reines HTTP wird vom Browser verworfen - die
|
# Ein Secure-Cookie über reines HTTP wird vom Browser verworfen - die
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -43,4 +43,5 @@ dist/
|
|||||||
.vscode/
|
.vscode/
|
||||||
*.swp
|
*.swp
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.zip
|
*.zip
|
||||||
|
doku/
|
||||||
|
|||||||
1465
docs/entwicklung.md
Normal file
1465
docs/entwicklung.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
# Betrieb hinter dem Reverse Proxy (OPNsense)
|
# Betrieb hinter dem Reverse Proxy (OPNsense)
|
||||||
|
|
||||||
Aufbau: Browser → HTTPS → nginx auf der OPNsense → HTTP → Port 46600 auf dem
|
Aufbau: Browser → HTTPS → nginx auf der OPNsense → HTTP → Port 8080 auf dem
|
||||||
Docker-Host → nginx im `web`-Container → `api`.
|
Docker-Host → nginx im `web`-Container → `api`.
|
||||||
|
|
||||||
## Drei Werte in der `.env`
|
## Drei Werte in der `.env`
|
||||||
@@ -15,7 +15,7 @@ Nach jeder Änderung `docker compose up -d`, nicht `restart` – ein Neustart
|
|||||||
übernimmt keine geänderten Umgebungswerte.
|
übernimmt keine geänderten Umgebungswerte.
|
||||||
|
|
||||||
**`PUBLIC_BASE_URL`** landet in jeder Verifikations-, Einladungs- und
|
**`PUBLIC_BASE_URL`** landet in jeder Verifikations-, Einladungs- und
|
||||||
Passwort-Reset-Mail. Steht dort noch `http://…:46600`, führen die Links ins
|
Passwort-Reset-Mail. Steht dort noch `http://…:8080`, führen die Links ins
|
||||||
Leere, sobald jemand von außen klickt.
|
Leere, sobald jemand von außen klickt.
|
||||||
|
|
||||||
**`COOKIE_SECURE=true`** ist ab jetzt Pflicht, aber auch erst ab jetzt möglich:
|
**`COOKIE_SECURE=true`** ist ab jetzt Pflicht, aber auch erst ab jetzt möglich:
|
||||||
|
|||||||
0
tools/check-all.sh
Normal file → Executable file
0
tools/check-all.sh
Normal file → Executable file
0
tools/check-env.sh
Normal file → Executable file
0
tools/check-env.sh
Normal file → Executable file
0
smoke-phase2.sh → tools/smoke-phase2.sh
Normal file → Executable file
0
smoke-phase2.sh → tools/smoke-phase2.sh
Normal file → Executable file
176
tools/smoke-test.sh
Executable file
176
tools/smoke-test.sh
Executable file
@@ -0,0 +1,176 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Durchlauf durch die Phase-2-Endpunkte. Legt eine Testliste an, füllt
|
||||||
|
# sie und prüft die gruppierte Ansicht.
|
||||||
|
#
|
||||||
|
# bash tools/smoke-test.sh http://localhost:8080 admin@example.de
|
||||||
|
#
|
||||||
|
# Das Passwort wird abgefragt, nicht als Argument übergeben. Damit landet
|
||||||
|
# es nicht in der Shell-History, und Sonderzeichen wie ! machen keinen
|
||||||
|
# Ärger: die History-Expansion der Bash greift nur bei interaktiver
|
||||||
|
# Eingabe auf der Kommandozeile, nicht bei "read".
|
||||||
|
#
|
||||||
|
# Alternativ per Umgebungsvariable, z.B. aus einem Passwortmanager:
|
||||||
|
# EA_PASSWORD="$(pass show einkaufsapp)" bash tools/smoke-test.sh URL MAIL
|
||||||
|
#
|
||||||
|
# Benötigt curl sowie jq oder python3.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASE="${1:-http://localhost:8080}"
|
||||||
|
EMAIL="${2:?E-Mail angeben}"
|
||||||
|
|
||||||
|
if [ -n "${EA_PASSWORD:-}" ]; then
|
||||||
|
PASSWORD="$EA_PASSWORD"
|
||||||
|
else
|
||||||
|
printf 'Passwort für %s: ' "$EMAIL" >&2
|
||||||
|
IFS= read -rs PASSWORD
|
||||||
|
printf '\n' >&2
|
||||||
|
fi
|
||||||
|
[ -n "$PASSWORD" ] || { echo "Kein Passwort angegeben." >&2; exit 1; }
|
||||||
|
|
||||||
|
# jq ist bequem, aber nicht zwingend. Ohne jq springt python3 ein -
|
||||||
|
# beides kann JSON korrekt erzeugen und lesen, was hier der Punkt ist:
|
||||||
|
# Passwoerter und Umlaute duerfen nicht per String-Bastelei in die
|
||||||
|
# Nutzlast wandern.
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
JSON_TOOL=jq
|
||||||
|
elif command -v python3 >/dev/null 2>&1; then
|
||||||
|
JSON_TOOL=python3
|
||||||
|
else
|
||||||
|
echo "Benötigt jq oder python3. Installieren mit: apt install jq" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# json_obj key value [key value ...] -> JSON-Objekt auf stdout
|
||||||
|
json_obj() {
|
||||||
|
if [ "$JSON_TOOL" = jq ]; then
|
||||||
|
local args=() filter="{" first=1 i=1
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
args+=(--arg "k$i" "$1" --arg "v$i" "$2")
|
||||||
|
[ $first -eq 1 ] || filter="$filter,"
|
||||||
|
filter="$filter(\$k$i): \$v$i"
|
||||||
|
first=0; i=$((i+1)); shift 2
|
||||||
|
done
|
||||||
|
jq -n "${args[@]}" "$filter}"
|
||||||
|
else
|
||||||
|
python3 -c 'import json,sys; a=sys.argv[1:]; print(json.dumps(dict(zip(a[::2], a[1::2]))))' "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# json_get <schluessel> - liest einen Wert aus JSON auf stdin
|
||||||
|
json_get() {
|
||||||
|
if [ "$JSON_TOOL" = jq ]; then
|
||||||
|
jq -r ".$1 // empty"
|
||||||
|
else
|
||||||
|
python3 -c 'import json,sys
|
||||||
|
try:
|
||||||
|
d = json.load(sys.stdin)
|
||||||
|
except Exception:
|
||||||
|
sys.exit(0)
|
||||||
|
v = d.get(sys.argv[1]) if isinstance(d, dict) else None
|
||||||
|
print("" if v is None else v)' "$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
JAR="$(mktemp)"
|
||||||
|
chmod 600 "$JAR"
|
||||||
|
trap 'rm -f "$JAR"' EXIT
|
||||||
|
|
||||||
|
say() { printf '\n\033[1m== %s\033[0m\n' "$1"; }
|
||||||
|
|
||||||
|
api() {
|
||||||
|
local method="$1" path="$2" body="${3:-}"
|
||||||
|
local csrf
|
||||||
|
csrf="$(awk '/ea_csrf/ {print $7}' "$JAR")"
|
||||||
|
if [ -n "$body" ]; then
|
||||||
|
curl -sS -b "$JAR" -c "$JAR" -X "$method" "$BASE$path" \
|
||||||
|
-H 'Content-Type: application/json' -H "X-CSRF-Token: $csrf" \
|
||||||
|
--data-binary "$body"
|
||||||
|
else
|
||||||
|
curl -sS -b "$JAR" -c "$JAR" -X "$method" "$BASE$path" \
|
||||||
|
-H "X-CSRF-Token: $csrf"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
say "Anmelden"
|
||||||
|
# JSON strukturiert erzeugen, nicht per String-Interpolation: sonst
|
||||||
|
# zerbricht jedes " oder \ im Passwort die Nutzlast.
|
||||||
|
LOGIN_JSON="$(json_obj email "$EMAIL" password "$PASSWORD")"
|
||||||
|
curl -sS -c "$JAR" -X POST "$BASE/api/auth/login" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
--data-binary "$LOGIN_JSON" > /tmp/ea_login.$$ 2>&1
|
||||||
|
WHO="$(json_get email < /tmp/ea_login.$$)"
|
||||||
|
if [ -z "$WHO" ]; then
|
||||||
|
echo " Anmeldung fehlgeschlagen: $(json_get detail < /tmp/ea_login.$$)" >&2
|
||||||
|
rm -f /tmp/ea_login.$$
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " angemeldet als $WHO"
|
||||||
|
rm -f /tmp/ea_login.$$
|
||||||
|
unset PASSWORD LOGIN_JSON
|
||||||
|
|
||||||
|
say "CSRF-Schutz muss greifen (erwartet 403)"
|
||||||
|
code=$(curl -sS -o /dev/null -w '%{http_code}' -b "$JAR" -X POST "$BASE/api/lists" \
|
||||||
|
-H 'Content-Type: application/json' --data-binary '{"name":"Ohne CSRF"}')
|
||||||
|
[ "$code" = "403" ] && echo " ok: $code" || { echo " FEHLER: $code statt 403"; exit 1; }
|
||||||
|
|
||||||
|
say "Liste anlegen"
|
||||||
|
LIST=$(api POST /api/lists "$(json_obj name Wocheneinkauf)" | json_get id)
|
||||||
|
echo " list_id=$LIST"
|
||||||
|
|
||||||
|
say "Märkte anlegen"
|
||||||
|
EDEKA=$(api POST "/api/lists/$LIST/markets" '{"name":"Edeka","sort_order":1}' | json_get id)
|
||||||
|
DM=$(api POST "/api/lists/$LIST/markets" '{"name":"dm","sort_order":2}' | json_get id)
|
||||||
|
echo " Edeka=$EDEKA dm=$DM"
|
||||||
|
|
||||||
|
say "Doppelter Marktname muss abgelehnt werden (erwartet 409)"
|
||||||
|
code=$(curl -sS -o /dev/null -w '%{http_code}' -b "$JAR" -X POST "$BASE/api/lists/$LIST/markets" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H "X-CSRF-Token: $(awk '/ea_csrf/ {print $7}' "$JAR")" \
|
||||||
|
--data-binary '{"name":"Edeka"}')
|
||||||
|
[ "$code" = "409" ] && echo " ok: $code" || { echo " FEHLER: $code statt 409"; exit 1; }
|
||||||
|
|
||||||
|
say "Warengruppen anlegen"
|
||||||
|
MOLK=$(api POST "/api/lists/$LIST/categories" '{"name":"Molkerei","sort_order":1}' | json_get id)
|
||||||
|
OBST=$(api POST "/api/lists/$LIST/categories" '{"name":"Obst und Gemüse","sort_order":2}' | json_get id)
|
||||||
|
|
||||||
|
say "Artikel mit Attributen anlegen"
|
||||||
|
api POST "/api/lists/$LIST/articles" "$(cat <<JSON
|
||||||
|
{"name":"Vollmilch","barcode":"4001234567890",
|
||||||
|
"default_market_id":"$EDEKA","default_category_id":"$MOLK",
|
||||||
|
"attributes":[{"name":"Verpackungseinheit","value":"1 Liter"},
|
||||||
|
{"name":"Fettgehalt","value":"3,5 %"}],
|
||||||
|
"available_market_ids":["$EDEKA"]}
|
||||||
|
JSON
|
||||||
|
)" | json_get name | sed 's/^/ Artikel angelegt: /'
|
||||||
|
|
||||||
|
say "Einträge anlegen (Artikel wird bei Bedarf erzeugt)"
|
||||||
|
for a in Butter Äpfel Bananen Zahnpasta; do
|
||||||
|
api POST "/api/lists/$LIST/items" "$(json_obj article_name "$a")" > /dev/null
|
||||||
|
done
|
||||||
|
MILCH=$(api POST "/api/lists/$LIST/items" \
|
||||||
|
'{"article_name":"Vollmilch","quantity":2,"unit":"l"}' | json_get id)
|
||||||
|
|
||||||
|
say "Zuordnungen setzen"
|
||||||
|
api PATCH "/api/lists/$LIST/items/$MILCH" \
|
||||||
|
"{\"market_id\":\"$EDEKA\",\"category_id\":\"$MOLK\",\"price_cents\":129}" \
|
||||||
|
| json_get article_name | sed 's/^/ Preis gesetzt für: /'
|
||||||
|
|
||||||
|
say "Barcode-Suche"
|
||||||
|
api GET "/api/lists/$LIST/articles/by-barcode/4001234567890" | json_get name | sed 's/^/ gefunden: /'
|
||||||
|
|
||||||
|
say "Gruppierte Ansicht"
|
||||||
|
api GET "/api/lists/$LIST/view" | python3 "$(dirname "$0")/render_view.py"
|
||||||
|
|
||||||
|
say "Abhaken und aufräumen"
|
||||||
|
api PATCH "/api/lists/$LIST/items/$MILCH" '{"status":"bought"}' | json_get status | sed 's/^/ neuer Status: /'
|
||||||
|
api POST "/api/lists/$LIST/items/clear-bought" | json_get removed | sed 's/^/ entfernte Einträge: /'
|
||||||
|
|
||||||
|
say "Fremde Liste darf nicht sichtbar sein (erwartet 404)"
|
||||||
|
code=$(curl -sS -o /dev/null -w '%{http_code}' -b "$JAR" \
|
||||||
|
"$BASE/api/lists/00000000-0000-0000-0000-000000000000")
|
||||||
|
[ "$code" = "404" ] && echo " ok: $code" || { echo " FEHLER: $code statt 404"; exit 1; }
|
||||||
|
|
||||||
|
say "Fertig"
|
||||||
|
echo "Testliste $LIST bleibt bestehen. Löschen mit:"
|
||||||
|
echo " curl -b <cookiejar> -X DELETE $BASE/api/lists/$LIST -H 'X-CSRF-Token: ...'"
|
||||||
@@ -1118,6 +1118,7 @@ body.modal-open { overflow: hidden; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-card .add-fields { margin-top: .6rem; }
|
.modal-card .add-fields { margin-top: .6rem; }
|
||||||
|
.modal-card .add-select { margin-top: .5rem; }
|
||||||
.modal-card .menu-actions { margin-top: 1.2rem; }
|
.modal-card .menu-actions { margin-top: 1.2rem; }
|
||||||
.modal-card .menu-actions button.primary { margin-top: 0; }
|
.modal-card .menu-actions button.primary { margin-top: 0; }
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,14 @@ export function parseCents(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function parseQuantity(text) {
|
export function parseQuantity(text) {
|
||||||
const cleaned = String(text).trim().replace(",", ".");
|
// Leerzeichen und Tausenderpunkte entfernen, Komma als Dezimaltrenner.
|
||||||
|
// Die Punkte fallen weg, weil eine Eingabe wie "1.500" im Deutschen
|
||||||
|
// eintausendfünfhundert meint - und weil formatQuantity keine
|
||||||
|
// Tausendertrennung mehr erzeugt, bleibt kein anderer Fall übrig.
|
||||||
|
const cleaned = String(text)
|
||||||
|
.trim()
|
||||||
|
.replace(/[\s\u00a0.]/g, "")
|
||||||
|
.replace(",", ".");
|
||||||
if (!cleaned) return null;
|
if (!cleaned) return null;
|
||||||
const value = Number(cleaned);
|
const value = Number(cleaned);
|
||||||
return Number.isFinite(value) && value >= 0 ? value : null;
|
return Number.isFinite(value) && value >= 0 ? value : null;
|
||||||
@@ -100,5 +107,13 @@ export function formatQuantity(value) {
|
|||||||
if (value === null || value === undefined) return "";
|
if (value === null || value === undefined) return "";
|
||||||
const num = Number(value);
|
const num = Number(value);
|
||||||
if (!Number.isFinite(num)) return String(value);
|
if (!Number.isFinite(num)) return String(value);
|
||||||
return num.toLocaleString("de-DE", { maximumFractionDigits: 3 });
|
// OHNE Tausendertrennung. Der Wert landet auch in Eingabefeldern, und
|
||||||
|
// parseQuantity liest den deutschen Tausenderpunkt als Dezimalpunkt:
|
||||||
|
// Aus einer 1000-Gramm-Packung wurde beim Bearbeiten eine mit 1 Gramm.
|
||||||
|
// Bei Mengen bringt die Trennung ohnehin wenig - "1000 g" liest sich
|
||||||
|
// gut genug.
|
||||||
|
return num.toLocaleString("de-DE", {
|
||||||
|
maximumFractionDigits: 3,
|
||||||
|
useGrouping: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -652,7 +652,14 @@ export async function listDetailView(
|
|||||||
// Sonst bestätigen lassen - auch bei einem Treffer in der
|
// Sonst bestätigen lassen - auch bei einem Treffer in der
|
||||||
// Produktdatenbank. Fremde Angaben sollen nicht ungeprüft in den
|
// Produktdatenbank. Fremde Angaben sollen nicht ungeprüft in den
|
||||||
// eigenen Artikelstamm wandern.
|
// eigenen Artikelstamm wandern.
|
||||||
const confirmed = await confirmScan(lookup);
|
// Auswahl aus der Eingabezeile als Vorbelegung mitgeben; was der
|
||||||
|
// Dialog zurückliefert, gilt.
|
||||||
|
const confirmed = await confirmScan(lookup, {
|
||||||
|
markets,
|
||||||
|
categories,
|
||||||
|
market: marketField ? marketField.value : null,
|
||||||
|
category: categoryField ? categoryField.value : null,
|
||||||
|
});
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
await change("item.create", {
|
await change("item.create", {
|
||||||
@@ -662,10 +669,8 @@ export async function listDetailView(
|
|||||||
pack_size: parseQuantity(confirmed.packSize),
|
pack_size: parseQuantity(confirmed.packSize),
|
||||||
pack_unit: confirmed.packUnit || null,
|
pack_unit: confirmed.packUnit || null,
|
||||||
variant: confirmed.variant || null,
|
variant: confirmed.variant || null,
|
||||||
...(marketField && marketField.value
|
...(confirmed.marketId ? { market_id: confirmed.marketId } : {}),
|
||||||
? { market_id: marketField.value } : {}),
|
...(confirmed.categoryId ? { category_id: confirmed.categoryId } : {}),
|
||||||
...(categoryField && categoryField.value
|
|
||||||
? { category_id: categoryField.value } : {}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,15 +6,23 @@
|
|||||||
// und sie ändern können, bevor sie im eigenen Bestand landen.
|
// und sie ändern können, bevor sie im eigenen Bestand landen.
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import { el } from "../dom.js";
|
import { el, formatQuantity } from "../dom.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object} lookup Antwort von /api/lists/{id}/barcode/{code}
|
* @param {object} lookup Antwort von /api/lists/{id}/barcode/{code}
|
||||||
* @returns {Promise<{name: string, quantity: string, unit: string, variant: string}|null>}
|
* @param {object} context
|
||||||
|
* @param {Array} context.markets Märkte der Liste
|
||||||
|
* @param {Array} context.categories Warengruppen der Liste
|
||||||
|
* @param {string|null} context.market Vorauswahl aus der Eingabezeile
|
||||||
|
* @param {string|null} context.category dito
|
||||||
|
* @returns {Promise<{name, count, packSize, packUnit, variant,
|
||||||
|
* marketId, categoryId}|null>}
|
||||||
*/
|
*/
|
||||||
export function confirmScan(lookup) {
|
export function confirmScan(lookup, context = {}) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const known = lookup.found && lookup.source === "openfoodfacts";
|
const known = lookup.found && lookup.source === "openfoodfacts";
|
||||||
|
const markets = context.markets || [];
|
||||||
|
const categories = context.categories || [];
|
||||||
|
|
||||||
const nameField = el("input", {
|
const nameField = el("input", {
|
||||||
type: "text",
|
type: "text",
|
||||||
@@ -33,8 +41,10 @@ export function confirmScan(lookup) {
|
|||||||
const qtyField = el("input.qty", {
|
const qtyField = el("input.qty", {
|
||||||
type: "text",
|
type: "text",
|
||||||
inputMode: "decimal",
|
inputMode: "decimal",
|
||||||
value: lookup.pack_size != null
|
// formatQuantity statt roher Zeichenkette: Die API liefert
|
||||||
? String(lookup.pack_size).replace(".", ",") : "",
|
// "200.000", und "200,000" im Feld sieht nach zweihunderttausend
|
||||||
|
// aus.
|
||||||
|
value: lookup.pack_size != null ? formatQuantity(lookup.pack_size) : "",
|
||||||
placeholder: "Gebinde",
|
placeholder: "Gebinde",
|
||||||
title: "Packungsgröße",
|
title: "Packungsgröße",
|
||||||
});
|
});
|
||||||
@@ -51,6 +61,29 @@ export function confirmScan(lookup) {
|
|||||||
placeholder: "Eigenschaft",
|
placeholder: "Eigenschaft",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Warengruppe und Markt wie in der Eingabezeile - der gescannte
|
||||||
|
// Artikel soll ohne Nacharbeit an der richtigen Stelle landen.
|
||||||
|
// Vorbelegt mit dem, was im Formular schon ausgewählt war.
|
||||||
|
const categoryField = categories.length
|
||||||
|
? el("select.category-select", { title: "Warengruppe" },
|
||||||
|
el("option", { value: "" }, "— Vorgabe des Artikels —"),
|
||||||
|
categories.map((c) => el("option", { value: c.id }, c.name)))
|
||||||
|
: null;
|
||||||
|
if (categoryField && context.category
|
||||||
|
&& categories.some((c) => c.id === context.category)) {
|
||||||
|
categoryField.value = context.category;
|
||||||
|
}
|
||||||
|
|
||||||
|
const marketField = markets.length
|
||||||
|
? el("select.market-select", { title: "Markt" },
|
||||||
|
el("option", { value: "" }, "— Vorgabe des Artikels —"),
|
||||||
|
markets.map((m) => el("option", { value: m.id }, m.name)))
|
||||||
|
: null;
|
||||||
|
if (marketField && context.market
|
||||||
|
&& markets.some((m) => m.id === context.market)) {
|
||||||
|
marketField.value = context.market;
|
||||||
|
}
|
||||||
|
|
||||||
function accept() {
|
function accept() {
|
||||||
const name = nameField.value.trim();
|
const name = nameField.value.trim();
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@@ -63,6 +96,8 @@ export function confirmScan(lookup) {
|
|||||||
packSize: qtyField.value,
|
packSize: qtyField.value,
|
||||||
packUnit: unitField.value.trim(),
|
packUnit: unitField.value.trim(),
|
||||||
variant: variantField.value.trim(),
|
variant: variantField.value.trim(),
|
||||||
|
marketId: marketField ? marketField.value || null : null,
|
||||||
|
categoryId: categoryField ? categoryField.value || null : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +118,8 @@ export function confirmScan(lookup) {
|
|||||||
|
|
||||||
el("label", {}, "Name"), nameField,
|
el("label", {}, "Name"), nameField,
|
||||||
el("div.add-fields", {}, countField, qtyField, unitField, variantField),
|
el("div.add-fields", {}, countField, qtyField, unitField, variantField),
|
||||||
|
categoryField ? el("div.add-select", {}, categoryField) : null,
|
||||||
|
marketField ? el("div.add-select", {}, marketField) : null,
|
||||||
|
|
||||||
el("div.menu-actions", {},
|
el("div.menu-actions", {},
|
||||||
el("button.primary", { type: "button", onclick: accept }, "Hinzufügen"),
|
el("button.primary", { type: "button", onclick: accept }, "Hinzufügen"),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
// Bei jeder Änderung an den Dateien unten hochzählen. Der Wert entscheidet,
|
// Bei jeder Änderung an den Dateien unten hochzählen. Der Wert entscheidet,
|
||||||
// wann alte Zwischenspeicher verworfen werden.
|
// wann alte Zwischenspeicher verworfen werden.
|
||||||
const VERSION = "v32";
|
const VERSION = "v33";
|
||||||
const SHELL_CACHE = `einkaufsapp-shell-${VERSION}`;
|
const SHELL_CACHE = `einkaufsapp-shell-${VERSION}`;
|
||||||
|
|
||||||
const SHELL = [
|
const SHELL = [
|
||||||
|
|||||||
Reference in New Issue
Block a user