SKU-Listen, CSV-Import und GraphQL-Batch-Mutation für Sammelbestellungen
Wer im B2B-Vertrieb regelmäßig große Stückzahlen bekannter Artikel bestellt, braucht mehr als die klassische Produktdetailseite. Ein Quick-Order-Formular mit freier SKU- und Mengen-Eingabe, optionalem CSV-Import und einer GraphQL-Batch-Mutation im Hintergrund verkürzt den Weg vom Einkaufszettel bis in den Warenkorb erheblich, inklusive Echtzeit-Validierung gegen den tatsächlichen Lagerbestand.
Inhaltsverzeichnis
- 1. Quick-Order im B2B-Kontext: Wo die Schnellbestellung ansetzt
- 2. Alpine.js-Komponente für die dynamische SKU-Zeilen-Eingabe
- 3. CSV-Import als Alternative zur manuellen Zeilen-Eingabe
- 4. GraphQL-Batch-Mutation zum Hinzufügen mehrerer Produkte
- 5. Lagerbestand in Echtzeit prüfen: Debounce und Stock-Abfrage
- 6. Fehler-Feedback pro Zeile: SKU nicht gefunden und Lagerbestand-Konflikte
- 7. Bestelllisten für wiederkehrende Sammelbestellungen
- 8. Performance bei großen SKU-Listen: Chunking, Debounce und Fortschritt
- 9. Zusammenspiel mit Firmenkonto und Freigabe-Workflows
- 10. Zusammenfassung
- 11. FAQ
1. Quick-Order im B2B-Kontext: Wo die Schnellbestellung ansetzt
Im B2B-Einkauf wiederholt sich ein bestimmtes Muster ständig: Ein Facility-Manager, ein Einkäufer oder eine Assistenz kennt die benötigten Artikelnummern bereits genau, oft aus einer ERP-Exportliste oder einer internen Excel-Tabelle, und möchte sie ohne Umwege über Produktsuche, Kategoriefilter und einzelne Produktdetailseiten in den Warenkorb bekommen. Genau für diesen Anwendungsfall existiert das Quick-Order-Formular als eigenständiger Bestellweg neben dem regulären PDP-Flow.
Wichtig ist die Abgrenzung zur Reorder-Funktion: Reorder wiederholt eine bereits im System vorhandene, konkrete Bestellung aus der Bestellhistorie mit exakt derselben Zusammenstellung aus SKUs und Mengen. Quick-Order kennt dagegen keine Bindung an eine vergangene Bestellung. Der Kunde tippt oder fügt beliebige SKUs und Mengen ein, unabhängig davon, ob diese Kombination jemals zuvor bestellt wurde. Beide Funktionen ergänzen sich, lösen aber unterschiedliche Probleme und sollten im Frontend auch als getrennte Einstiegspunkte sichtbar bleiben.
Ebenso wichtig ist die Abgrenzung zum breiteren Thema Firmenkonten und B2B-Funktionen im Hyvä-Theme. Dieser Artikel behandelt ausschließlich das Quick-Order-Formular selbst: die Eingabe, die Validierung und den Weg der Daten bis in den Warenkorb. Firmenkonten, Rollen und Freigabe-Workflows werden am Ende nur kurz im Zusammenspiel eingeordnet, nicht im Detail erklärt.
2. Alpine.js-Komponente für die dynamische SKU-Zeilen-Eingabe
Das Herzstück des Quick-Order-Formulars ist eine Alpine.js-Komponente, die eine Liste von Zeilen verwaltet, jede Zeile mit einem Feld für die SKU und einem Feld für die Menge. Neue Zeilen lassen sich per Klick hinzufügen, leere oder nicht mehr benötigte Zeilen per Klick entfernen. Da Hyvä konsequent auf Alpine.js statt Knockout.js setzt, bleibt die Komponente schlank und ohne zusätzliche Build-Abhängigkeiten direkt im Template nutzbar.
Jede Zeile trägt zusätzlich einen internen Status, der von idle über prüfe bis hin zu verfügbar oder Fehlermeldung reicht. Dieser Status wird im nächsten Abschnitt durch eine echte GraphQL-Abfrage befüllt, hier zunächst nur die reine Formularstruktur mit x-for über das Zeilen-Array und x-model für die Zwei-Wege-Bindung von SKU und Menge.
<div x-data="quickOrderForm()" class="not-prose">
<template x-for="(row, index) in rows" :key="row.id">
<div class="grid grid-cols-12 gap-2 items-center mb-2">
<input
type="text"
x-model="row.sku"
@input.debounce.400ms="validateRow(index)"
placeholder="SKU"
class="col-span-5 border rounded px-2 py-1"
/>
<input
type="number"
min="1"
x-model.number="row.qty"
@input.debounce.400ms="validateRow(index)"
class="col-span-3 border rounded px-2 py-1"
/>
<span class="col-span-3 text-sm" x-text="row.status"></span>
<button type="button" @click="removeRow(index)" class="col-span-1 text-red-600">×</button>
</div>
</template>
<button type="button" @click="addRow()" class="mt-2 text-sm font-medium">+ Zeile hinzufügen</button>
</div>
<script>
function quickOrderForm() {
return {
rows: [{ id: 1, sku: '', qty: 1, status: '' }],
nextId: 2,
addRow() {
this.rows.push({ id: this.nextId++, sku: '', qty: 1, status: '' });
},
removeRow(index) {
this.rows.splice(index, 1);
},
async validateRow(index) {
const row = this.rows[index];
if (!row.sku) return;
row.status = 'prüfe...';
// Der GraphQL-Check gegen den Lagerbestand folgt in Abschnitt 5
}
};
}
</script>
3. CSV-Import als Alternative zur manuellen Zeilen-Eingabe
Für Sammelbestellungen mit mehreren Dutzend oder Hundert Positionen ist manuelles Eintippen unrealistisch. Ein CSV-Import, meist direkt aus dem ERP-System oder einer Excel-Tabelle des Kunden exportiert, ersetzt die manuelle Eingabe durch einen einzelnen Datei-Upload. Zwei Spalten genügen im Regelfall pro Position: SKU und Menge, getrennt durch Komma oder Semikolon.
Beim Parsing gibt es eine grundsätzliche Entscheidung zwischen clientseitig und serverseitig. Clientseitiges Parsing im Browser liefert sofortiges Feedback ohne Round-Trip zum Server, stößt aber bei sehr großen Dateien und bei Encoding-Fallstricken wie Windows-1252-Exporten aus Excel an Grenzen. Serverseitiges Parsing über einen dedizierten Upload-Endpoint normalisiert Trennzeichen und Zeichensatz zuverlässiger und eignet sich besser für sehr große Listen, kostet aber einen zusätzlichen Request und etwas mehr Implementierungsaufwand.
In der Praxis hat sich eine Kombination bewährt: kleine Dateien bis einige hundert Zeilen werden clientseitig geparst und sofort in die Zeilen-Tabelle übernommen, größere Dateien gehen an einen Server-Endpoint, der die Validierung übernimmt und ein strukturiertes Ergebnis inklusive Fehlerliste zurückliefert.
function parseCsv(text) {
const lines = text.trim().split(/\r?\n/);
const separator = lines[0].includes(';') ? ';' : ',';
return lines
.map((line) => {
const [sku, qty] = line.split(separator).map((v) => v.trim());
return { sku, qty: Number(qty) || 1 };
})
.filter((row) => row.sku && row.sku.toLowerCase() !== 'sku');
}
document.getElementById('csvUpload').addEventListener('change', async (event) => {
const file = event.target.files[0];
if (!file) return;
const text = await file.text();
const parsedRows = parseCsv(text);
if (parsedRows.length > 500) {
// Große Datei: an Server-Endpoint zur Validierung übergeben
await uploadCsvToServer(file);
return;
}
Alpine.store('quickOrder').rows = parsedRows;
});
4. GraphQL-Batch-Mutation zum Hinzufügen mehrerer Produkte
Sobald die Zeilenliste steht, sollen alle Positionen in einem einzigen Schritt in den Warenkorb wandern, nicht als Kette einzelner addProductToCart-Aufrufe. Magento 2.4.8 bietet dafür die Mutation addProductsToCart, die eine Cart-ID sowie ein Array aus SKU und Menge pro Position entgegennimmt und in einem einzigen Request verarbeitet.
Besonders wertvoll ist das Feld user_errors in der Antwort: Es liefert für jede fehlgeschlagene Position einen eigenen Fehlercode und einen Pfad, der auf die konkrete Zeile im übermittelten Array verweist. Damit lässt sich im Frontend präzise nachvollziehen, welche Zeile betroffen ist, ohne dass der gesamte Request als Ganzes fehlschlägt.
Eine einzelne Mutation für die komplette Liste reduziert die Anzahl der Requests drastisch gegenüber einer Schleife mit einzelnen Aufrufen. Bei sehr langen Listen stößt aber auch diese Variante an praktische Grenzen, dazu mehr im Abschnitt über Performance und Chunking.
mutation AddProductsToCart($cartId: String!, $items: [CartItemInput!]!) {
addProductsToCart(cartId: $cartId, cartItems: $items) {
cart {
id
total_quantity
}
user_errors {
code
message
path
}
}
}
# Variablen-Beispiel
# {
# "cartId": "abc123",
# "items": [
# { "sku": "24-MB01", "quantity": 5 },
# { "sku": "24-MB02", "quantity": 12 }
# ]
# }
5. Lagerbestand in Echtzeit prüfen: Debounce und Stock-Abfrage
Bevor eine Zeile überhaupt in den Warenkorb geschickt wird, lohnt sich eine sofortige Rückmeldung zum Lagerbestand direkt im Formular. Dafür wird bei jeder Änderung an SKU oder Menge eine GraphQL-Abfrage gegen das products-Feld ausgelöst, gefiltert nach der eingegebenen SKU, mit stock_status und, sofern Multi-Source-Inventory aktiv ist, der verfügbaren Menge im Feld only_x_left_in_stock.
Ohne Debounce würde jede einzelne Tastatureingabe eine eigene Anfrage auslösen, das belastet unnötig sowohl Frontend als auch Server. Ein Debounce von etwa 300 bis 500 Millisekunden pro Zeile, umgesetzt über setTimeout und clearTimeout oder über den eingebauten Alpine-Modifier .debounce, sorgt dafür, dass erst nach einer kurzen Tippschreibpause tatsächlich eine Anfrage rausgeht.
Bei schnellem Wechsel zwischen Zeilen können ältere Antworten später eintreffen als neuere. Eine einfache Absicherung ist ein Request-Zähler oder ein AbortController pro Zeile, der veraltete Antworten verwirft, damit der angezeigte Status immer zum zuletzt eingegebenen Wert passt.
let debounceTimer;
function onRowInput(index) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => checkStock.call(this, index), 400);
}
async function checkStock(index) {
const row = this.rows[index];
row.status = 'prüfe...';
const query = `
query CheckStock($skus: [String!]!) {
products(filter: { sku: { in: $skus } }) {
items {
sku
stock_status
only_x_left_in_stock
}
}
}
`;
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables: { skus: [row.sku] } }),
});
const { data } = await response.json();
const found = data.products.items[0];
if (!found) {
row.status = 'SKU nicht gefunden';
} else if (found.stock_status !== 'IN_STOCK') {
row.status = 'nicht auf Lager';
} else if (found.only_x_left_in_stock && row.qty > found.only_x_left_in_stock) {
row.status = `nur ${found.only_x_left_in_stock} verfügbar`;
} else {
row.status = 'verfügbar';
}
}
6. Fehler-Feedback pro Zeile: SKU nicht gefunden und Lagerbestand-Konflikte
Für die Nutzerführung ist entscheidend, dass jede Zeile ihren eigenen, klar sichtbaren Status trägt, statt eine einzige Sammelfehlermeldung am Ende des Formulars anzuzeigen. Üblich ist ein kleines Icon direkt neben dem Mengenfeld, ergänzt um einen kurzen Text darunter, in Grün für verfügbar, in Gelb für eingeschränkt verfügbar und in Rot für einen harten Fehler.
Zwischen den beiden häufigsten Fehlertypen sollte klar unterschieden werden. Eine SKU, die keinen Treffer in der Produktabfrage liefert, ist ein harter Fehler: Die Zeile lässt sich nicht in den Warenkorb übernehmen, bevor die SKU korrigiert wurde. Eine bekannte SKU mit zu geringem Lagerbestand ist dagegen ein weicher Fehler, hier bietet sich an, die maximal verfügbare Menge direkt als klickbaren Vorschlag anzuzeigen, sodass der Kunde die Menge mit einem Klick anpassen kann, statt selbst nachzurechnen.
Oberhalb der Zeilenliste hilft eine kompakte Zusammenfassung, etwa die Anzahl der Zeilen mit offenen Fehlern. Der Submit-Button bleibt so lange deaktiviert oder zeigt eine Warnung, bis entweder alle Fehler behoben sind oder der Kunde bewusst entscheidet, nur die fehlerfreien Zeilen zu übernehmen.
7. Bestelllisten für wiederkehrende Sammelbestellungen
Viele B2B-Kunden bestellen nicht nur einmalig, sondern in regelmäßigem Rhythmus dieselbe oder eine sehr ähnliche Grundausstattung an Artikeln, etwa monatlich für den Verbrauchsmaterial-Nachschub. Eine gespeicherte Bestellliste, die sich mit einem Klick in das Quick-Order-Formular laden lässt, spart in diesem Fall deutlich mehr Zeit als jeder noch so gute CSV-Import.
Adobe Commerce B2B bringt dafür mit Requisition Lists ein vollwertiges natives Modul mit, inklusive Anbindung an Firmenkonten und Freigabe-Workflows. Diese Funktion ist allerdings ausschließlich in Adobe Commerce beziehungsweise Adobe Commerce Cloud verfügbar, nicht in Magento Open Source.
Für Open-Source-Projekte lässt sich ein schlankerer, eigener Ansatz umsetzen: eine eigene Entität, die SKU-Mengen-Paare zusammen mit einem Listennamen an die customer_id koppelt. Das Quick-Order-Formular kann eine solche gespeicherte Liste laden, die Zeilen vorbefüllen und anschließend dieselbe Echtzeit-Validierung durchlaufen lassen wie eine frisch eingetippte Liste. Der Funktionsumfang bleibt bewusst schmaler als bei Requisition Lists, reicht aber für die meisten Sammelbestellungs-Szenarien vollkommen aus.
8. Performance bei großen SKU-Listen: Chunking, Debounce und Fortschritt
Ab einer gewissen Zeilenzahl, in der Praxis oft irgendwo zwischen 100 und 300 Positionen, wird eine einzelne addProductsToCart-Mutation zu einem Risiko: Die Antwortzeit steigt, und serverseitige Timeouts oder Speichergrenzen können den kompletten Request scheitern lassen, obwohl die überwiegende Mehrheit der Zeilen problemlos verarbeitbar gewesen wäre.
Die Lösung ist Chunking: Die Gesamtliste wird in kleinere Blöcke von zum Beispiel 50 Positionen aufgeteilt, die nacheinander als eigene Mutationen an den Server geschickt werden. Jeder Block liefert sein eigenes user_errors-Ergebnis zurück, sodass Fehler weiterhin präzise einer Zeile zugeordnet bleiben, auch über mehrere Chunks hinweg.
Für die Nutzerführung gehört eine Fortschrittsanzeige dazu, etwa in Prozent oder als Zähler verarbeiteter gegenüber gesamter Positionen. Da jeder Chunk-Request asynchron mit await innerhalb einer Schleife läuft, bleibt die Oberfläche währenddessen responsiv, und der Kunde kann den Fortschritt einer großen Sammelbestellung live mitverfolgen.
async function submitRowsInChunks(rows, chunkSize = 50) {
const chunks = [];
for (let i = 0; i < rows.length; i += chunkSize) {
chunks.push(rows.slice(i, i + chunkSize));
}
let processed = 0;
for (const chunk of chunks) {
await addProductsToCart(chunk);
processed += chunk.length;
this.progress = Math.round((processed / rows.length) * 100);
}
}
9. Zusammenspiel mit Firmenkonto und Freigabe-Workflows
In Umgebungen mit aktivierter Firmenkonto-Struktur greift nach dem Absenden des Quick-Order-Formulars unter Umständen ein zusätzlicher Schritt: Ist für den jeweiligen Nutzer ein Purchase-Order-Approval-Workflow konfiguriert, landet die Bestellung zunächst als ausstehend bei einem oder mehreren Freigebenden, bevor sie tatsächlich final abgeschickt wird.
Für das Quick-Order-Formular selbst ändert sich dabei praktisch nichts: Eingabe, CSV-Import, Lagerbestand-Validierung und Batch-Mutation laufen unverändert ab. Lediglich der letzte Schritt im Checkout unterscheidet sich je nach Company-Konfiguration zwischen direkter Bestellabgabe und dem Einstellen in eine Freigabe-Warteschlange.
Wie Firmenkonten, Rollen und Freigabe-Regeln im Hyvä-Theme im Detail aufgebaut werden, ist bewusst nicht Thema dieses Artikels, das gehört in die breitere Betrachtung von B2B-Funktionen im Theme. An dieser Stelle genügt die Einordnung, dass Quick-Order als Eingabeweg unabhängig vom gewählten Freigabe-Modell funktioniert.
| Methode | Eingabeaufwand | Performance bei großen Listen | Fehlerbehandlung | Verfügbarkeit |
|---|---|---|---|---|
SKU-Zeilen manuell |
gering bis mittel, Copy-Paste möglich | gut bis ca. 50 Zeilen | pro Zeile, sofort sichtbar | immer, reine Frontend-Komponente |
CSV-Import clientseitig |
sehr gering, Datei-Upload | gut bis ca. 500 Zeilen | Sammelmeldung nach Parsing | immer, kein Server-Endpoint nötig |
CSV-Import serverseitig |
sehr gering, Datei-Upload | gut auch bei mehreren tausend Zeilen | detaillierte Rückmeldung pro Zeile | eigener REST- oder GraphQL-Endpoint nötig |
GraphQL-Batch-Mutation |
keine zusätzliche Eingabe, nur Versand | gut mit Chunking, sonst Timeout-Risiko | user_errors pro Position |
Magento 2.4.8 GraphQL-API, alle Editionen |
Requisition Lists |
gering, gespeicherte Listen wiederverwenden | vom Hersteller optimiert | nativ in Freigabe-Workflow integriert | nur Adobe Commerce B2B, nicht Open Source |
Eigene Bestellliste |
gering, gespeicherte Listen wiederverwenden | abhängig von eigener Implementierung | so gut wie selbst gebaut | Open Source und Commerce gleichermaßen |
Mironsoft
Hyvä-Theme-Entwicklung und Luma-Migration
Noch auf Luma unterwegs oder ein Hyvä-Theme, das nicht rund läuft?
Wir entwickeln Hyvä-Themes für Magento von Grund auf oder migrieren bestehende Luma-Shops sauber, mit Tailwind CSS, Alpine.js und ohne unnötiges JavaScript-Gepäck.
Luma-zu-Hyvä-Migration
Bestehenden Shop strukturiert und ohne Funktionsverlust auf Hyvä umstellen.
Custom-Theme-Entwicklung
Individuelles Hyvä-Theme nach Design-Vorgaben von Grund auf umsetzen.
Performance-Optimierung
Core Web Vitals und Ladezeiten im Hyvä-Frontend gezielt verbessern.
10. Zusammenfassung
Quick-Order im Hyvä-Theme
SKU-Listen & CSV-Import
Dynamische Alpine-Zeilen für SKU und Menge, ergänzt um clientseitigen oder serverseitigen CSV-Import für größere Sammelbestellungen.
GraphQL-Batch-Mutation
addProductsToCart nimmt mehrere Positionen in einem Request entgegen und liefert user_errors pro Zeile für gezieltes Feedback.
Echtzeit-Lagerbestand
Debounced Stock-Abfrage pro Zeile erkennt fehlende SKUs und knappen Lagerbestand, bevor die Bestellung abgeschickt wird.
Bestelllisten & Firmenkonto
Eigene Bestelllisten ersetzen in Open Source die Requisition Lists von Adobe Commerce, Purchase-Order-Freigaben greifen unverändert im Checkout.