gruene-seele/resources/views/admin/inventory/api-orders/index.blade.php
Kevin Adametz 71d5591e7b Warenwirtschaft: AP-13 API-Bestellungen — Protokoll mit SKU-Fehlermarkierung
Neuer Navigationspunkt „API-Bestellungen": jede wp/order-Übertragung wird
protokolliert (api_order_logs) — auch abgelehnte (Kunde unbekannt, Bestellung
existiert bereits, Speicherfehler). Positionen ohne Produktzuordnung
(unbekannte WP-Nummer) markieren den Eintrag rot; das Modal zeigt den Inhalt
der Übertragung je Position (SKU, Produkt, Menge, Preis, Meldung übersetzt).
Dazu SKU-Konsistenzprüfung auf der Seite: doppelte WP-Nummern aktiver Produkte
(Warnung) und aktive Produkte ohne WP-Nummer (aufklappbar). Behebt die stille
Lücke, dass „article not found" nur in der API-Antwort unterging und die
Position kommentarlos in Bestellung und Bestandsabzug fehlte (Plan §4).
ShoppingUserController per Pint formatiert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 08:04:16 +00:00

228 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
$lines = $log->payload['lines'] ?? [];
$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>
@if ($log->shopping_order_id)
<a href="{{ route('admin_sales_detail', $log->shopping_order_id) }}">#{{ $log->shopping_order_id }}</a>
@else
<span class="text-muted">—</span>
@endif
</td>
<td class="text-right">{{ count($lines) ?: '—' }}</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-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,
'raw' => isset($log->payload['raw']) ? json_encode($log->payload['raw'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) : null,
]) }}"
data-lines="{{ json_encode($lines) }}">
<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 && $logs->count() >= $listLimit)
<p class="text-muted small px-3 pb-2 mb-0">
{{ __('Es werden die neuesten :n Übertragungen angezeigt.', ['n' => $listLimit]) }}
<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;
}
$('.js-api-log-view').on('click', function () {
var meta = $(this).data('meta') || {};
var lines = $(this).data('lines') || [];
$('#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 || '');
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', !meta.raw).text(meta.raw || '');
$('#api-log-modal').modal('show');
});
});
</script>
@endsection