fix(labels): eindeutige Platzhalter-Namen, echte Beispiele, Hinweis bei doppeltem Inhalt
- 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:
1 parent
d2581abbd4
commit
b59be8e201
9 files changed
+245
-29
No files matched your search
@@ -919,3 +919,82 @@ describe('SAMPLE_BOTTLING', () => {
|
||||
expect(values.lotLine).toBe('L 2026-0001');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkLabel: the same content twice', () => {
|
||||
function duplicates(elements: LabelTextElement[], values = input().values) {
|
||||
return checkLabel(input({ elements, values })).warnings.filter(
|
||||
(warning) => warning.code === 'DUPLICATE_TEXT',
|
||||
);
|
||||
}
|
||||
|
||||
it('a full label without repeats has no DUPLICATE_TEXT', () => {
|
||||
expect(duplicates(fullLabel())).toEqual([]);
|
||||
});
|
||||
|
||||
it('warns when one placeholder is placed twice', () => {
|
||||
const [finding] = duplicates([
|
||||
...fullLabel(),
|
||||
textElement('legal-2', 'legalName', 86),
|
||||
]);
|
||||
expect(finding.elementIds).toEqual(['legal', 'legal-2']);
|
||||
expect(finding.detail).toContain('„Blütenhonig“');
|
||||
});
|
||||
|
||||
it('warns when two placeholders print the same value (honey type = article name)', () => {
|
||||
const values = {
|
||||
...input().values,
|
||||
honeyType: 'Blütenhonig',
|
||||
productName: 'Blütenhonig',
|
||||
};
|
||||
const [finding] = duplicates(
|
||||
[
|
||||
...fullLabel(),
|
||||
textElement('type', 'honeyType', 86),
|
||||
textElement('name', 'productName', 98),
|
||||
],
|
||||
values,
|
||||
);
|
||||
expect(finding.elementIds).toEqual(['legal', 'type', 'name']);
|
||||
});
|
||||
|
||||
it('warns when the lot number and the lot line both print the lot', () => {
|
||||
const [finding] = duplicates([
|
||||
...fullLabel(),
|
||||
textElement('lot-number', 'lotNumber', 86),
|
||||
]);
|
||||
expect(finding.elementIds).toEqual(['lot-number', 'lot']);
|
||||
expect(finding.detail).toContain('„2026-0001“');
|
||||
});
|
||||
|
||||
it('warns when the best-before date and the best-before line both print the date', () => {
|
||||
const [finding] = duplicates([
|
||||
...fullLabel(),
|
||||
textElement('bb-date', 'bestBeforeDate', 86),
|
||||
]);
|
||||
expect(finding.elementIds).toEqual(['bb-date', 'bb']);
|
||||
});
|
||||
|
||||
it('does not warn about repeated fixed text (the designer shows it as is)', () => {
|
||||
expect(
|
||||
duplicates([
|
||||
...fullLabel(),
|
||||
textElement('brand-1', null, 86, { text: 'Wabenhain' }),
|
||||
textElement('brand-2', null, 98, { text: 'Wabenhain' }),
|
||||
]),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not warn about placeholders without a value', () => {
|
||||
const values = { ...input().values, honeyType: '' };
|
||||
expect(
|
||||
duplicates(
|
||||
[
|
||||
...fullLabel(),
|
||||
textElement('type-1', 'honeyType', 86),
|
||||
textElement('type-2', 'honeyType', 98),
|
||||
],
|
||||
values,
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -23,7 +23,8 @@ export type LabelFindingCode =
|
||||
| 'LOT_PREFIX'
|
||||
| 'X_HEIGHT'
|
||||
| 'NET_QUANTITY_HEIGHT'
|
||||
| 'TEXT_CLIPPED';
|
||||
| 'TEXT_CLIPPED'
|
||||
| 'DUPLICATE_TEXT';
|
||||
|
||||
export interface LabelFinding {
|
||||
code: LabelFindingCode;
|
||||
@@ -96,6 +97,7 @@ const LEGAL_NAME_WORD = 'honig';
|
||||
|
||||
interface Candidate {
|
||||
element: LabelTextElement;
|
||||
text: string;
|
||||
normalized: string;
|
||||
clipped: boolean;
|
||||
}
|
||||
@@ -452,7 +454,7 @@ function candidatesOf(
|
||||
normalized !== '' &&
|
||||
measurer.heightOfStringPt(text, element) >
|
||||
element.height * MM_TO_PT + CLIP_TOLERANCE_PT;
|
||||
return { element, normalized, clipped };
|
||||
return { element, text, normalized, clipped };
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -476,6 +478,52 @@ function unique(ids: string[]): string[] {
|
||||
return [...new Set(ids)];
|
||||
}
|
||||
|
||||
// The first key prints a value the second key's line already holds.
|
||||
const PRINTED_WITHIN: [PlaceholderKey, PlaceholderKey][] = [
|
||||
['lotNumber', 'lotLine'],
|
||||
['bestBeforeDate', 'bestBeforeLine'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Elements that print the same content twice: equal text where a placeholder
|
||||
* is involved (fixed text the designer shows as is), or a value next to the
|
||||
* line that already holds it ("2026-0001" and "L 2026-0001").
|
||||
*/
|
||||
function duplicateTextFinding(candidates: Candidate[]): LabelFinding | null {
|
||||
const filled = candidates.filter((candidate) => candidate.normalized !== '');
|
||||
const byText = new Map<string, Candidate[]>();
|
||||
for (const candidate of filled) {
|
||||
byText.set(candidate.normalized, [
|
||||
...(byText.get(candidate.normalized) ?? []),
|
||||
candidate,
|
||||
]);
|
||||
}
|
||||
const withKey = (key: PlaceholderKey) =>
|
||||
filled.filter((candidate) => candidate.element.placeholderKey === key);
|
||||
const groups = [
|
||||
...[...byText.values()].filter(
|
||||
(group) =>
|
||||
group.length > 1 &&
|
||||
group.some((candidate) => candidate.element.placeholderKey),
|
||||
),
|
||||
...PRINTED_WITHIN.map(([value, line]) => [withKey(value), withKey(line)])
|
||||
.filter(([values, lines]) => values.length > 0 && lines.length > 0)
|
||||
.map(([values, lines]) => [...values, ...lines]),
|
||||
];
|
||||
if (groups.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const texts = unique(
|
||||
groups.map((group) => `„${group[0].text.replace(/\s+/g, ' ').trim()}“`),
|
||||
);
|
||||
return {
|
||||
code: 'DUPLICATE_TEXT',
|
||||
title: 'Gleicher Inhalt mehrfach',
|
||||
detail: `Mehrere Felder drucken denselben Inhalt: ${texts.join(', ')}. Prüfen, ob das gewollt ist, sonst ein Feld entfernen.`,
|
||||
elementIds: unique(groups.flat().map(({ element }) => element.id)),
|
||||
};
|
||||
}
|
||||
|
||||
function xHeightFinding(
|
||||
carriers: Candidate[],
|
||||
minXHeightMm: number,
|
||||
@@ -635,6 +683,11 @@ export function checkLabel(input: LabelCheckInput): LabelCheckResult {
|
||||
warnings.push(netQuantityHeight);
|
||||
}
|
||||
|
||||
const duplicate = duplicateTextFinding(candidates);
|
||||
if (duplicate) {
|
||||
warnings.push(duplicate);
|
||||
}
|
||||
|
||||
if (clippedCarriers.size > 0) {
|
||||
warnings.push({
|
||||
code: 'TEXT_CLIPPED',
|
||||
|
||||
Reference in new issue
Block a user