diff --git a/app/Http/Controllers/Admin/Inventory/OrderFulfillmentController.php b/app/Http/Controllers/Admin/Inventory/OrderFulfillmentController.php index 1c0e6e2..a9077ed 100644 --- a/app/Http/Controllers/Admin/Inventory/OrderFulfillmentController.php +++ b/app/Http/Controllers/Admin/Inventory/OrderFulfillmentController.php @@ -31,34 +31,46 @@ class OrderFulfillmentController extends Controller $withRelations = ['shopping_user', 'shopping_payment_last', 'shipping_country.country', 'shopping_order_items.product.setItems']; - // Offene Abwicklung (Status offen/in Bearbeitung) — Basis für Liste und Kachel-Zähler + // Offene Abwicklung (Status offen/in Bearbeitung) — Basis für Liste und Kachel-Zähler. + // Bewusst OHNE Limit (Review 20.07.2026): das Cap galt vorher schon für die Zähler + // und Filter-Listen, wodurch bei > 500 offenen Bestellungen ausgerechnet die + // ältesten versandfertigen unsichtbar wurden — und der Hinweis-Banner verglich + // den Post-Filter-Count, erschien also nie. Gekappt wird jetzt nur die Anzeige. $open = ShoppingOrder::query() ->with($withRelations) ->whereIn('shipped', [0, 1]) ->orderByDesc('id') - ->when($limit !== null, fn ($query) => $query->limit($limit)) ->get(); + $pick = $open->where('shipped', 0)->filter->isReadyToShip()->values(); + $ready = $open->where('shipped', 1)->filter->isReadyToShip()->values(); + $counts = [ - 'today' => ShoppingOrder::query()->whereDate('created_at', today())->count(), - 'pick' => $open->where('shipped', 0)->filter->isReadyToShip()->count(), - 'ready' => $open->where('shipped', 1)->filter->isReadyToShip()->count(), - 'shipped_today' => ShoppingOrder::query()->whereDate('shipped_at', today())->count(), + 'today' => ShoppingOrder::query()->whereBetween('created_at', [today(), today()->endOfDay()])->count(), + 'pick' => $pick->count(), + 'ready' => $ready->count(), + 'shipped_today' => ShoppingOrder::query()->whereBetween('shipped_at', [today(), today()->endOfDay()])->count(), ]; $orders = match ($filter) { - 'today' => ShoppingOrder::query()->with($withRelations)->whereDate('created_at', today())->orderByDesc('id')->get(), - 'pick' => $open->where('shipped', 0)->filter->isReadyToShip()->values(), - 'ready' => $open->where('shipped', 1)->filter->isReadyToShip()->values(), - 'shipped_today' => ShoppingOrder::query()->with($withRelations)->whereDate('shipped_at', today())->orderByDesc('id')->get(), + 'today' => ShoppingOrder::query()->with($withRelations)->whereBetween('created_at', [today(), today()->endOfDay()])->orderByDesc('id')->get(), + 'pick' => $pick, + 'ready' => $ready, + 'shipped_today' => ShoppingOrder::query()->with($withRelations)->whereBetween('shipped_at', [today(), today()->endOfDay()])->orderByDesc('id')->get(), default => $open, }; + $listTotal = $orders->count(); + if ($limit !== null) { + $orders = $orders->take($limit)->values(); + } + return view('admin.inventory.orders.index', [ 'orders' => $orders, 'counts' => $counts, 'filter' => $filter, 'listLimit' => $limit, + 'listTotal' => $listTotal, ]); } diff --git a/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md b/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md index 1ca8e67..912ba96 100644 --- a/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md +++ b/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md @@ -279,6 +279,7 @@ Der Warenkorb kommt also **per Knopfdruck** (Fulfillment-Übergabe), nicht autom | 20.07.2026 | **Review-Fix: AP-32-Brücke auf Base64 umgestellt (Connector 1.5.0)** | Code-Review vor Live-Release, drei Befunde in einem Zug behoben: **(1) Die Brücke hätte in Produktion nicht funktioniert** — der Connector 1.4.0 übertrug die rohe `label_url` aus `wp-content/uploads/woocommerce_dhl_label/`, den das DHL-Plugin per „deny from all" schützt → der Laravel-Download lief immer auf 403, das Label wurde nie gespeichert, und der View-Fallback verlinkte dieselbe 403-URL. **(2) SSRF:** der Server rief eine Client-URL ohne Host-/Schema-Prüfung ab. **(3) Timeout-Mismatch:** Download bis 20 s im Request-Pfad vs. 5 s WordPress-Default → Übertragung wäre shopseitig dauerhaft „fehlgeschlagen" (Exists-400 bei jedem Retry). Jetzt: Connector 1.5.0 liest die PDF vom Shop-Dateisystem (`label_path`, realpath-Pinning auf wp-uploads, `%PDF`-Check) und sendet sie **Base64** (`wp_label_pdf`); `storeWpLabel()` dekodiert + validiert (max. 10 MB, `%PDF`), **kein `Http::get` mehr**; `wp_label_url` bleibt reines Herkunfts-Metadatum, View-Fallback-Links entfernt; Connector-Timeout 30 s. Doku `ap-32-brueckenloesung-connector-update.md` + Hinweise überarbeitet (Deployment: Label kommt erst mit Connector 1.5.0 im Shop an). | `WpLabelTransferTest` auf Base64 umgestellt (10 grün; neu: ungültiges Base64/Nicht-PDF verworfen, URL-only = Altconnector blockiert nicht) | | 20.07.2026 | **Review-Fix: API-Protokoll — falsch-grüne Einträge behoben** | `ApiOrderLog::recordTransfer()` löste die SKU **erneut** auf und markierte nur unbekannte Artikel als Fehler — eine Zeile mit gültiger SKU, aber **fehlender Menge/Preis** wurde von `prepareOrder()` verworfen (Position fehlt in der Bestellung!) und trotzdem grün protokolliert. Jetzt liefert `prepareOrder()` je Zeile `included`/`product_id`/`product_name` mit und das Protokoll konsumiert nur noch diese Wahrheit — `failed` = „Position nicht übernommen" (deckt SKU unbekannt UND Parameter fehlen ab); nebenbei entfallen die N+1-Produkt-Queries je Position. | `ApiOrderLogTest` +1: bekannte SKU ohne Menge ⇒ rot markiert, Position fehlt in Bestellung (9 grün) | | 20.07.2026 | **Review-Fix: PDF-Altdaten-Robustheit + Dedup** | Zwei 500er-Szenarien behoben: **(1)** `document('combined')` (Kombi-Druck im Board) prüfte keine Datei-Existenz — bei fehlender Alt-PDF flog eine `PDFMerger`-Exception statt 404/Regeneration. **(2)** Der Sammel-Druck regenerierte nur bei fehlendem **Lieferschein**; fehlte nur die Rechnungs-PDF, platzte der gesamte Batch (bis 100 Bestellungen). Kern: neue zentrale Methode **`InvoiceRepository::ensureFilesOnDisk()`** (prüft/regeneriert Rechnung UND Lieferschein mit bestehender Nummer) — ersetzt den dreifach kopierten Altdaten-Check (`MailLogistic` 2x, `batchDocuments`) und wird jetzt auch in `document()` genutzt. Dazu die vierfach duplizierte Merge-/Response-Sequenz im `OrderFulfillmentController` zu `mergedPdf()` + `pdfResponse()` zusammengezogen. | `OrderFulfillmentTest` +2: Kombi-Download und Sammel-Druck regenerieren fehlende Rechnungs-PDF (16 grün); `LogisticMailTest` unverändert grün | +| 20.07.2026 | **Review-Fix: Abwicklungs-Board (Rechte-Links, 500er-Cap, Kleinigkeiten)** | **(1) Copyreader-Links:** Bestellnummern in Abwicklung + API-Bestellungen verlinkten auf `admin_sales_detail` (admin ≥ 7) — für Versand-Mitarbeiter (copyreader, Kundenentscheid AP-33 Punkt 4) ein stiller Redirect nach `/home`; Link jetzt nur für Admins, sonst reiner Text. **(2) 500er-Cap:** Kacheln „Kommissionieren/Versandfertig" und deren Filter-Listen rechneten auf den neuesten 500 offenen Bestellungen — bei Rückstau wären ausgerechnet die **ältesten versandfertigen unsichtbar** gewesen, und der Hinweis-Banner verglich den Post-Filter-Count (erschien nie). Jetzt: Zähler + Filter über **alle** offenen Bestellungen, gekappt wird nur die Anzeige („neueste :n von :total" + „Alle anzeigen"). `whereDate()` → `whereBetween` (indexfähig, Vorbereitung Index-AP). **(3)** Packlisten-Toggle nutzt `data-order-id` statt Link-Text-Scraping; Gewicht über `formatNumber()`. | `OrderFulfillmentTest` +2 (copyreader ohne Link / admin mit Link; 18 grün) | | _offen_ | AP-32 | Versand im System: DHL-Etiketten per API — **neu aufgenommen + Ansatz dokumentiert (§4, 15.07.2026)**; Klärungspunkte (DHL-Konto, Versandprodukte, Druck-Setup) für nächste Anforderungsrunde notiert. **Zwischenstand 17.07.2026: Brückenlösung aktiv** (Label aus dem Shop, s. Protokollzeile) — deckt den Versand-Flow ab, bis die DHL-Klärungen da sind | – | | _offen_ | AP-14/15/16/17 | Audit-Trail, Blockrechte, 2FA, WaWi-Einstellungen | – | diff --git a/resources/views/admin/inventory/api-orders/index.blade.php b/resources/views/admin/inventory/api-orders/index.blade.php index bc9d750..4ff53ae 100644 --- a/resources/views/admin/inventory/api-orders/index.blade.php +++ b/resources/views/admin/inventory/api-orders/index.blade.php @@ -83,8 +83,12 @@ {{ $log->wp_order_number ?? '—' }} {{ $customer !== '' ? $customer : '—' }} - @if ($log->shopping_order_id) + {{-- Verkäufe-Detail verlangt admin ≥ 7 — Versand-Mitarbeiter (copyreader) + bekämen sonst einen stillen Redirect (Review 20.07.2026) --}} + @if ($log->shopping_order_id && auth()->user()->isAdmin()) #{{ $log->shopping_order_id }} + @elseif ($log->shopping_order_id) + #{{ $log->shopping_order_id }} @else — @endif diff --git a/resources/views/admin/inventory/orders/index.blade.php b/resources/views/admin/inventory/orders/index.blade.php index 008a4fd..b9d5401 100644 --- a/resources/views/admin/inventory/orders/index.blade.php +++ b/resources/views/admin/inventory/orders/index.blade.php @@ -132,7 +132,7 @@ $readyToShip = $order->isReadyToShip(); $itemCount = $order->shopping_order_items->sum('qty'); @endphp - + @if ($order->isApiOrder()) @@ -141,7 +141,13 @@ @endif - #{{ $order->id }} + {{-- Verkäufe-Detail verlangt admin ≥ 7 — Versand-Mitarbeiter (copyreader) + bekämen sonst einen stillen Redirect (Review 20.07.2026) --}} + @if (auth()->user()->isAdmin()) + #{{ $order->id }} + @else + #{{ $order->id }} + @endif @if ($order->isApiOrder()) API @elseif ($order->user_shop_id) @@ -243,7 +249,7 @@ {{ __('Packliste') }} @if ($order->weight) - · {{ __('Gewicht') }}: {{ number_format($order->weight / 1000, 2, ',', '.') }} kg + · {{ __('Gewicht') }}: {{ formatNumber($order->weight / 1000) }} kg @endif