feat: Produkte aus ApiCore synchronisieren

Der Shop konnte Ernten und Chargen bereits aus ApiCore uebernehmen, den
Produktkatalog aber nicht - jedes Glas musste im Admin ein zweites Mal von
Hand angelegt werden. Neuer Knopf unter /admin/produkte zieht die Produkte
und traegt anschliessend die passende Charge am Produkt nach, wo ApiCore
die Abfuellung einem Produkt zugeordnet hat. Genau diese Verknuepfung
fuettert die Herkunftsseite hinter dem QR-Code auf dem Glas.

ApiCore fuehrt ueberhaupt keine Verkaufspreise. Importierte Produkte
entstehen deshalb als deaktivierte Entwuerfe mit Preis 0, den nur ein
Mensch setzen kann. Damit daraus nie eine Gratis-Bestellung wird, lehnen
die Aktivierung im Admin und der Checkout ein Produkt ohne Preis jetzt
ausdruecklich ab, und die Produktliste kennzeichnet solche Entwuerfe.

Ein bereits vorhandenes Produkt wird uebersprungen, nie ueberschrieben:
genau die drei Felder, die ApiCore liefert (Name, Beschreibung, Gewicht),
zieht der Admin vor der Freischaltung von Hand nach - ein Update wuerde
diese Arbeit bei jedem weiteren Lauf wegwerfen. Ein in ApiCore
ausgelaufenes Produkt wird nicht automatisch deaktiviert, sondern nur
gemeldet, damit der Restbestand abverkauft werden kann.

Nebenbei behoben: die beiden Produktformulare und die Bestelluebersicht
haben den ?error=-Parameter aus fail() nie ausgegeben - abgelehnte
Eingaben landeten wortlos wieder auf dem Formular.

Der Login gegen ApiCore ist jetzt von den Abfragen getrennt
(openApiCoreSession), damit ein Sync-Lauf mit einer einzigen Anmeldung
auskommt statt mit dreien. Die Feld-Abbildung liegt neu in
lib/apicore-mapping.ts und ist damit testbar, statt hinter "use server"
zu verschwinden.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5 committed 2026-09-16 18:03:05 +02:00
1 parent c61ae92390
commit 78c19f1b73
15 files changed
+908 -226

No files matched your search

+13 -1
View File
@@ -903,7 +903,7 @@ export const de: Dictionary = {
create: "Anlegen",
syncHeading: "ApiCore-Synchronisierung",
syncIntro:
"Importiert Ernten UND Chargen aus ApiCore. Ernten werden als Entwürfe angelegt (nicht öffentlich sichtbar); bereits veröffentlichte, zuvor synchronisierte Ernten werden dabei nicht verändert. Chargen werden nur neu angelegt (nie überschrieben, damit echte Lagerbestände erhalten bleiben) und brauchen eine bereits vorhandene Ernte.",
"Importiert Ernten UND Chargen aus ApiCore. Ernten werden als Entwürfe angelegt (nicht öffentlich sichtbar); bereits veröffentlichte, zuvor synchronisierte Ernten werden dabei nicht verändert. Chargen werden nur neu angelegt (nie überschrieben, damit echte Lagerbestände erhalten bleiben) und brauchen eine bereits vorhandene Ernte. Die Produkte selbst kommen über den eigenen Sync-Knopf unter „Produkte“.",
syncButton: "Jetzt synchronisieren",
syncResult: (created, updated, skipped, failed) =>
`Ernten: ${created} neu angelegt, ${updated} aktualisiert, ${skipped} übersprungen (bereits veröffentlicht)${failed > 0 ? `, ${failed} fehlgeschlagen` : ""}.`,
@@ -945,6 +945,18 @@ export const de: Dictionary = {
statusInactive: "Inaktiv",
edit: "Bearbeiten",
noProductsYet: "Noch keine Produkte angelegt.",
syncHeading: "ApiCore-Synchronisierung",
syncIntro:
"Übernimmt den Produktkatalog aus ApiCore. Importierte Produkte sind zunächst deaktiviert und haben keinen Preis — ApiCore führt keine Verkaufspreise. Erst wenn Preis, Gewicht und Beschreibung gepflegt sind, lässt sich ein Produkt aktiv schalten und verkaufen. Vorhandene Produkte werden nur übersprungen, nie überschrieben — so geht keine eigene Pflege verloren. Wo ApiCore eine Abfüllung einem Produkt zuordnet, wird zusätzlich die passende Charge verknüpft (sofern sie schon über den Ernten-Sync importiert wurde).",
syncButton: "Jetzt synchronisieren",
syncResult: (created, skipped, failed) =>
`Produkte: ${created} neu angelegt, ${skipped} übersprungen (bereits vorhanden)${failed > 0 ? `, ${failed} fehlgeschlagen (siehe Server-Log)` : ""}.`,
syncDiscontinued: (count) =>
`${count} aktive${count === 1 ? "s" : ""} Produkt${count === 1 ? "" : "e"} ${count === 1 ? "ist" : "sind"} in ApiCore ausgelaufen. Der Verkauf läuft bewusst weiter, damit der Restbestand abverkauft werden kann — bitte selbst entscheiden, ob deaktiviert werden soll.`,
syncLinked: (count) => `${count} Produkt${count === 1 ? "" : "e"} mit der zugehörigen Charge verknüpft.`,
syncError: "Synchronisierung mit ApiCore fehlgeschlagen.",
linkError: "Chargen konnten nicht geladen werden, die Produkte wurden trotzdem übernommen",
draftHint: "Entwurf: kein Preis gepflegt",
},
adminProductEditPage: {
backToProducts: "Zurück zu Produkten",