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>
236 lines
13 KiB
PHP
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">×</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
|