gruene-seele/resources/views/admin/inventory/api-orders/index.blade.php
Kevin Adametz 50243a07b4 Warenwirtschaft: Review-Fix — Indizes + schlanke API-Bestellungen-Seite
Indizes auf shopping_orders.shipped/shipped_at/created_at (Board-Filter,
Tages-Kacheln) und products.wp_number (SKU-Auflösung). Die
API-Bestellungen-Seite lud das komplette JSON-payload für alle 500 Zeilen
und serialisierte es in Inline-Attribute; jetzt persistierte
positions_count, Liste ohne payload, Inhalt on-demand über show-Endpoint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 13:02:47 +00:00

236 lines
13 KiB
PHP

@extends('layouts.layout-2')
@section('content')
@include('admin.inventory.partials.wawi-ui')
<div class="wawi-page">
<div class="wawi-page-head">
<div>
<h1 class="wawi-page-head__title">{{ __('API-Bestellungen') }}</h1>
<p class="wawi-page-head__subtitle">{{ __('Über die Partnercenter-API übertragene Shop-Bestellungen — Positionen ohne Produktzuordnung sind markiert') }}</p>
</div>
</div>
@if ($duplicateSkuProducts->isNotEmpty())
<div class="wawi-card mb-3" style="border-left: 4px solid #d63031;">
<div class="p-3">
<strong class="text-danger"><span class="fas fa-exclamation-triangle mr-1"></span>{{ __('Doppelte WP-Nummern (SKU) bei aktiven Produkten') }}</strong>
<p class="text-muted small mb-2">{{ __('Bei doppelten WP-Nummern ist nicht eindeutig, welches Produkt einer API-Position zugeordnet wird — bitte bereinigen.') }}</p>
<ul class="mb-0 small">
@foreach ($duplicateSkuProducts->groupBy('wp_number') as $wpNumber => $products)
<li>
<strong>{{ $wpNumber }}</strong>:
@foreach ($products as $product)
<a href="{{ route('admin_product_edit', $product->id) }}">{{ $product->name }}</a>@if (! $loop->last), @endif
@endforeach
</li>
@endforeach
</ul>
</div>
</div>
@endif
@if ($missingSkuProducts->isNotEmpty())
<div class="wawi-card mb-3">
<div class="p-3">
<details>
<summary class="text-muted">
{{ __(':n aktive Produkte ohne WP-Nummer (können per API nicht zugeordnet werden)', ['n' => $missingSkuProducts->count()]) }}
</summary>
<ul class="mb-0 mt-2 small">
@foreach ($missingSkuProducts as $product)
<li><a href="{{ route('admin_product_edit', $product->id) }}">{{ $product->name }}</a></li>
@endforeach
</ul>
</details>
</div>
</div>
@endif
<div class="wawi-card">
<div class="wawi-toolbar">
<label class="mb-0 small d-flex align-items-center" style="gap:.4rem;">
<input type="checkbox" id="api-log-only-errors">
{{ __('Nur Einträge mit Fehlern') }}
@if ($errorCount > 0)
<span class="wawi-pill wawi-pill--danger">{{ $errorCount }}</span>
@endif
</label>
</div>
<div class="table-responsive">
<table class="table wawi-table" id="api-log-table">
<thead>
<tr>
<th>{{ __('Eingegangen') }}</th>
<th>{{ __('WP-Bestellnr.') }}</th>
<th>{{ __('Kunde') }}</th>
<th>{{ __('Bestellung') }}</th>
<th class="text-right">{{ __('Positionen') }}</th>
<th>{{ __('Status') }}</th>
<th></th>
</tr>
</thead>
<tbody>
@forelse ($logs as $log)
@php
$customer = $log->shoppingUser
? trim(($log->shoppingUser->billing_firstname ?? '').' '.($log->shoppingUser->billing_lastname ?? ''))
: '';
@endphp
<tr class="api-log-row" data-errors="{{ $log->has_errors ? 1 : 0 }}">
<td>{{ $log->created_at->format('d.m.Y H:i') }}</td>
<td>{{ $log->wp_order_number ?? '—' }}</td>
<td>{{ $customer !== '' ? $customer : '—' }}</td>
<td>
{{-- 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())
<a href="{{ route('admin_sales_detail', $log->shopping_order_id) }}">#{{ $log->shopping_order_id }}</a>
@elseif ($log->shopping_order_id)
<span class="font-weight-bold">#{{ $log->shopping_order_id }}</span>
@else
<span class="text-muted">—</span>
@endif
</td>
<td class="text-right">{{ $log->positions_count ?: '—' }}</td>
<td>
@if ($log->has_errors)
<span class="wawi-pill wawi-pill--danger">{{ __('Fehler') }}</span>
@if ($log->error_summary)
<div class="small text-danger">{{ $log->error_summary }}</div>
@endif
@else
<span class="wawi-pill wawi-pill--ok">{{ __('OK') }}</span>
@endif
</td>
<td class="text-right">
<button type="button" class="btn btn-sm btn-outline-secondary js-api-log-view"
data-url="{{ route('admin.inventory.api-orders.show', $log->id) }}"
data-meta="{{ json_encode([
'received' => $log->created_at->format('d.m.Y H:i'),
'wp_order_number' => $log->wp_order_number,
'customer' => $customer,
'summary' => $log->error_summary,
]) }}">
<span class="far fa-eye mr-1"></span>{{ __('Ansehen') }}
</button>
</td>
</tr>
@empty
<tr>
<td colspan="7">
<div class="wawi-empty">
<div><span class="fas fa-cloud-download-alt"></span></div>
{{ __('Noch keine Bestell-Übertragungen eingegangen.') }}
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if (($listLimit ?? null) !== null && ($listTotal ?? 0) > $listLimit)
<p class="text-muted small px-3 pb-2 mb-0">
{{ __('Es werden die neuesten :n von :total Übertragungen angezeigt.', ['n' => $listLimit, 'total' => $listTotal]) }}
<a href="{{ route('admin.inventory.api-orders.index', ['all' => 1]) }}">{{ __('Alle anzeigen') }}</a>
</p>
@endif
</div>
<div class="modal fade" id="api-log-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ __('Inhalt der Bestell-Übertragung') }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<p class="small text-muted mb-2" id="api-log-modal-meta"></p>
<p class="small text-danger mb-2 d-none" id="api-log-modal-summary"></p>
<div class="table-responsive d-none" id="api-log-modal-lines-wrap">
<table class="table table-sm wawi-table mb-0">
<thead>
<tr>
<th>{{ __('WP-Nr. (SKU)') }}</th>
<th>{{ __('Produkt im Partnercenter') }}</th>
<th class="text-right">{{ __('Menge') }}</th>
<th class="text-right">{{ __('Preis (WP)') }}</th>
<th>{{ __('Meldung') }}</th>
</tr>
</thead>
<tbody id="api-log-modal-lines"></tbody>
</table>
</div>
<pre class="small bg-light p-2 d-none" id="api-log-modal-raw" style="max-height:300px;overflow:auto;"></pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{ __('Schließen') }}</button>
</div>
</div>
</div>
</div>
</div>
<script>
$(function () {
$('#api-log-only-errors').on('change', function () {
var only = $(this).is(':checked');
$('#api-log-table tbody tr.api-log-row').each(function () {
$(this).toggle(!only || $(this).data('errors') === 1);
});
});
function messageLabel(message) {
if (message === 'article not found') {
return '{{ __('Produkt nicht zugeordnet — WP-Nummer im Partnercenter unbekannt') }}';
}
if (message === 'article parameter is missing') {
return '{{ __('Position unvollständig (Artikel/Menge/Preis fehlt)') }}';
}
if (message.indexOf('different price:') === 0) {
var cents = parseInt(message.replace('different price:', '').trim(), 10);
var euro = isNaN(cents) ? '?' : (cents / 100).toFixed(2).replace('.', ',');
return '{{ __('Preis weicht ab — Preis im Partnercenter:') }} ' + euro + ' €';
}
return message;
}
function renderLines(lines, raw) {
var $body = $('#api-log-modal-lines').empty();
$('#api-log-modal-lines-wrap').toggleClass('d-none', lines.length === 0);
$.each(lines, function (_, line) {
var $tr = $('<tr>').toggleClass('table-danger', !!line.failed);
$tr.append($('<td>').text(line.article !== null && line.article !== undefined ? line.article : '—'));
$tr.append($('<td>').text(line.product_name || '{{ __('— nicht zugeordnet —') }}')
.toggleClass('text-danger font-weight-bold', !line.product_id));
$tr.append($('<td class="text-right">').text(line.qty !== null && line.qty !== undefined ? line.qty : '—'));
var price = parseInt(line.price, 10);
$tr.append($('<td class="text-right">').text(isNaN(price) ? '—' : (price / 100).toFixed(2).replace('.', ',') + ' €'));
var messages = (line.messages || []).map(messageLabel).join('; ');
$tr.append($('<td>').text(messages).addClass(line.failed ? 'text-danger' : 'text-muted'));
$body.append($tr);
});
$('#api-log-modal-raw').toggleClass('d-none', !raw).text(raw || '');
}
$('.js-api-log-view').on('click', function () {
var meta = $(this).data('meta') || {};
var url = $(this).data('url');
$('#api-log-modal-meta').text(
'{{ __('Eingegangen') }}: ' + (meta.received || '—')
+ ' · {{ __('WP-Bestellnr.') }}: ' + (meta.wp_order_number || '—')
+ (meta.customer ? ' · {{ __('Kunde') }}: ' + meta.customer : '')
);
$('#api-log-modal-summary').toggleClass('d-none', !meta.summary).text(meta.summary || '');
// Inhalt erst beim Öffnen laden (Review 20.07.2026)
renderLines([], null);
$('#api-log-modal').modal('show');
$.getJSON(url, function (data) {
renderLines(data.lines || [], data.raw || null);
});
});
});
</script>
@endsection