fix(labels): eindeutige Platzhalter-Namen, echte Beispiele, Hinweis bei doppeltem Inhalt
Backend E2E / e2e (push) Canceled after 0s
Backend Image / build-and-push (push) Successful in 16m7s
Frontend Image / build-and-push (push) Successful in 42m6s

- Die Werte waren richtig verdrahtet; irreführend waren die Namen:
  „Chargennummer“ (Honigcharge) und „Los-/Chargennummer der Abfüllung“
  lasen sich gleich. Jetzt mit Herkunft im Namen, z. B. „Chargennummer der
  Honigcharge (nicht Los)“, „Los-Zeile („L“ + Los-Nr.)“, „MHD (nur Datum)“,
  und einem Beispiel in Druckform unter „Inhalt“.
- Beispielwerte entsprechen dem Druck und sind alle verschieden.
- Etiketten-Prüfung: neue Warnung DUPLICATE_TEXT, wenn zwei Felder den
  gleichen Text drucken oder Nummer und Zeile nebeneinander stehen (nicht
  blockierend, abwärtskompatibel).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick.RosenloecherandClaude Opus 5.5 committed 2026-10-19 19:39:12 +02:00
1 parent d2581abbd4
commit b59be8e201
9 files changed
+245 -29

No files matched your search

@@ -60,4 +60,19 @@ describe("ElementPropertiesPanel", () => {
await userEvent.click(screen.getByRole("button", { name: "Kursiv" }));
expect(onChange).toHaveBeenCalledWith({ italic: true });
});
it("explains a placeholder under the content field, with the sample it prints", () => {
renderPanel({ ...TEXT, placeholderKey: "lotNumber" });
const content = screen.getByRole("combobox", { name: "Inhalt" });
expect(content).toHaveAccessibleDescription(
"Nur die Nummer der Abfüllung, ohne „L“. Mit „L“ davor die Los-Zeile nehmen, nicht beides. Beispiel: „2026-0001“",
);
});
it("shows no placeholder hint for fixed text", () => {
renderPanel(TEXT);
expect(screen.getByRole("combobox", { name: "Inhalt" })).not.toHaveAccessibleDescription();
});
});
@@ -10,6 +10,7 @@ import {
PLACEHOLDER_KEYS,
PLACEHOLDER_LABELS,
} from "@/lib/label-templates/types";
import { PLACEHOLDER_HINTS, PLACEHOLDER_SAMPLES } from "@/lib/label-templates/placeholder-keys";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -135,18 +136,25 @@ function TextFields({
element: Extract<LabelElement, { type: "text" }>;
onChange: (patch: Partial<LabelElement>) => void;
}) {
const key = element.placeholderKey;
return (
<>
<div className="flex flex-col gap-2">
<Label htmlFor="element-text-source">Inhalt</Label>
<EnumSelect
id="element-text-source"
value={element.placeholderKey ?? ""}
onChange={(key) => onChange({ placeholderKey: key === "" ? null : key })}
value={key ?? ""}
onChange={(next) => onChange({ placeholderKey: next === "" ? null : next })}
values={PLACEHOLDER_KEYS}
labels={PLACEHOLDER_LABELS}
noneLabel="Fester Text"
describedBy={key ? "element-text-source-hint" : undefined}
/>
{key ? (
<p id="element-text-source-hint" className="text-xs text-muted-foreground">
{PLACEHOLDER_HINTS[key]} Beispiel: „{PLACEHOLDER_SAMPLES[key]}“
</p>
) : null}
</div>
{!element.placeholderKey ? (
<div className="flex flex-col gap-2">
@@ -78,11 +78,11 @@ describe("LabelDesigner placeholder menu", () => {
expect(insertMenu).not.toBeNull();
await userEvent.click(insertMenu as HTMLElement);
await userEvent.click(await screen.findByRole("option", { name: "Chargennummer" }));
await userEvent.click(await screen.findByRole("option", { name: "Chargennummer der Honigcharge (nicht Los)" }));
// The placeholder element was added and is now edited in the properties panel.
expect(await screen.findByRole("combobox", { name: "Inhalt" })).toHaveTextContent(
"Chargennummer",
"Chargennummer der Honigcharge (nicht Los)",
);
expect(insertMenu).toHaveTextContent("Platzhalter einfügen");
expect(insertMenu).not.toHaveTextContent("batchNumber");
@@ -23,7 +23,8 @@ export type LabelFindingCode =
| "LOT_PREFIX"
| "X_HEIGHT"
| "NET_QUANTITY_HEIGHT"
| "TEXT_CLIPPED";
| "TEXT_CLIPPED"
| "DUPLICATE_TEXT";
/** The six particulars that block printing, in the order the panel lists them. */
export const PARTICULAR_CODES = [
@@ -1,7 +1,12 @@
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { PLACEHOLDER_KEYS, PLACEHOLDER_LABELS } from "./placeholder-keys";
import {
PLACEHOLDER_HINTS,
PLACEHOLDER_KEYS,
PLACEHOLDER_LABELS,
PLACEHOLDER_SAMPLES,
} from "./placeholder-keys";
// The monorepo sits next to the frontend; vitest runs from ApiCore-Frontend.
// The frontend image build only sees ApiCore-Frontend, so the comparison runs
@@ -26,4 +31,36 @@ describe("placeholder keys", () => {
expect(PLACEHOLDER_LABELS[key]).toBeTruthy();
}
});
it("names every key differently, so no two menu entries read alike", () => {
const labels = PLACEHOLDER_KEYS.map((key) => PLACEHOLDER_LABELS[key]);
expect(new Set(labels).size).toBe(labels.length);
// The honey batch number is not the lot of the bottling.
expect(PLACEHOLDER_LABELS.batchNumber).not.toBe(PLACEHOLDER_LABELS.lotNumber);
expect(PLACEHOLDER_LABELS.batchNumber).toMatch(/Honigcharge/);
expect(PLACEHOLDER_LABELS.lotNumber).toMatch(/Abfüllung/);
});
it("has a short hint for every key", () => {
for (const key of PLACEHOLDER_KEYS) {
expect(PLACEHOLDER_HINTS[key]).toBeTruthy();
}
});
it("shows distinct samples, so the preview tells the placeholders apart", () => {
const samples = PLACEHOLDER_KEYS.map((key) => PLACEHOLDER_SAMPLES[key]);
expect(new Set(samples).size).toBe(samples.length);
});
// Same wording as buildPlaceholderValues in the backend.
it("shows samples in the form the label prints them", () => {
expect(PLACEHOLDER_SAMPLES.lotNumber).not.toMatch(/^L/);
expect(PLACEHOLDER_SAMPLES.lotLine).toBe(`L ${PLACEHOLDER_SAMPLES.lotNumber}`);
expect(PLACEHOLDER_SAMPLES.bestBeforeDate).toMatch(/^\d{2}\.\d{2}\.\d{4}$/);
expect(PLACEHOLDER_SAMPLES.bottlingDate).toMatch(/^\d{2}\.\d{2}\.\d{4}$/);
expect(PLACEHOLDER_SAMPLES.bestBeforeLine).toBe(
`Mindestens haltbar bis ${PLACEHOLDER_SAMPLES.bestBeforeDate}`,
);
expect(PLACEHOLDER_SAMPLES.originCountry).toBe("Deutschland");
});
});
@@ -20,38 +20,61 @@ export const PLACEHOLDER_KEYS = [
] as const;
export type PlaceholderKey = (typeof PLACEHOLDER_KEYS)[number];
/** Menu and field names. Each one says where the value comes from. */
export const PLACEHOLDER_LABELS: Record<PlaceholderKey, string> = {
batchNumber: "Chargennummer",
honeyType: "Honigsorte",
lotNumber: "Los-/Chargennummer der Abfüllung",
batchNumber: "Chargennummer der Honigcharge (nicht Los)",
honeyType: "Honigsorte der Honigcharge",
lotNumber: "Los-/Chargennummer der Abfüllung (nur Nummer)",
bottlingDate: "Abfülldatum",
bestBeforeDate: "Mindesthaltbarkeitsdatum (MHD)",
jarSize: "Glasgröße / Füllmenge",
productName: "Produktname",
legalName: "Verkehrsbezeichnung (z. B. Blütenhonig)",
originCountry: "Herkunftsland",
bestBeforeDate: "MHD (nur Datum)",
jarSize: "Füllmenge (Gramm)",
productName: "Artikelname",
legalName: "Verkehrsbezeichnung des Artikels (z. B. Blütenhonig)",
originCountry: "Herkunftsland (nur Land)",
foodBusinessName: "Lebensmittelunternehmer: Name",
foodBusinessAddress: "Lebensmittelunternehmer: Anschrift",
bestBeforeLine: "Zeile „Mindestens haltbar bis …“",
lotLine: "Zeile „L Losnummer“",
bestBeforeLine: "MHD-Zeile („Mindestens haltbar bis …“)",
lotLine: "Los-Zeile („L“ + Los-/Chargennummer)",
};
/** One sentence under the content field: what prints, and what not to combine. */
export const PLACEHOLDER_HINTS: Record<PlaceholderKey, string> = {
batchNumber:
"Nummer der Honigcharge (Ernte). Sie ist nicht die Losnummer der Abfüllung und erfüllt die Pflichtangabe „Los“ nicht.",
honeyType: "Sorte an der Honigcharge. Kann gleich lauten wie Artikelname oder Verkehrsbezeichnung.",
lotNumber:
"Nur die Nummer der Abfüllung, ohne „L“. Mit „L“ davor die Los-Zeile nehmen, nicht beides.",
bottlingDate: "Tag der Abfüllung, nicht das MHD.",
bestBeforeDate:
"Nur das Datum. „Mindestens haltbar bis“ davor selbst setzen oder die MHD-Zeile nehmen, nicht beides.",
jarSize: "Füllmenge der Abfüllung.",
productName: "Name des Artikels im Sortiment.",
legalName: "Steht am Artikel. Ist sie dort leer, druckt das Feld nichts.",
originCountry: "Land aus den Einstellungen, ohne „Herkunft:“ davor.",
foodBusinessName: "Aus den Einstellungen.",
foodBusinessAddress: "Aus den Einstellungen, auch mehrzeilig.",
bestBeforeLine: "Ganze Zeile mit Datum. Nicht zusätzlich das MHD setzen.",
lotLine: "Ganze Zeile mit „L“ und Nummer der Abfüllung. Nicht zusätzlich die Los-/Chargennummer setzen.",
};
/**
* What the designer preview shows for a placeholder instead of the raw
* `{{key}}`: a plausible value, so sizes and line breaks look like the print.
* `{{key}}`: a plausible value in the form the label prints it (see
* `buildPlaceholderValues` in the backend), each one different so the preview
* tells the placeholders apart.
*/
export const PLACEHOLDER_SAMPLES: Record<PlaceholderKey, string> = {
batchNumber: "WH-2026-0001",
honeyType: "Blütenhonig",
lotNumber: "L-0001",
bottlingDate: "15.10.2026",
bestBeforeDate: "10/2028",
batchNumber: "HB-2026-001",
honeyType: "Sommerhonig",
lotNumber: "2026-0001",
bottlingDate: "01.07.2026",
bestBeforeDate: "01.07.2028",
jarSize: "500 g",
productName: "Wabenhain Blütenhonig",
productName: "Wabenhain Sommerhonig",
legalName: "Blütenhonig",
originCountry: "Herkunft: Deutschland",
originCountry: "Deutschland",
foodBusinessName: "Max Mustermann",
foodBusinessAddress: "Musterstr. 1\n86450 Altenmünster",
bestBeforeLine: "Mindestens haltbar bis Ende 10/2028",
lotLine: "L 0001",
bestBeforeLine: "Mindestens haltbar bis 01.07.2028",
lotLine: "L 2026-0001",
};