Bestellstatus & Sendungsverfolgung im Hyvä-Konto
AI generated
Hyvä
phtml
Hyvä Theme
Bestellstatus und Sendungsverfolgung im Hyvä-Kundenkonto
Von der Tracking-Nummer bis zur Status-Timeline

Die Standard-Bestellhistorie von Hyvä zeigt nur einen groben Order-Status, aber keine Tracking-Nummer und keine Zustell-Timeline. Dieser Artikel zeigt, wie sich Shipment- und Track-Daten performant per GraphQL laden lassen, wie ein Carrier-Code-zu-Tracking-URL-Mapping als ViewModel aussieht und wie eine Alpine-Komponente daraus eine verständliche Status-Timeline baut. Ergänzt um Teillieferungen, Caching-Aspekte und die wichtigsten Edge Cases.

9 Min. Lesezeit GraphQL Alpine.js Tracking-URLs Status-Timeline

1. Was Hyvä im Kundenkonto bereits mitbringt, und wo die Lücke liegt

Hyvä liefert im Kundenkonto eine schlanke Bestellhistorie: Bestellnummer, Datum, Status-Label und einen Link zur Detailansicht. Das reicht für die reine Auflistung, sagt aber nichts darüber aus, wo sich eine Sendung gerade befindet. Wer nach dem Versand wissen will, ob das Paket schon beim Zusteller ist, muss aktuell auf die Sendungsbestätigung per E-Mail warten oder die Tracking-Nummer manuell in ein externes Portal kopieren.

Der Standard-Block Magento\Sales\Block\Order\History beziehungsweise dessen Hyvä-Pendant zeigt zwar den aggregierten Order-Status an, zum Beispiel processing oder complete, verknüpft diesen aber nicht mit den einzelnen Shipment-Datensätzen. Genau diese Verknüpfung, Tracking-Nummer, Carrier und ein visuelles Statusmodell, ist der Teil, den dieser Artikel ergänzt.

Abgegrenzt wird das bewusst von Themen wie Reorder-Button, Dashboard-Kacheln oder der Order-Success-Page, die bereits an anderer Stelle behandelt werden. Hier geht es ausschließlich um die Detailansicht einer bereits aufgegebenen Bestellung und die Frage, wie weit sie tatsächlich unterwegs ist.

2. Magento-Datenmodell: Shipment- und Track-Entity im Überblick

Sendungsverfolgung basiert in Magento auf zwei Entitäten. Eine Order kann mehrere Shipment-Datensätze besitzen, etwa bei Teillieferungen, und jedes Shipment kann wiederum mehrere Track-Einträge referenzieren, weil ein Paket theoretisch über mehrere Carrier oder Pakete verteilt versendet werden kann. Das Track-Objekt trägt die eigentliche Tracking-Nummer, den Carrier-Code und optional eine Titel-Bezeichnung.

Relevant für das Frontend sind vor allem ShipmentTrackInterface::getCarrierCode(), getTrackNumber() und getTitle(). Der Carrier-Code ist dabei keine freie URL, sondern ein interner Bezeichner wie dhl, ups, fedex oder custom bei manuell eingetragenen Versanddienstleistern. Diese Rohdaten müssen im Frontend erst auf eine anklickbare Tracking-URL gemappt werden, dazu mehr im ViewModel-Abschnitt weiter unten.

Wichtig ist außerdem: Ein Shipment entsteht erst, wenn der Händler die Bestellung tatsächlich verpackt und im Backend als versendet markiert hat. Bis dahin existiert schlicht kein Track-Datensatz, und die Timeline im Frontend muss diesen Zustand als regulären, nicht als fehlerhaften Fall behandeln.

3. GraphQL-Query für Bestellhistorie inklusive Sendungen und Tracking

Die Standard-Query customer.orders liefert im shipments-Feld bereits die verschachtelten Tracking-Daten, sie wird in Hyvä-Standardthemes jedoch meist nur rudimentär abgefragt. Für die Detailansicht lohnt sich eine dedizierte, schlanke Query, die gezielt nur die Felder lädt, die Timeline und Tracking-Links tatsächlich benötigen.

Pagination erfolgt über pageSize und currentPage auf Order-Ebene, nicht auf Shipment-Ebene, da eine einzelne Bestellung in der Praxis selten mehr als eine Handvoll Sendungen hat. Wichtig ist, die Query pro Seite unterschiedlich zu skopieren: Die Listenansicht braucht keine Tracking-Details, die Detailansicht schon.


query CustomerOrderTracking {
  customer {
    orders(pageSize: 5, currentPage: 1) {
      total_count
      page_info {
        current_page
        page_size
        total_pages
      }
      items {
        number
        order_date
        status
        shipments {
          number
          tracking {
            carrier
            title
            number
          }
        }
      }
    }
  }
}

4. Performance: N+1 vermeiden und Query-Kosten im Griff behalten

Wer in der Bestellhistorie für jede Zeile eine eigene Detail-Query gegen customer.orders feuert, um Tracking-Informationen nachzuladen, erzeugt ein klassisches N+1-Problem: Bei zwanzig Bestellungen auf einer Seite entstehen zwanzig zusätzliche Round-Trips zum GraphQL-Endpunkt. Das lässt sich vermeiden, indem die Tracking-Daten von Anfang an in derselben Query wie die Bestellliste mitgeladen werden, allerdings mit reduzierter Feldauswahl.

Ein sinnvoller Kompromiss ist zweistufiges Laden: Die Listenansicht fragt nur number, status und ein leichtgewichtiges shipments { tracking { carrier } } ab, um pro Zeile ein kleines Versand-Icon zu zeigen. Die vollständige Tracking-Nummer und die Statuszeitpunkte werden erst nachgeladen, wenn der Nutzer eine Zeile tatsächlich aufklappt, ausgelöst per Alpine x-init mit einem gezielten Fetch statt einer erneuten vollen Bestellhistorie-Query.

Auf Server-Seite hilft zusätzlich, den Resolver für shipments zu prüfen: Standardmäßig lädt Magento Shipments über ein Collection-Objekt, das bei ungünstiger Konfiguration pro Order einzeln nachlädt. Ein Blick in den Query-Log via bin/magento dev:query-log:enable zeigt schnell, ob pro Zeile tatsächlich nur eine oder mehrere SQL-Abfragen anfallen.


query OrderShipmentDetail($orderNumber: String!) {
  customer {
    orders(filter: { number: { eq: $orderNumber } }) {
      items {
        number
        shipments {
          number
          tracking {
            carrier
            title
            number
          }
        }
      }
    }
  }
}

5. Status-Timeline als Alpine-Komponente umsetzen

Die Timeline bildet drei bis vier Schritte ab: aufgegeben, in Bearbeitung, versendet, zugestellt. Magento liefert dafür keinen direkten Timeline-Status, sondern nur den aggregierten Order-Status als String. Das Mapping muss also selbst gebaut werden, idealerweise in einer kleinen Alpine-Komponente, die den Order-Status und die Anzahl vorhandener Shipments als Eingabe nimmt.

Der aggregierte Status complete bedeutet nicht zwingend delivered, denn Magento setzt diesen Status bereits, sobald alle Items fakturiert und versendet sind, unabhängig vom tatsächlichen Zustellstatus beim Carrier. Wer eine echte Zustellbestätigung anzeigen will, braucht entweder ein Carrier-Tracking-API oder ein manuell gepflegtes Statusfeld, denn Magento selbst hat keinen automatischen Zustellstatus.


<div x-data="orderStatusTimeline({
        orderStatus: '<?= $block->escapeJs($order->getStatus()) ?>',
        shipmentCount: <?= (int) count($order->getShipmentsCollection()) ?>
    })" class="mt-6">
    <ol class="flex items-center w-full">
        <template x-for="(step, index) in steps" :key="step.key">
            <li class="flex-1 relative">
                <div class="flex items-center">
                    <div :class="step.done ? 'bg-primary text-white' : 'bg-gray-200 text-gray-500'"
                         class="w-8 h-8 rounded-full flex items-center justify-center text-sm">
                        <span x-text="index + 1"></span>
                    </div>
                    <div x-show="index < steps.length - 1" class="flex-1 h-0.5"
                         :class="step.done ? 'bg-primary' : 'bg-gray-200'"></div>
                </div>
                <p class="mt-2 text-sm" :class="step.done ? 'font-semibold text-gray-900' : 'text-gray-400'"
                   x-text="step.label"></p>
            </li>
        </template>
    </ol>
</div>

<script>
function orderStatusTimeline({ orderStatus, shipmentCount }) {
    return {
        steps: [],
        init() {
            const map = {
                pending: 0,
                processing: shipmentCount > 0 ? 2 : 1,
                complete: 2,
                closed: 3,
                canceled: -1
            };
            const reached = map[orderStatus] ?? 0;
            const labels = ['Aufgegeben', 'In Bearbeitung', 'Versendet', 'Zugestellt'];
            this.steps = labels.map((label, index) => ({
                key: label,
                label,
                done: index <= reached
            }));
        }
    };
}
</script>
<?php $hyvaCsp->registerInlineScript(); ?>

6. Carrier-Code zu Tracking-URL: das ViewModel-Pattern

Da der Carrier-Code allein keine klickbare URL ergibt, braucht es eine kleine Übersetzungsschicht. Ein ViewModel, das ArgumentInterface implementiert, ist dafür der richtige Ort, weil es sich sauber per Layout-XML in das Template injizieren lässt, ohne einen eigenen Block anzulegen.

Das Mapping selbst sollte konfigurierbar bleiben, damit neue Carrier ohne Code-Deploy ergänzt werden können. In der Praxis hat sich eine Kombination aus einem Basis-Array im ViewModel und optionalem Override über system.xml bewährt, etwa für Fälle, in denen ein Kunde kurzfristig einen zusätzlichen Regionalversender wie Hermes oder Evri einsetzt.

Für unbekannte Carrier-Codes muss die Methode einen sinnvollen Fallback liefern, etwa eine Suche nach Tracking-Nummer und Carrier-Namen, statt schlicht keinen Link anzuzeigen. Das verhindert, dass Kunden bei selten genutzten Versanddienstleistern komplett ohne Anlaufstelle dastehen.


<?php

declare(strict_types=1);

namespace Mironsoft\OrderTracking\ViewModel;

use Magento\Framework\View\Element\Block\ArgumentInterface;

/**
 * Mappt Carrier-Codes auf klickbare Tracking-URLs für die Bestellhistorie.
 */
class CarrierTrackingUrl implements ArgumentInterface
{
    /**
     * Basis-Mapping von Magento-Carrier-Code zu URL-Muster mit Platzhalter für die Tracking-Nummer.
     *
     * @var array<string, string>
     */
    private const CARRIER_URL_MAP = [
        'dhl' => 'https://www.dhl.de/de/privatkunden/dhl-sendungsverfolgung.html?piececode=%s',
        'ups' => 'https://www.ups.com/track?tracknum=%s',
        'dpd' => 'https://tracking.dpd.de/status/de_DE/parcel/%s',
        'hermes' => 'https://www.myhermes.de/empfangen/sendungsverfolgung/sendungsinformation/#%s',
        'fedex' => 'https://www.fedex.com/fedextrack/?trknbr=%s',
    ];

    /**
     * Baut die Tracking-URL für einen gegebenen Carrier-Code und eine Tracking-Nummer.
     * Liefert einen Such-Fallback, wenn der Carrier-Code unbekannt ist.
     *
     * @param string $carrierCode
     * @param string $trackNumber
     * @return string
     */
    public function getTrackingUrl(string $carrierCode, string $trackNumber): string
    {
        $pattern = self::CARRIER_URL_MAP[strtolower($carrierCode)] ?? null;

        if ($pattern === null) {
            return sprintf(
                'https://www.google.com/search?q=%s',
                urlencode($carrierCode . ' tracking ' . $trackNumber)
            );
        }

        return sprintf($pattern, urlencode($trackNumber));
    }
}

7. Private Content und Caching im Kundenkonto

Das gesamte Kundenkonto läuft in Magento über den private content-Mechanismus und wird grundsätzlich nicht durch den Full-Page-Cache ausgeliefert. Das bedeutet, die Bestellhistorie inklusive Timeline wird bei jedem Aufruf serverseitig frisch gerendert, was performancetechnisch unkritisch ist, aber eben auch heißt, dass eine clientseitige Zwischenspeicherung der GraphQL-Antwort sinnvoll sein kann.

Für die Timeline selbst ergibt sich daraus ein Vorteil: Es gibt kein Problem mit veraltetem FPC-Cache, der einen alten Status anzeigt, wie es etwa bei Produktseiten mit Preisen der Fall sein kann. Die Daten kommen bei jedem Request live aus der Datenbank. Wer dennoch unnötige Requests vermeiden will, sollte auf Alpine-Ebene ein einfaches In-Memory-Caching pro Seiten-Lebensdauer implementieren, etwa über Alpine.store, statt bei jedem Tab-Wechsel neu zu laden.

Wichtig ist außerdem, den GraphQL-Endpunkt selbst nicht versehentlich über Varnish cachen zu lassen. Bei korrekt konfiguriertem X-Magento-Cache-Debug-Header lässt sich schnell prüfen, dass Bestellhistorie-Requests tatsächlich als MISS beziehungsweise gar nicht durch den FPC laufen.

8. Teillieferungen und Multi-Shipment-UI

Sobald eine Bestellung in mehreren Paketen verschickt wird, reicht eine einzelne Timeline nicht mehr aus. Sinnvoll ist eine Gruppierung pro Shipment, jeweils mit eigener Tracking-Nummer, eigenem Carrier und eigenem Status, während der übergeordnete Order-Status weiterhin als Zusammenfassung oben stehen bleibt.

In der Praxis bewährt sich ein Akkordeon-Muster: Standardmäßig ist nur das erste beziehungsweise letzte Shipment aufgeklappt, alle weiteren lassen sich per Klick einblenden. Das hält die Detailansicht auch bei Bestellungen mit fünf oder sechs Einzelpaketen übersichtlich, ohne dass der Nutzer durch eine lange, unstrukturierte Liste scrollen muss.


<?php /** @var \Magento\Sales\Model\Order $order */ ?>
<div class="space-y-4">
    <?php foreach ($order->getShipmentsCollection() as $index => $shipment): ?>
        <div x-data="{ open: <?= $index === 0 ? 'true' : 'false' ?> }" class="border rounded-lg">
            <button type="button" @click="open = !open" class="w-full flex items-center justify-between p-4">
                <span class="font-semibold">
                    <?= $block->escapeHtml(__('Sendung %1', $shipment->getIncrementId())) ?>
                </span>
                <span x-text="open ? '−' : '+'"></span>
            </button>
            <div x-show="open" x-collapse class="p-4 border-t">
                <?php foreach ($shipment->getTracksCollection() as $track): ?>
                    <p class="text-sm">
                        <?= $block->escapeHtml($track->getCarrierCode()) ?>:
                        <?= $block->escapeHtml($track->getTrackNumber()) ?>
                    </p>
                <?php endforeach; ?>
            </div>
        </div>
    <?php endforeach; ?>
</div>

9. Edge Cases: fehlende Tracking-Nummer, Carrier ohne Code, verzögerter Status

Nicht jeder Versand hat eine Tracking-Nummer. Bei Selbstabholung, manchen Speditionslieferungen oder Versand durch Drittanbieter ohne Tracking-Integration bleibt das Feld schlicht leer. Die Timeline muss diesen Fall explizit abfangen und statt eines leeren oder kaputten Links einen Klartext-Hinweis zeigen, etwa dass für diese Sendung keine Sendungsverfolgung verfügbar ist.

Ähnlich verhält es sich, wenn zwar eine Tracking-Nummer, aber kein bekannter Carrier-Code hinterlegt ist, etwa bei manuell im Backend eingetragenen Versanddienstleistern mit dem Code custom. Hier sollte das ViewModel aus dem vorherigen Abschnitt den beschriebenen Fallback greifen lassen, statt die rohe Tracking-Nummer unverlinkt und ohne Kontext anzuzeigen.

Ein dritter Fall betrifft Statusverzögerungen: Wenn ein Carrier tagelang keinen neuen Scan meldet, wirkt eine reine Anzeige als versendet irreführend. Ohne aktive Anbindung an eine Tracking-API lässt sich das im eigenen Shop nur über ein Zeitstempel-Hinweisfeld abfedern, etwa versendet am mit Angabe der letzten Aktualisierung, damit Kunden nicht den Eindruck bekommen, ihr Paket sei verloren gegangen.

Magento-Order-Status Shipment vorhanden? Timeline-Schritt im Frontend
pending Nein Aufgegeben, Zahlungseingang ausstehend
processing Nein In Bearbeitung, Kommissionierung läuft
processing Ja, mindestens ein Shipment Versendet, Tracking-Nummer verfügbar
complete Ja, alle Items fakturiert und versendet Versendet oder zugestellt, ohne Carrier-API nur Annahme
closed Ja, ggf. mit Retoure Abgeschlossen, inklusive Rückerstattung
canceled Nein Storniert, Timeline wird ausgeblendet

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

Bestellstatus & Tracking

Datenmodell

Order, Shipment und Track-Entity liefern Carrier-Code und Tracking-Nummer, aber keinen fertigen Timeline-Status.

GraphQL

Eine schlanke, zweistufige Query trennt Listenansicht und Detailansicht, um unnötige Feldauswahl zu vermeiden.

Timeline-UI

Eine Alpine-Komponente mappt den aggregierten Order-Status und die Shipment-Anzahl auf drei bis vier Timeline-Schritte.

Edge Cases

Fehlende Tracking-Nummer, unbekannter Carrier-Code und verzögerter Status brauchen explizite Fallbacks statt leerer Links.

11. FAQ: Bestellstatus & Tracking

1Liefert Magento GraphQL von Haus aus eine fertige Status-Timeline?
Nein, die Query liefert nur den aggregierten Order-Status und die rohen Shipment- und Track-Daten, das Mapping auf Timeline-Schritte muss im Frontend selbst erfolgen.
2Wie unterscheide ich versendet von zugestellt, wenn Magento keinen Zustellstatus kennt?
Magento selbst hat kein natives Zustellstatusfeld, echte Zustellbestätigungen erfordern eine Anbindung an die Tracking-API des jeweiligen Carriers oder ein manuell gepflegtes Statusfeld.
3Welches Feld enthält den Carrier-Code in GraphQL?
Im Feld shipments.tracking liefert carrier den internen Carrier-Code, während title die vom Händler hinterlegte Anzeigebezeichnung ist.
4Wie vermeide ich N+1-Anfragen bei vielen Bestellungen?
Tracking-Daten mit reduzierter Feldauswahl direkt in der Listen-Query mitladen und Details erst per Lazy Load nachladen, wenn eine Zeile aufgeklappt wird.
5Wird die Bestellhistorie durch den Full-Page-Cache ausgeliefert?
Nein, das Kundenkonto läuft über private content und wird bei jedem Aufruf serverseitig neu gerendert, ein FPC-Caching-Problem mit veraltetem Status entfällt dadurch.
6Wie gehe ich mit Sendungen ohne Tracking-Nummer um?
Die Timeline sollte diesen Zustand explizit erkennen und einen Klartext-Hinweis statt eines leeren oder kaputten Links anzeigen.
7Wo lege ich das Mapping von Carrier-Code zu Tracking-URL ab?
In einem ViewModel, das ArgumentInterface implementiert und per Layout-XML ins Template injiziert wird, mit konfigurierbarem Array und Fallback für unbekannte Codes.
8Wie stelle ich Teillieferungen mit mehreren Shipments dar?
Gruppierung pro Shipment mit eigener Tracking-Nummer und eigenem Status, kombiniert mit einem Akkordeon-Muster, damit die Ansicht übersichtlich bleibt.
9Muss ich für jeden Carrier eine eigene Codezeile pflegen?
Ein Basis-Array im ViewModel deckt die gängigen Carrier ab, ergänzt um eine optionale system.xml-Konfiguration für kurzfristig neue Regionalversender.
10Wie erkenne ich einen verzögerten Status ohne Tracking-API-Anbindung?
Über ein Zeitstempel-Hinweisfeld, das anzeigt, wann der letzte bekannte Status gesetzt wurde, damit Kunden den Unterschied zwischen normal und auffällig lange unverändert einschätzen können.