gruene-seele/resources/views/admin/inventory/orders/index.blade.php
Kevin Adametz f93b16c8f7 Warenwirtschaft: F-10 — Versandabschluss nur mit Zahlungsfreigabe
Board-Statuswechsel auf „versendet“ prüft serverseitig isReadyToShip()
(Rechnung/API weiterhin erlaubt); Button im Board ohne Freigabe deaktiviert.
Konfektionieren bleibt vor Zahlungseingang möglich.

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

328 lines
22 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@extends('layouts.layout-2')
@section('content')
@include('admin.inventory.partials.wawi-ui')
<style>
/* AP-33a: iPad-Optimierung — Positionen (letzte Spalte) und Aktionen bleiben
beim horizontalen Scrollen rechts angepinnt, alles andere ist kompakt. */
#of-table th, #of-table td { padding: .45rem .5rem; }
#of-table td { vertical-align: middle; }
#of-table th.of-pos,
#of-table td.of-pos {
position: sticky;
right: 0;
width: 56px;
min-width: 56px;
background: #fff;
text-align: center;
}
#of-table th.of-actions,
#of-table td.of-actions {
position: sticky;
right: 56px;
background: #fff;
box-shadow: -6px 0 6px -6px rgba(24, 28, 33, .15);
white-space: nowrap;
}
#of-table td.of-customer { max-width: 150px; }
#of-table td.of-customer .wawi-item-name {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: bottom;
}
@media (max-width: 1199.98px) {
.of-btn-text { display: none; }
#of-table .btn .mr-1 { margin-right: 0 !important; }
#of-table td.of-customer { max-width: 110px; }
}
</style>
<div class="wawi-page">
<div class="wawi-page-head">
<div>
<h1 class="wawi-page-head__title">{{ __('Bestell-Abwicklung') }}</h1>
<p class="wawi-page-head__subtitle">{{ __('Tagesübersicht: konfektionieren, Dokumente drucken, versenden — versandfertig = bezahlt (Rechnung sofort, Vorkasse nach Zahlungseingang, API immer)') }}</p>
</div>
<div class="wawi-page-head__actions">
<a href="{{ route('admin.inventory.api-orders.index') }}" class="btn btn-outline-secondary btn-sm">
<span class="fas fa-cloud-download-alt mr-1"></span>{{ __('API-Bestellungen') }}
</a>
</div>
</div>
<div class="wawi-stats">
<a class="wawi-stat is-clickable text-reset" href="{{ route('admin.inventory.orders.index', ['filter' => 'today']) }}">
<div class="wawi-stat__icon"><span class="fas fa-inbox"></span></div>
<div class="wawi-stat__body">
<div class="wawi-stat__value">{{ $counts['today'] }}</div>
<div class="wawi-stat__label">{{ __('Heute eingegangen') }}</div>
</div>
</a>
<a class="wawi-stat wawi-stat--warning is-clickable text-reset" href="{{ route('admin.inventory.orders.index', ['filter' => 'pick']) }}">
<div class="wawi-stat__icon"><span class="fas fa-box-open"></span></div>
<div class="wawi-stat__body">
<div class="wawi-stat__value">{{ $counts['pick'] }}</div>
<div class="wawi-stat__label">{{ __('Zu konfektionieren') }}</div>
</div>
</a>
<a class="wawi-stat wawi-stat--ok is-clickable text-reset" href="{{ route('admin.inventory.orders.index', ['filter' => 'ready']) }}">
<div class="wawi-stat__icon"><span class="fas fa-truck-loading"></span></div>
<div class="wawi-stat__body">
<div class="wawi-stat__value">{{ $counts['ready'] }}</div>
<div class="wawi-stat__label">{{ __('Versandfertig') }}</div>
</div>
</a>
<a class="wawi-stat is-clickable text-reset" href="{{ route('admin.inventory.orders.index', ['filter' => 'shipped_today']) }}">
<div class="wawi-stat__icon"><span class="fas fa-shipping-fast"></span></div>
<div class="wawi-stat__body">
<div class="wawi-stat__value">{{ $counts['shipped_today'] }}</div>
<div class="wawi-stat__label">{{ __('Heute versendet') }}</div>
</div>
</a>
</div>
<div class="wawi-card">
<div class="wawi-toolbar">
<div class="d-flex align-items-center flex-wrap" style="gap:.75rem;">
@if ($filter !== '')
<a href="{{ route('admin.inventory.orders.index') }}" class="btn btn-sm btn-outline-secondary">
<span class="fas fa-times mr-1"></span>{{ __('Filter zurücksetzen') }}
</a>
@endif
<input type="search" id="of-search" class="form-control form-control-sm" style="width:240px"
placeholder="{{ __('Suche Bestellnr. / Kunde …') }}">
{{-- AP-33b: Sammel-Druck der ausgewählten Bestellungen (öffnet ein PDF im neuen Tab) --}}
<form id="of-batch-form" method="post" action="{{ route('admin.inventory.orders.documents.batch') }}" target="_blank" class="mb-0 d-flex align-items-center" style="gap:.5rem;">
@csrf
<button type="submit" name="type" value="delivery" class="btn btn-sm btn-outline-secondary js-of-batch" disabled
title="{{ __('Lieferscheine der Auswahl als ein PDF (fehlende Dokumente werden erstellt)') }}">
<span class="fas fa-dolly mr-1"></span>{{ __('Lieferscheine drucken') }} (<span class="of-batch-count">0</span>)
</button>
<button type="submit" name="type" value="combined" class="btn btn-sm btn-outline-secondary js-of-batch" disabled
title="{{ __('Rechnungen + Lieferscheine der Auswahl als ein PDF (fehlende Dokumente werden erstellt)') }}">
<span class="fas fa-print mr-1"></span>{{ __('+ Rechnungen') }}
</button>
</form>
</div>
</div>
<div class="table-responsive">
<table class="table wawi-table" id="of-table">
<thead>
<tr>
<th style="width:32px;"><input type="checkbox" id="of-check-all" title="{{ __('Alle sichtbaren auswählen') }}"></th>
<th>{{ __('Bestellung') }}</th>
<th>{{ __('Kunde') }}</th>
<th>{{ __('Status') }}</th>
<th>{{ __('Dokumente') }}</th>
<th class="text-right of-actions">{{ __('Aktionen') }}</th>
<th class="of-pos" title="{{ __('Positionen') }}">{{ __('Pos.') }}</th>
</tr>
</thead>
<tbody>
@forelse ($orders as $order)
@php
$customer = $order->shopping_user
? trim(($order->shopping_user->shipping_firstname ?: $order->shopping_user->billing_firstname ?? '').' '.($order->shopping_user->shipping_lastname ?: $order->shopping_user->billing_lastname ?? ''))
: '';
$clearingtype = $order->shopping_payment_last?->clearingtype;
$readyToShip = $order->isReadyToShip();
$itemCount = $order->shopping_order_items->sum('qty');
@endphp
<tr class="of-row" data-order-id="{{ $order->id }}" data-search="{{ mb_strtolower($order->id.' '.$customer) }}">
<td>
@if ($order->isApiOrder())
<input type="checkbox" disabled title="{{ __('API-Bestellung — Dokumente liegen im externen Shop') }}">
@else
<input type="checkbox" class="js-of-check" form="of-batch-form" name="order_ids[]" value="{{ $order->id }}">
@endif
</td>
<td>
{{-- Verkäufe-Detail verlangt admin ≥ 7 — Versand-Mitarbeiter (copyreader)
bekämen sonst einen stillen Redirect (Review 20.07.2026) --}}
@if (auth()->user()->isAdmin())
<a href="{{ route('admin_sales_detail', $order->id) }}">#{{ $order->id }}</a>
@else
<span class="font-weight-bold">#{{ $order->id }}</span>
@endif
@if ($order->isApiOrder())
<span class="wawi-pill" style="background:rgba(24,28,33,.06);color:#6c757d;">API</span>
@elseif ($order->user_shop_id)
<span class="wawi-pill" style="background:rgba(24,28,33,.06);color:#6c757d;">{{ __('Shop') }}</span>
@elseif ($order->promotion_user_id)
<span class="wawi-pill" style="background:rgba(24,28,33,.06);color:#6c757d;">{{ __('Promo') }}</span>
@endif
<div class="small text-muted">{{ $order->created_at->format('d.m.Y H:i') }}</div>
</td>
<td class="of-customer" title="{{ $customer }}">
<span class="wawi-item-name">{{ $customer !== '' ? $customer : '—' }}</span>
@if ($order->shipping_country?->country)
<span class="text-muted small">{{ strtoupper($order->shipping_country->country->code) }}</span>
@endif
</td>
<td>
@if ((int) $order->shipped === 0)
<span class="wawi-pill wawi-pill--warning">{{ __('offen') }}</span>
@else
<span class="wawi-pill" style="background:rgba(3,155,229,.12);color:#0277bd;">{{ $order->getShippedType() }}</span>
@endif
<div class="small mt-1">
@if ($readyToShip)
<span class="wawi-pill wawi-pill--ok">{{ __('versandfertig') }}</span>
@else
<span class="wawi-pill wawi-pill--danger">{{ __('wartet auf Zahlung') }}</span>
@endif
@if ($clearingtype === 'fnc')
<span class="text-muted">{{ __('Rechnung') }}</span>
@elseif ($clearingtype === 'vor')
<span class="text-muted">{{ __('Vorkasse') }}</span>
@endif
</div>
</td>
<td style="white-space:nowrap;">
@if ($order->isApiOrder())
@if ($order->wp_invoice_path)
<a href="{{ $order->wp_invoice_path }}" target="_blank" rel="noopener" title="{{ __('Externe Rechnung (Shop)') }}">
<span class="fas fa-external-link-alt"></span> {{ __('Extern') }}
</a>
@else
<span class="text-muted small">{{ __('extern') }}</span>
@endif
{{-- AP-32 Brückenlösung: DHL-Label aus dem Shop (lokale Kopie; die Shop-URL
ist zugriffsgeschützt und wird bewusst nicht verlinkt, Review 20.07.2026) --}}
@if ($order->wp_label_path)
<a href="{{ route('admin.inventory.orders.document', [$order->id, 'label']) }}" target="_blank" title="{{ __('DHL-Label') }}" class="ml-1">
<span class="fas fa-tag"></span> {{ __('Label') }}
</a>
@endif
@if ($order->tracking)
<div class="small text-muted" title="{{ __('Sendungsnummer') }}">{{ $order->tracking }}</div>
@endif
@elseif (\App\Services\Invoice::isInvoice($order))
<a href="{{ route('admin.inventory.orders.document', [$order->id, 'invoice']) }}" target="_blank" title="{{ __('Rechnung') }}" class="mr-1"><span class="far fa-file-alt"></span> {{ __('Re.') }}</a>
<a href="{{ route('admin.inventory.orders.document', [$order->id, 'delivery']) }}" target="_blank" title="{{ __('Lieferschein') }}" class="mr-1"><span class="fas fa-dolly"></span> {{ __('LS') }}</a>
<a href="{{ route('admin.inventory.orders.document', [$order->id, 'combined']) }}" target="_blank" title="{{ __('Rechnung + Lieferschein') }}"><span class="fas fa-print"></span></a>
@else
<form action="{{ route('admin.inventory.orders.documents', $order->id) }}" method="post" class="d-inline">
@csrf
<input type="hidden" name="back_filter" value="{{ $filter }}">
<button type="submit" class="btn btn-sm btn-outline-secondary" title="{{ __('Rechnung + Lieferschein erstellen (ohne E-Mail-Versand)') }}"
onclick="return confirm(@json(__('Rechnung + Lieferschein jetzt erstellen? (Es wird keine E-Mail versendet — Versand wie gewohnt über die Bestell-Details.)')));">
<span class="fas fa-file-medical mr-1"></span><span class="of-btn-text">{{ __('erstellen') }}</span>
</button>
</form>
@endif
</td>
<td class="text-right of-actions">
@if ((int) $order->shipped === 0)
<form action="{{ route('admin.inventory.orders.status', $order->id) }}" method="post" class="d-inline">
@csrf
<input type="hidden" name="shipped" value="1">
<input type="hidden" name="back_filter" value="{{ $filter }}">
<button type="submit" class="btn btn-sm btn-outline-primary" title="{{ __('Als konfektioniert (in Bearbeitung) markieren') }}">
<span class="fas fa-box mr-1"></span><span class="of-btn-text">{{ __('Konfektioniert') }}</span>
</button>
</form>
@endif
@if (in_array((int) $order->shipped, [0, 1], true) && ! $readyToShip)
{{-- F-10: ohne Zahlungsfreigabe kein Versandabschluss (Server prüft ebenfalls) --}}
<button type="button" class="btn btn-sm btn-primary" disabled title="{{ __('Wartet auf Zahlung — Versand erst nach Zahlungseingang') }}">
<span class="fas fa-shipping-fast mr-1"></span><span class="of-btn-text">{{ __('Versendet') }}</span>
</button>
@elseif (in_array((int) $order->shipped, [0, 1], true))
<form action="{{ route('admin.inventory.orders.status', $order->id) }}" method="post" class="d-inline"
onsubmit="return confirm(@json(__('Bestellung als versendet buchen? Der Produktbestand wird abgezogen.')));">
@csrf
<input type="hidden" name="shipped" value="2">
<input type="hidden" name="back_filter" value="{{ $filter }}">
<button type="submit" class="btn btn-sm btn-primary" title="{{ __('Als versendet buchen (Bestandsabzug)') }}">
<span class="fas fa-shipping-fast mr-1"></span><span class="of-btn-text">{{ __('Versendet') }}</span>
</button>
</form>
@endif
</td>
<td class="of-pos">
<button type="button" class="btn btn-sm btn-link p-0 js-of-toggle" data-target="of-items-{{ $order->id }}" title="{{ __('Packliste anzeigen') }}">
{{ $itemCount }} <span class="fas fa-chevron-down small"></span>
</button>
</td>
</tr>
<tr class="of-items d-none" id="of-items-{{ $order->id }}">
<td colspan="7" class="pl-4" style="background:rgba(24,28,33,.02);">
<strong class="small text-muted">{{ __('Packliste') }}</strong>
@if ($order->weight)
<span class="small text-muted">· {{ __('Gewicht') }}: {{ formatNumber($order->weight / 1000) }} kg</span>
@endif
<ul class="mb-1 small">
@foreach ($order->shopping_order_items as $item)
<li>
{{ $item->qty }} × {{ $item->product?->name ?? __('(Produkt gelöscht)') }}
@if ($item->product?->is_set)
<span class="wawi-pill" style="background:rgba(24,28,33,.06);color:#6c757d;">SET</span>
<ul class="text-muted">
@foreach ($item->product->setItems as $component)
<li>{{ $item->qty * $component->pivot->quantity }} × {{ $component->name }}</li>
@endforeach
</ul>
@endif
</li>
@endforeach
</ul>
</td>
</tr>
@empty
<tr>
<td colspan="7">
<div class="wawi-empty">
<div><span class="fas fa-mug-hot"></span></div>
{{ __('Nichts zu tun — keine Bestellungen in diesem Filter.') }}
</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 Bestellungen angezeigt.', ['n' => $listLimit, 'total' => $listTotal]) }}
<a href="{{ route('admin.inventory.orders.index', array_filter(['all' => 1, 'filter' => $filter])) }}">{{ __('Alle anzeigen') }}</a>
</p>
@endif
</div>
</div>
<script>
$(function () {
$('.js-of-toggle').on('click', function () {
$('#' + $(this).data('target')).toggleClass('d-none');
});
$('#of-search').on('input', function () {
var term = ($(this).val() || '').toLowerCase().trim();
$('#of-table tbody tr.of-row').each(function () {
var show = term === '' || ($(this).data('search') || '').toString().indexOf(term) !== -1;
$(this).toggle(show);
if (!show) {
$('#of-items-' + $(this).data('order-id')).addClass('d-none');
}
});
});
// AP-33b: Auswahl für den Sammel-Druck
function refreshBatchButtons() {
var count = $('.js-of-check:checked').length;
$('.js-of-batch').prop('disabled', count === 0);
$('.of-batch-count').text(count);
}
$('#of-check-all').on('change', function () {
var checked = $(this).is(':checked');
$('#of-table tbody tr.of-row:visible .js-of-check').prop('checked', checked);
refreshBatchButtons();
});
$(document).on('change', '.js-of-check', refreshBatchButtons);
});
</script>
@endsection