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>
This commit is contained in:
parent
211aeed384
commit
71d5591e7b
8 changed files with 859 additions and 170 deletions
228
resources/views/admin/inventory/api-orders/index.blade.php
Normal file
228
resources/views/admin/inventory/api-orders/index.blade.php
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
@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">×</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
|
||||
Loading…
Add table
Add a link
Reference in a new issue