feat: ApiCore-Sync auf Chargen erweitern
Der bestehende Ernten-Sync (admin/ernten) laedt jetzt zusaetzlich Chargen aus ApiCore. Fachliche Zuordnung recherchiert direkt im ApiCore-Backend-Quellcode (../ApiCore/ApiCore-Backend): Wabenhains "Charge" (model Batch) entspricht ApiCores "BottlingBatch" (Schleuderung -> HoneyBatch -> Abfuellung -> BottlingBatch), NICHT "HoneyBatch" selbst — die Felder passen fast 1:1 (lotNumber/code, date/fillDate, bestBeforeDate/bestBeforeDate, jarCount/glassCount). - lib/apicore-client.ts: fetchApiCoreBottlingBatches() laedt GET /bottling-batches (inkl. verknuepfter HoneyBatch) und GET /extractions (inkl. verknuepfter Ernten) und loest daraus die zugehoerige ApiCore-Ernten-ID pro Charge auf. - admin/actions/apicore-sync.ts: Chargen-Sync laeuft nach dem Ernten-Sync (damit frisch angelegte Ernten schon als harvestId verfuegbar sind), legt Chargen NUR NEU an (nie Update) — anders als Harvest hat Batch kein isPublished-Feld, ein Update wuerde insbesondere remainingGlasses (echter, durch Verkaeufe veraenderter Lagerbestand) stillschweigend zuruecksetzen. Nicht-fatal: schlaegt nur der Chargen-Teil fehl, bleiben bereits synchronisierte Ernten erhalten. - admin/ernten/page.tsx + i18n: zeigt Chargen-Sync-Ergebnis separat an. - .env.example: zwei zusaetzliche ApiCore-Berechtigungen dokumentiert (extractions:read, bottling:read). Verifiziert: tsc --noEmit clean, 118/118 Tests gruen, pnpm build clean. Konnte nicht gegen einen echten ApiCore-Server getestet werden (siehe bereits bestehende Einschraenkung beim Ernten-Sync) — Feldnamen/Shape direkt aus ApiCore-Backends Prisma-Schema und Service-Code entnommen. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
5caa85ce3e
commit
d52753cfef
7 files changed
+231
-17
No files matched your search
+4
-2
@@ -87,8 +87,10 @@ SMTP_FROM="Wabenhain <[email protected]>"
|
|||||||
# Projekt (NestJS-Backend) mit reinem E-Mail/Passwort-Login (kein API-Key-
|
# Projekt (NestJS-Backend) mit reinem E-Mail/Passwort-Login (kein API-Key-
|
||||||
# Mechanismus) — hier IDEALERWEISE die Zugangsdaten eines dedizierten,
|
# Mechanismus) — hier IDEALERWEISE die Zugangsdaten eines dedizierten,
|
||||||
# MFA-freien Service-Kontos mit ausschliesslich den Berechtigungen
|
# MFA-freien Service-Kontos mit ausschliesslich den Berechtigungen
|
||||||
# "harvests:read" (und optional "apiaries:read") eintragen, nicht die eines
|
# "harvests:read", "apiaries:read" (optional, fuer Standortnamen),
|
||||||
# echten Admin-Zugangs. Details siehe src/lib/apicore-client.ts.
|
# "extractions:read" und "bottling:read" (beide fuer den Chargen-Sync)
|
||||||
|
# eintragen, nicht die eines echten Admin-Zugangs. Details siehe
|
||||||
|
# src/lib/apicore-client.ts.
|
||||||
APICORE_API_URL=
|
APICORE_API_URL=
|
||||||
APICORE_SYNC_EMAIL=
|
APICORE_SYNC_EMAIL=
|
||||||
APICORE_SYNC_PASSWORD=
|
APICORE_SYNC_PASSWORD=
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import { prisma } from "@/lib/prisma";
|
|||||||
import { logAdminAction } from "@/lib/audit-log";
|
import { logAdminAction } from "@/lib/audit-log";
|
||||||
import {
|
import {
|
||||||
fetchApiCoreHarvests,
|
fetchApiCoreHarvests,
|
||||||
|
fetchApiCoreBottlingBatches,
|
||||||
ApiCoreConfigError,
|
ApiCoreConfigError,
|
||||||
ApiCoreRequestError,
|
ApiCoreRequestError,
|
||||||
type ApiCoreHarvestWithApiaryName,
|
type ApiCoreHarvestWithApiaryName,
|
||||||
|
type ApiCoreBottlingBatchWithHarvestId,
|
||||||
} from "@/lib/apicore-client";
|
} from "@/lib/apicore-client";
|
||||||
import { requireAdmin } from "./_shared";
|
import { requireAdmin } from "./_shared";
|
||||||
|
|
||||||
@@ -39,12 +41,27 @@ function buildDescription(source: ApiCoreHarvestWithApiaryName): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zieht Ernten aus ApiCore und legt sie als unveroeffentlichte Entwuerfe an
|
* Zieht Ernten UND Chargen aus ApiCore.
|
||||||
* (isPublished: false) bzw. aktualisiert bereits vorhandene, noch
|
*
|
||||||
* unveroeffentlichte Entwuerfe. Bereits vom Admin veroeffentlichte,
|
* Ernten werden als unveroeffentlichte Entwuerfe angelegt (isPublished:
|
||||||
* frueher synchronisierte Ernten werden NICHT angefasst — sonst wuerde ein
|
* false) bzw. bereits vorhandene, noch unveroeffentlichte Entwuerfe
|
||||||
* erneuter Sync eine bewusste Freigabe- und Bearbeitungsentscheidung des
|
* aktualisiert. Bereits vom Admin veroeffentlichte, frueher synchronisierte
|
||||||
* Admins stillschweigend zuruecksetzen.
|
* Ernten werden NICHT angefasst — sonst wuerde ein erneuter Sync eine
|
||||||
|
* bewusste Freigabe- und Bearbeitungsentscheidung des Admins stillschweigend
|
||||||
|
* zuruecksetzen.
|
||||||
|
*
|
||||||
|
* Chargen (Wabenhain: Batch, ApiCore: BottlingBatch — siehe Kommentar in
|
||||||
|
* lib/apicore-client.ts fuer die Modul-Zuordnung) werden dagegen NUR neu
|
||||||
|
* angelegt, nie aktualisiert: anders als Harvest hat Batch kein
|
||||||
|
* isPublished-Feld, das eine bereits erfolgte Admin-Bearbeitung markieren
|
||||||
|
* koennte. Ein Update wuerde insbesondere remainingGlasses ueberschreiben —
|
||||||
|
* das ist Wabenhains EIGENER, durch echte Verkaeufe veraenderter
|
||||||
|
* Lagerbestand und darf niemals durch einen ApiCore-Sync zurueckgesetzt
|
||||||
|
* werden. Existiert eine Charge (per code) bereits, wird sie uebersprungen.
|
||||||
|
* Jede Charge braucht eine bereits in Wabenhain vorhandene (synchronisierte
|
||||||
|
* oder manuell angelegte) Ernte als harvestId — Chargen laufen deshalb nach
|
||||||
|
* den Ernten in diesem Lauf, damit gerade frisch angelegte Ernten schon
|
||||||
|
* verfuegbar sind.
|
||||||
*
|
*
|
||||||
* Admin-Berechtigungsfeld "type" bei Wabenhain (Erntesaison, z.B.
|
* Admin-Berechtigungsfeld "type" bei Wabenhain (Erntesaison, z.B.
|
||||||
* "Sommertracht") und ApiCores "honeyType" (Honigsorte, z.B. "Lindenhonig")
|
* "Sommertracht") und ApiCores "honeyType" (Honigsorte, z.B. "Lindenhonig")
|
||||||
@@ -122,6 +139,87 @@ export async function syncHarvestsFromApiCoreAction(): Promise<void> {
|
|||||||
detail: `${created} neu, ${updated} aktualisiert, ${skipped} übersprungen, ${failed} fehlgeschlagen`,
|
detail: `${created} neu, ${updated} aktualisiert, ${skipped} übersprungen, ${failed} fehlgeschlagen`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Chargen-Sync laeuft NACH den Ernten (s.o., Doc-Kommentar) und ist
|
||||||
|
// bewusst nicht fatal fuer den gesamten Aufruf: schlaegt nur das Laden der
|
||||||
|
// Chargen fehl, sollen die bereits erfolgreich synchronisierten Ernten
|
||||||
|
// trotzdem sichtbar bleiben statt durch einen Redirect mit Fehlerseite
|
||||||
|
// verloren zu gehen.
|
||||||
|
let batchCreated = 0;
|
||||||
|
let batchSkipped = 0;
|
||||||
|
let batchFailed = 0;
|
||||||
|
let batchLoadError: string | null = null;
|
||||||
|
|
||||||
|
let sourceBatches: ApiCoreBottlingBatchWithHarvestId[] = [];
|
||||||
|
try {
|
||||||
|
sourceBatches = await fetchApiCoreBottlingBatches();
|
||||||
|
} catch (error: unknown) {
|
||||||
|
batchLoadError =
|
||||||
|
error instanceof ApiCoreConfigError || error instanceof ApiCoreRequestError
|
||||||
|
? error.message
|
||||||
|
: error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Unbekannter Fehler";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const source of sourceBatches) {
|
||||||
|
if (!source.harvestSourceId) {
|
||||||
|
batchFailed++;
|
||||||
|
console.error(`[apicore-sync] Charge ${source.lotNumber} ist keiner Ernte zugeordnet — übersprungen.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const fillDate = new Date(source.date);
|
||||||
|
const bestBeforeDate = new Date(source.bestBeforeDate);
|
||||||
|
if (Number.isNaN(fillDate.getTime()) || Number.isNaN(bestBeforeDate.getTime())) {
|
||||||
|
batchFailed++;
|
||||||
|
console.error(`[apicore-sync] Charge ${source.lotNumber} hat ein ungültiges Datum — übersprungen.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existing = await prisma.batch.findUnique({ where: { code: source.lotNumber } });
|
||||||
|
if (existing) {
|
||||||
|
batchSkipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const harvest = await prisma.harvest.findUnique({ where: { slug: slugFor(source.harvestSourceId) } });
|
||||||
|
if (!harvest) {
|
||||||
|
batchFailed++;
|
||||||
|
console.error(
|
||||||
|
`[apicore-sync] Charge ${source.lotNumber}: zugehörige Ernte (ApiCore-ID ${source.harvestSourceId}) ist in Wabenhain noch nicht synchronisiert — übersprungen.`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.batch.create({
|
||||||
|
data: {
|
||||||
|
code: source.lotNumber,
|
||||||
|
harvestId: harvest.id,
|
||||||
|
fillDate,
|
||||||
|
bestBeforeDate,
|
||||||
|
glassCount: Math.max(Math.round(source.jarCount), 0),
|
||||||
|
remainingGlasses: Math.max(Math.round(source.jarCount), 0),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
batchCreated++;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
batchFailed++;
|
||||||
|
const message = error instanceof Error ? error.message : "Unbekannter Fehler";
|
||||||
|
console.error(`[apicore-sync] Charge ${source.lotNumber} konnte nicht importiert werden: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await logAdminAction(admin, {
|
||||||
|
action: "batch.sync_apicore",
|
||||||
|
entityType: "Batch",
|
||||||
|
detail: batchLoadError
|
||||||
|
? `Chargen konnten nicht geladen werden: ${batchLoadError}`
|
||||||
|
: `${batchCreated} neu, ${batchSkipped} übersprungen, ${batchFailed} fehlgeschlagen`,
|
||||||
|
});
|
||||||
|
|
||||||
revalidatePath("/admin/ernten");
|
revalidatePath("/admin/ernten");
|
||||||
redirect(`/admin/ernten?synced=${created}&updated=${updated}&skipped=${skipped}&failed=${failed}`);
|
redirect(
|
||||||
|
`/admin/ernten?synced=${created}&updated=${updated}&skipped=${skipped}&failed=${failed}` +
|
||||||
|
`&batchSynced=${batchCreated}&batchSkipped=${batchSkipped}&batchFailed=${batchFailed}` +
|
||||||
|
(batchLoadError ? `&batchLoadError=${encodeURIComponent(batchLoadError)}` : "")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,8 @@ import { getDictionary } from "@/i18n/get-dictionary";
|
|||||||
|
|
||||||
export default async function AdminHarvestsPage(props: PageProps<"/admin/ernten">) {
|
export default async function AdminHarvestsPage(props: PageProps<"/admin/ernten">) {
|
||||||
const t = getDictionary(await getLocale());
|
const t = getDictionary(await getLocale());
|
||||||
const { synced, updated, skipped, failed, syncError } = await props.searchParams;
|
const { synced, updated, skipped, failed, syncError, batchSynced, batchSkipped, batchFailed, batchLoadError } =
|
||||||
|
await props.searchParams;
|
||||||
const harvests = await prisma.harvest.findMany({
|
const harvests = await prisma.harvest.findMany({
|
||||||
orderBy: [{ year: "desc" }, { harvestDate: "desc" }],
|
orderBy: [{ year: "desc" }, { harvestDate: "desc" }],
|
||||||
include: { _count: { select: { batches: true, products: true } } },
|
include: { _count: { select: { batches: true, products: true } } },
|
||||||
@@ -29,6 +30,16 @@ export default async function AdminHarvestsPage(props: PageProps<"/admin/ernten"
|
|||||||
{t.adminHarvestsPage.syncError} ({syncError})
|
{t.adminHarvestsPage.syncError} ({syncError})
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
{batchSynced !== undefined ? (
|
||||||
|
<p className="mb-6 text-sm text-forest bg-forest/5 border border-forest/20 px-4 py-3">
|
||||||
|
{t.adminHarvestsPage.batchSyncResult(Number(batchSynced), Number(batchSkipped ?? 0), Number(batchFailed ?? 0))}
|
||||||
|
</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.adminHarvestsPage.batchSyncError} ({batchLoadError})
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="overflow-x-auto bg-background border border-ink/10">
|
<div className="overflow-x-auto bg-background border border-ink/10">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
|
|||||||
@@ -897,11 +897,14 @@ export const de: Dictionary = {
|
|||||||
create: "Anlegen",
|
create: "Anlegen",
|
||||||
syncHeading: "ApiCore-Synchronisierung",
|
syncHeading: "ApiCore-Synchronisierung",
|
||||||
syncIntro:
|
syncIntro:
|
||||||
"Importiert Ernten aus ApiCore als Entwürfe (nicht öffentlich sichtbar). Bereits veröffentlichte, zuvor synchronisierte Ernten werden dabei nicht verändert — nur neue oder noch unveröffentlichte Entwürfe werden aktualisiert.",
|
"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.",
|
||||||
syncButton: "Jetzt synchronisieren",
|
syncButton: "Jetzt synchronisieren",
|
||||||
syncResult: (created, updated, skipped, failed) =>
|
syncResult: (created, updated, skipped, failed) =>
|
||||||
`Synchronisierung abgeschlossen: ${created} neu angelegt, ${updated} aktualisiert, ${skipped} übersprungen (bereits veröffentlicht)${failed > 0 ? `, ${failed} fehlgeschlagen` : ""}.`,
|
`Ernten: ${created} neu angelegt, ${updated} aktualisiert, ${skipped} übersprungen (bereits veröffentlicht)${failed > 0 ? `, ${failed} fehlgeschlagen` : ""}.`,
|
||||||
syncError: "Synchronisierung mit ApiCore fehlgeschlagen.",
|
syncError: "Synchronisierung mit ApiCore fehlgeschlagen.",
|
||||||
|
batchSyncResult: (created, skipped, failed) =>
|
||||||
|
`Chargen: ${created} neu angelegt, ${skipped} übersprungen (bereits vorhanden)${failed > 0 ? `, ${failed} fehlgeschlagen (siehe Server-Log)` : ""}.`,
|
||||||
|
batchSyncError: "Chargen konnten nicht von ApiCore geladen werden",
|
||||||
},
|
},
|
||||||
adminHarvestEditPage: {
|
adminHarvestEditPage: {
|
||||||
backToHarvests: "Zurück zu Ernten",
|
backToHarvests: "Zurück zu Ernten",
|
||||||
|
|||||||
@@ -897,11 +897,14 @@ export const en: Dictionary = {
|
|||||||
create: "Create",
|
create: "Create",
|
||||||
syncHeading: "ApiCore sync",
|
syncHeading: "ApiCore sync",
|
||||||
syncIntro:
|
syncIntro:
|
||||||
"Imports harvests from ApiCore as drafts (not publicly visible). Previously synced harvests that are already published are left untouched — only new or still-unpublished drafts are updated.",
|
"Imports harvests AND batches from ApiCore. Harvests are created as drafts (not publicly visible) — previously synced harvests that are already published are left untouched. Batches are only ever created, never updated (so real stock levels are never overwritten), and need an already-existing harvest.",
|
||||||
syncButton: "Sync now",
|
syncButton: "Sync now",
|
||||||
syncResult: (created, updated, skipped, failed) =>
|
syncResult: (created, updated, skipped, failed) =>
|
||||||
`Sync complete: ${created} created, ${updated} updated, ${skipped} skipped (already published)${failed > 0 ? `, ${failed} failed` : ""}.`,
|
`Harvests: ${created} created, ${updated} updated, ${skipped} skipped (already published)${failed > 0 ? `, ${failed} failed` : ""}.`,
|
||||||
syncError: "Sync with ApiCore failed.",
|
syncError: "Sync with ApiCore failed.",
|
||||||
|
batchSyncResult: (created, skipped, failed) =>
|
||||||
|
`Batches: ${created} created, ${skipped} skipped (already exist)${failed > 0 ? `, ${failed} failed (see server log)` : ""}.`,
|
||||||
|
batchSyncError: "Batches could not be loaded from ApiCore",
|
||||||
},
|
},
|
||||||
adminHarvestEditPage: {
|
adminHarvestEditPage: {
|
||||||
backToHarvests: "Back to harvests",
|
backToHarvests: "Back to harvests",
|
||||||
|
|||||||
@@ -603,6 +603,9 @@ export interface Dictionary {
|
|||||||
/** created, updated, skipped, failed */
|
/** created, updated, skipped, failed */
|
||||||
syncResult: (created: number, updated: number, skipped: number, failed: number) => string;
|
syncResult: (created: number, updated: number, skipped: number, failed: number) => string;
|
||||||
syncError: string;
|
syncError: string;
|
||||||
|
/** created, skipped, failed */
|
||||||
|
batchSyncResult: (created: number, skipped: number, failed: number) => string;
|
||||||
|
batchSyncError: string;
|
||||||
};
|
};
|
||||||
adminHarvestEditPage: {
|
adminHarvestEditPage: {
|
||||||
backToHarvests: string;
|
backToHarvests: string;
|
||||||
|
|||||||
@@ -23,9 +23,22 @@ import { z } from "zod";
|
|||||||
* APICORE_SYNC_EMAIL E-Mail des Service-Kontos
|
* APICORE_SYNC_EMAIL E-Mail des Service-Kontos
|
||||||
* APICORE_SYNC_PASSWORD Passwort des Service-Kontos
|
* APICORE_SYNC_PASSWORD Passwort des Service-Kontos
|
||||||
*
|
*
|
||||||
* Ohne diese drei Variablen wirft fetchApiCoreHarvests() sofort einen
|
* Ohne diese drei Variablen wirft fetchApiCoreHarvests()/
|
||||||
* ApiCoreConfigError — der Aufrufer (apicore-sync.ts) zeigt dessen Message
|
* fetchApiCoreBottlingBatches() sofort einen ApiCoreConfigError — der
|
||||||
* dem Admin an, statt die Aktion stillschweigend nichts tun zu lassen.
|
* Aufrufer (apicore-sync.ts) zeigt dessen Message dem Admin an, statt die
|
||||||
|
* Aktion stillschweigend nichts tun zu lassen.
|
||||||
|
*
|
||||||
|
* Fuer Wabenhains "Charge" (prisma/schema.prisma: model Batch) ist NICHT
|
||||||
|
* ApiCores "HoneyBatch" (roher Rohhonig direkt nach der Schleuderung, vor
|
||||||
|
* dem Abfuellen) das fachlich richtige Gegenstueck, sondern "BottlingBatch"
|
||||||
|
* (Schleuderung -> HoneyBatch -> Abfuellung -> BottlingBatch) — dessen
|
||||||
|
* Felder passen fast 1:1: lotNumber/code, date/fillDate,
|
||||||
|
* bestBeforeDate/bestBeforeDate, jarCount/glassCount. Die zugehoerige Ernte
|
||||||
|
* ergibt sich erst ueber zwei Stufen (BottlingBatch.honeyBatch.extractionId
|
||||||
|
* -> Extraction.harvests[].harvestId), daher werden zusaetzlich alle
|
||||||
|
* Schleuderungen geladen (GET /extractions) und clientseitig verknuepft.
|
||||||
|
* Das erfordert zusaetzlich die Berechtigungen "extractions:read" und
|
||||||
|
* "bottling:read" fuer das Sync-Service-Konto.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class ApiCoreConfigError extends Error {}
|
export class ApiCoreConfigError extends Error {}
|
||||||
@@ -132,3 +145,84 @@ export async function fetchApiCoreHarvests(): Promise<ApiCoreHarvestWithApiaryNa
|
|||||||
apiaryName: apiaryNames.get(harvest.apiaryId) ?? null,
|
apiaryName: apiaryNames.get(harvest.apiaryId) ?? null,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Chargen (BottlingBatch, siehe Modul-Kommentar oben)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const extractionSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
// ApiCores GET /extractions liefert je Schleuderung die verknuepften
|
||||||
|
// Ernten als ExtractionHarvest-Zeilen — pro Zeile reicht harvestId, die
|
||||||
|
// uebrigen (verschachtelten Volk-/Standort-)Felder werden nicht gebraucht.
|
||||||
|
harvests: z.array(z.object({ harvestId: z.string() })),
|
||||||
|
});
|
||||||
|
|
||||||
|
const bottlingBatchSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
lotNumber: z.string(),
|
||||||
|
date: z.string(),
|
||||||
|
bestBeforeDate: z.string(),
|
||||||
|
jarCount: z.number(),
|
||||||
|
honeyBatch: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
extractionId: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ApiCoreBottlingBatch = z.infer<typeof bottlingBatchSchema>;
|
||||||
|
|
||||||
|
export interface ApiCoreBottlingBatchWithHarvestId extends ApiCoreBottlingBatch {
|
||||||
|
/**
|
||||||
|
* ApiCore-ID der ERSTEN mit der Schleuderung verknuepften Ernte, oder null
|
||||||
|
* wenn die Schleuderung (noch) keiner Ernte zugeordnet ist. Eine
|
||||||
|
* Schleuderung kann fachlich mehrere Ernten buendeln (ExtractionHarvest
|
||||||
|
* ist many-to-many) — Wabenhains Batch.harvestId ist dagegen ein
|
||||||
|
* einzelner Pflicht-Fremdschluessel, daher bewusst nur die erste
|
||||||
|
* verknuepfte Ernte. Admin kann das beim Review in Wabenhain jederzeit
|
||||||
|
* auf die tatsaechlich passende Ernte korrigieren.
|
||||||
|
*/
|
||||||
|
harvestSourceId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchApiCoreBottlingBatches(): Promise<ApiCoreBottlingBatchWithHarvestId[]> {
|
||||||
|
const { baseUrl, email, password } = getConfig();
|
||||||
|
const token = await login(baseUrl, email, password);
|
||||||
|
const authHeader = { Authorization: `Bearer ${token}` };
|
||||||
|
|
||||||
|
let extractionsRes: Response;
|
||||||
|
let batchesRes: Response;
|
||||||
|
try {
|
||||||
|
[extractionsRes, batchesRes] = await Promise.all([
|
||||||
|
fetch(`${baseUrl}/extractions`, { headers: authHeader }),
|
||||||
|
fetch(`${baseUrl}/bottling-batches`, { headers: authHeader }),
|
||||||
|
]);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : "Unbekannter Fehler";
|
||||||
|
throw new ApiCoreRequestError(`ApiCore unter ${baseUrl} nicht erreichbar: ${message}`);
|
||||||
|
}
|
||||||
|
if (!extractionsRes.ok) {
|
||||||
|
throw new ApiCoreRequestError(`ApiCore: Schleuderungen konnten nicht geladen werden (HTTP ${extractionsRes.status}).`);
|
||||||
|
}
|
||||||
|
if (!batchesRes.ok) {
|
||||||
|
throw new ApiCoreRequestError(`ApiCore: Chargen konnten nicht geladen werden (HTTP ${batchesRes.status}).`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedExtractions = z.array(extractionSchema).safeParse(await extractionsRes.json());
|
||||||
|
if (!parsedExtractions.success) {
|
||||||
|
throw new ApiCoreRequestError("ApiCore: unerwartete Antwortstruktur fuer Schleuderungen.");
|
||||||
|
}
|
||||||
|
const parsedBatches = z.array(bottlingBatchSchema).safeParse(await batchesRes.json());
|
||||||
|
if (!parsedBatches.success) {
|
||||||
|
throw new ApiCoreRequestError("ApiCore: unerwartete Antwortstruktur fuer Chargen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstHarvestByExtraction = new Map<string, string | null>(
|
||||||
|
parsedExtractions.data.map((extraction) => [extraction.id, extraction.harvests[0]?.harvestId ?? null])
|
||||||
|
);
|
||||||
|
|
||||||
|
return parsedBatches.data.map((batch) => ({
|
||||||
|
...batch,
|
||||||
|
harvestSourceId: firstHarvestByExtraction.get(batch.honeyBatch.extractionId) ?? null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
Reference in new issue
Block a user