Checkbox-Auswahl im Abwicklungs-Board (Alle-auswählen, Zähler in den Buttons) + zwei Sammel-Aktionen: „Lieferscheine drucken" und „+ Rechnungen" — je ein zusammengeführtes PDF (PDFMerger/FPDI) im neuen Tab zum Stapeldruck. Fehlende Dokumente werden dabei ohne Mailversand erzeugt, auf der Platte fehlende Altdateien mit bestehender Nummer regeneriert. API-Bestellungen sind von der Auswahl ausgenommen (deaktivierte Checkbox, Rechnung liegt im externen Shop) und werden in gemischter Auswahl übersprungen; max. 100 je Druck. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
307 lines
20 KiB
PHP
307 lines
20 KiB
PHP
@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-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>
|
||
<a href="{{ route('admin_sales_detail', $order->id) }}">#{{ $order->id }}</a>
|
||
@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
|
||
@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))
|
||
<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') }}: {{ number_format($order->weight / 1000, 2, ',', '.') }} 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 && $orders->count() >= $listLimit)
|
||
<p class="text-muted small px-3 pb-2 mb-0">
|
||
{{ __('Es werden die neuesten :n Bestellungen angezeigt.', ['n' => $listLimit]) }}
|
||
<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).find('a').first().text().replace('#', '')).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
|