feat: ein Knopf synchronisiert Ernten, Chargen und Produkte zusammen
Bisher gab es zwei Knoepfe auf zwei Seiten, und ihre Reihenfolge war nicht beliebig: eine Charge braucht ihre Ernte als Pflicht-Fremdschluessel, ein Produkt entsteht je Abfuellung und braucht die zugehoerige Charge. Wer mit den Produkten anfing, bekam reihenweise "wartet auf den Ernten-Sync" und musste danach noch einmal klicken. Diese Reihenfolge gehoert in den Ablauf, nicht in den Kopf der bedienenden Person. syncApiCoreAction ersetzt beide Actions und laeuft Ernten -> Chargen -> Produkte in einem Durchgang. "Alles synchronisieren" steht auf beiden Seiten und springt danach dorthin zurueck, von wo er ausgeloest wurde. Das Ziel kommt aus einem Formularfeld und wird gegen eine feste Liste geprueft — ein ungeprueftes returnTo waere eine offene Weiterleitung. Nebeneffekt: die Abfuellungen werden nur noch EINMAL von ApiCore geladen. Vorher holten Chargen- und Produkt-Sync dieselbe Liste getrennt. Knopf und Ergebnisanzeige liegen jetzt in einer gemeinsamen Komponente (components/apicore-sync-panel.tsx) statt zweimal in zwei Seiten. Die Ergebnisse stehen als ein Block beieinander, je eine Zeile fuer Ernten, Chargen und Produkte, plus die beiden Hinweise (Abfuellung ohne Charge, ausgelaufener Artikel) als Hinweis statt als Fehler. Damit wurden zehn i18n-Schluessel ueberfluessig, die vorher die beiden getrennten Anzeigen bedienten; sie sind aus dem Vertrag und beiden Sprachdateien entfernt. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
8f0f06fd13
commit
92ac826baa
8 files changed
+246
-213
No files matched your search
@@ -0,0 +1,104 @@
|
||||
import { syncApiCoreAction } from "@/app/admin/actions";
|
||||
import { getLocale } from "@/i18n/locale";
|
||||
import { getDictionary } from "@/i18n/get-dictionary";
|
||||
|
||||
/**
|
||||
* Der eine Sync-Knopf und die Anzeige seines Ergebnisses.
|
||||
*
|
||||
* Gemeinsam genutzt von /admin/ernten und /admin/produkte: es ist derselbe
|
||||
* Vorgang, egal von welcher Seite er ausgeloest wird — Ernten, Chargen und
|
||||
* Produkte laufen ohnehin immer zusammen. Zwei getrennte Bloecke in zwei
|
||||
* Seiten waeren dieselbe Anzeige zweimal gepflegt.
|
||||
*/
|
||||
|
||||
/** Rohwerte aus der Adresszeile — Next liefert Query-Parameter so aus. */
|
||||
export type SyncSearchParams = Record<string, string | string[] | undefined>;
|
||||
|
||||
function count(value: string | string[] | undefined): number {
|
||||
return Number(Array.isArray(value) ? value[0] : value) || 0;
|
||||
}
|
||||
|
||||
function text(value: string | string[] | undefined): string | undefined {
|
||||
const first = Array.isArray(value) ? value[0] : value;
|
||||
return first === undefined ? undefined : String(first);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ergebnis des letzten Laufs. Zeigt nichts an, solange gerade nicht
|
||||
* synchronisiert wurde — die Seite soll nach einem spaeteren Aufruf nicht
|
||||
* dauerhaft ein altes Ergebnis behaupten.
|
||||
*/
|
||||
export async function ApiCoreSyncResult({ searchParams }: { searchParams: SyncSearchParams }) {
|
||||
const t = getDictionary(await getLocale()).adminApiCoreSync;
|
||||
const syncError = text(searchParams.syncError);
|
||||
const batchLoadError = text(searchParams.batchLoadError);
|
||||
const done = text(searchParams.syncDone) !== undefined;
|
||||
|
||||
if (!done && syncError === undefined) return null;
|
||||
|
||||
const pending = count(searchParams.pPending);
|
||||
const discontinued = count(searchParams.pDiscontinued);
|
||||
|
||||
return (
|
||||
<>
|
||||
{syncError !== undefined ? (
|
||||
<p className="mb-6 text-sm text-red-700 bg-red-50 border border-red-200 px-4 py-3">
|
||||
{t.error} ({syncError})
|
||||
</p>
|
||||
) : null}
|
||||
{done ? (
|
||||
<div className="mb-6 text-sm text-forest bg-forest/5 border border-forest/20 px-4 py-3 space-y-1">
|
||||
<p>
|
||||
{t.resultHarvests(
|
||||
count(searchParams.hNew),
|
||||
count(searchParams.hUpdated),
|
||||
count(searchParams.hSkipped),
|
||||
count(searchParams.hFailed)
|
||||
)}
|
||||
</p>
|
||||
<p>{t.resultBatches(count(searchParams.bNew), count(searchParams.bSkipped), count(searchParams.bFailed))}</p>
|
||||
<p>{t.resultProducts(count(searchParams.pNew), count(searchParams.pSkipped), count(searchParams.pFailed))}</p>
|
||||
</div>
|
||||
) : null}
|
||||
{/* Beides sind Hinweise, keine Fehler: die Abfuellung ist in Ordnung,
|
||||
es fehlt nur etwas drumherum bzw. der Artikel laeuft aus. */}
|
||||
{pending > 0 ? (
|
||||
<p className="mb-6 text-sm text-honey-dark bg-honey/10 border border-honey/30 px-4 py-3">
|
||||
{t.pending(pending)}
|
||||
</p>
|
||||
) : null}
|
||||
{discontinued > 0 ? (
|
||||
<p className="mb-6 text-sm text-honey-dark bg-honey/10 border border-honey/30 px-4 py-3">
|
||||
{t.discontinued(discontinued)}
|
||||
</p>
|
||||
) : null}
|
||||
{batchLoadError !== undefined ? (
|
||||
<p className="mb-6 text-sm text-red-700 bg-red-50 border border-red-200 px-4 py-3">
|
||||
{t.batchLoadError} ({batchLoadError})
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Knopf selbst. "returnTo" bestimmt, auf welche Seite es danach
|
||||
* zurueckgeht — die Action prueft den Wert gegen eine feste Liste, damit
|
||||
* daraus keine offene Weiterleitung wird.
|
||||
*/
|
||||
export async function ApiCoreSyncPanel({ returnTo }: { returnTo: string }) {
|
||||
const t = getDictionary(await getLocale()).adminApiCoreSync;
|
||||
|
||||
return (
|
||||
<div className="bg-background border border-ink/10 p-6">
|
||||
<h2 className="text-lg text-forest mb-4">{t.heading}</h2>
|
||||
<p className="text-sm text-ink/65 mb-4">{t.intro}</p>
|
||||
<form action={syncApiCoreAction}>
|
||||
<input type="hidden" name="returnTo" value={returnTo} />
|
||||
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
|
||||
{t.button}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user