gruene-seele/resources/views/admin/inventory/raw-material-stock/show.blade.php
Kevin Adametz 7a5b5e0f7b Warenwirtschaft: AP-27 Wareneingang gleiche Charge zusammenführen
- Chargen-Existenz-Check beim Buchen (JSON-Endpoint batch-check): Hinweis
  „Charge bereits im Haus" + Vorbelegung MHD/Qualität aus dem letzten Eingang
- Rohstoffbestand-Detail: Chargen je batch_number summiert
  (InventoryService::chargeGroupsForIngredient), Einkäufe als Unterzeilen
- Produktion: Charge-Dropdown je Charge mit Summen-Rest (batches im
  Recipe-Payload), Verbrauch wird per expandBatchLines FEFO auf die
  Einzel-Einkäufe der Charge verteilt; Service-API bleibt je stock_entry
- Jeder Einkauf bleibt eigener stock_entry (Preis/Datum/Audit)
- Hinweise-Doku + Plan V6.0 §6 fortgeschrieben

Tests: StockEntryChargeMergeTest (7 neu); Suite 193 grün, 698 Assertions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:36:19 +00:00

291 lines
16 KiB
PHP

@extends('layouts.layout-2')
@php
$statusPill = match ($status) {
'critical' => '<span class="wawi-pill wawi-pill--danger">' . __('Kritisch') . '</span>',
'critical_ordered' => '<span class="wawi-pill wawi-pill--warning">' . __('Kritisch · bereits bestellt') . '</span>',
'warning' => '<span class="wawi-pill wawi-pill--warning">' . __('Bald nachbestellen') . '</span>',
default => '<span class="wawi-pill wawi-pill--ok">' . __('Ausreichend') . '</span>',
};
$stockCritical = in_array($status, ['critical', 'critical_ordered'], true);
@endphp
@section('content')
@include('admin.inventory.partials.wawi-ui')
<div class="wawi-page">
<div class="wawi-page-head">
<div>
<h1 class="wawi-page-head__title">
{{ $ingredient->name }} {!! $statusPill !!}
</h1>
<p class="wawi-page-head__subtitle">
{{ __('Rohstoffbestellung & Bestand') }}
@if($ingredient->alt_name)
· {{ __('Alternativer Name') }}: {{ $ingredient->alt_name }}
@endif
</p>
</div>
<div class="wawi-page-head__actions">
<a href="{{ route('admin.inventory.raw-material-stock.index') }}" class="btn btn-outline-secondary btn-sm">
<span class="fas fa-arrow-left mr-1"></span>{{ __('Zurück') }}
</a>
</div>
</div>
<div class="wawi-stats">
<div class="wawi-stat {{ $stockCritical ? 'wawi-stat--danger' : 'wawi-stat--ok' }}">
<div class="wawi-stat__icon"><span class="fas fa-warehouse"></span></div>
<div class="wawi-stat__body">
<div class="wawi-stat__value">{{ \App\Services\Util::formatNumber($remaining, 0) }} g</div>
<div class="wawi-stat__label">{{ __('Gesamtbestand') }}</div>
</div>
</div>
<div class="wawi-stat">
<div class="wawi-stat__icon"><span class="fas fa-truck"></span></div>
<div class="wawi-stat__body">
<div class="wawi-stat__value">{{ $openTotal > 0 ? \App\Services\Util::formatNumber($openTotal, 0) . ' g' : '—' }}</div>
<div class="wawi-stat__label">{{ __('Offen bestellt') }}</div>
</div>
</div>
<div class="wawi-stat">
<div class="wawi-stat__icon"><span class="fas fa-gauge-high"></span></div>
<div class="wawi-stat__body">
<div class="wawi-stat__value">{{ ($daily !== null && $daily > 0) ? \App\Services\Util::formatNumber($daily, 0) . ' g' : '—' }}</div>
<div class="wawi-stat__label">{{ __('Verbrauch / Tag') }}</div>
</div>
</div>
<div class="wawi-stat {{ ($expectedEmpty !== null && $daysUntilEmpty <= 30) ? 'wawi-stat--warning' : '' }}">
<div class="wawi-stat__icon"><span class="fas fa-hourglass-half"></span></div>
<div class="wawi-stat__body">
<div class="wawi-stat__value">{{ $expectedEmpty !== null ? $expectedEmpty->format('d.m.Y') : '—' }}</div>
<div class="wawi-stat__label">
{{ __('Voraussichtlich auf Null') }}
@if($expectedEmpty !== null)
({{ $daysUntilEmpty }} {{ trans_choice('Tag|Tagen', $daysUntilEmpty) }})
@endif
</div>
</div>
</div>
</div>
<div class="wawi-card" style="margin-bottom:1.25rem;">
<div class="wawi-card__footer" style="border-top:0; background:transparent;">
<span class="text-muted small mr-3">
{{ __('Qualität') }}: <strong>{{ $ingredient->materialQuality?->name ?? '—' }}</strong>
</span>
@if($ingredient->min_stock_alert !== null)
<span class="text-muted small mr-3">
{{ __('Meldebestand') }}: <strong>{{ \App\Services\Util::formatNumber($ingredient->min_stock_alert, 0) }} g</strong>
</span>
@endif
@if($ingredient->storageLabel())
<span class="text-muted small">
{{ __('Lagerort') }}: <strong>{{ $ingredient->storageLabel() }}</strong>
</span>
@endif
</div>
</div>
<div class="wawi-card">
<div class="wawi-card__header">{{ __('Enthalten in') }}</div>
<div class="table-responsive">
<table class="table wawi-table">
<thead>
<tr>
<th>{{ __('Produkt') }}</th>
<th class="text-right">{{ __('Produktbestand') }}</th>
<th class="text-right">{{ __('Rezeptur-Anteil (g/Stück)') }}</th>
</tr>
</thead>
<tbody>
@forelse($ingredient->products as $product)
<tr>
<td>{{ $product->name }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($productStock[$product->id] ?? 0, 0) }} {{ __('Stück') }}</td>
<td class="text-right">
@if($product->pivot->gram !== null && $product->pivot->gram !== '')
{{ \App\Services\Util::formatNumber($product->pivot->gram, 2) }} g
@else
<span class="text-muted">—</span>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="3"><div class="wawi-empty">{{ __('Dieser Rohstoff wird in keiner aktiven Rezeptur verwendet.') }}</div></td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<div class="wawi-card">
<div class="wawi-card__header">{{ __('Lieferanten & Bestellung') }}</div>
<div class="table-responsive">
<table class="table wawi-table">
<thead>
<tr>
<th>{{ __('Lieferant') }}</th>
<th>{{ __('Lieferzeit') }}</th>
<th class="text-right">{{ __('Letzter Einkauf (Netto/kg)') }}</th>
<th class="text-right" style="width: 11rem;"></th>
</tr>
</thead>
<tbody>
@forelse($ingredient->suppliers as $supplier)
@php
$deliveryTime = $ingredient->delivery_time ?: $supplier->delivery_time;
$lastPrice = $lastPriceBySupplier[$supplier->id] ?? null;
// AP-21: Bestellweg pro INCI-Lieferant — Pivot-URL gesetzt ⇒ Shop, leer ⇒ Mail.
$orderUrl = $supplier->pivot->url;
$orderEmail = $supplier->order_email ?: $supplier->email;
@endphp
<tr>
<td>
<span class="wawi-item-name">{{ $supplier->name }}</span>
@if($supplier->pivot->preferred)
<span class="wawi-pill wawi-pill--ok ml-1">{{ __('bevorzugt') }}</span>
@endif
@if($supplier->pivot->supplier_sku)
<div class="wawi-item-sub">{{ __('Art.-Nr.') }}: {{ $supplier->pivot->supplier_sku }}</div>
@endif
</td>
<td>{{ $deliveryTime ?: '—' }}</td>
<td class="text-right">
@if($lastPrice !== null)
{{ \App\Services\Util::formatNumber($lastPrice, 2) }} €
@else
<span class="text-muted">—</span>
@endif
</td>
<td class="text-right">
@if($orderUrl)
<a href="{{ $orderUrl }}" target="_blank" rel="noopener" class="btn btn-sm btn-outline-secondary">{{ __('Zum Shop') }}</a>
@elseif($orderEmail)
<a href="mailto:{{ $orderEmail }}?subject={{ rawurlencode(__('Bestellung') . ': ' . $ingredient->name) }}" class="btn btn-sm btn-outline-secondary">{{ __('Per Mail') }}</a>
@else
<span class="text-muted">—</span>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="4"><div class="wawi-empty">{{ __('Diesem Rohstoff ist kein Lieferant zugeordnet.') }}</div></td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if(Auth::user()->isAdmin())
<div class="wawi-card__footer">
<a href="{{ route('admin.inventory.stock-entries.create', ['ingredient_id' => $ingredient->id]) }}" class="btn btn-sm btn-primary">{{ __('Einkauf erfassen') }}</a>
<a href="{{ route('admin.inventory.stock-disposals.create', ['ingredient_id' => $ingredient->id]) }}" class="btn btn-sm btn-outline-danger">{{ __('Ausschuss erfassen') }}</a>
</div>
@endif
</div>
<div class="wawi-card">
<div class="wawi-card__header">{{ __('Offene Bestellungen / unterwegs') }}</div>
<div class="table-responsive">
<table class="table wawi-table">
<thead>
<tr>
<th>{{ __('Bestellt am') }}</th>
<th>{{ __('Lieferant') }}</th>
<th>{{ __('Lagerort') }}</th>
<th class="text-right">{{ __('Bestellte Menge') }}</th>
<th class="text-right" style="width: 8rem;"></th>
</tr>
</thead>
<tbody>
@forelse($openOrders as $order)
<tr>
<td>{{ $order->ordered_at?->format('d.m.Y') ?? '—' }}</td>
<td>{{ $order->supplier?->name ?? '—' }}</td>
<td>{{ $order->location?->name ?? '—' }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($order->ordered_quantity, 0) }} g</td>
<td class="text-right">
<a href="{{ route('admin.inventory.stock-entries.show', $order) }}" class="btn icon-btn btn-sm btn-primary" title="{{ __('Wareneingang buchen') }}">
<span class="far fa-eye"></span>
</a>
</td>
</tr>
@empty
<tr>
<td colspan="5"><div class="wawi-empty">{{ __('Keine offenen Bestellungen.') }}</div></td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($openOrders->isNotEmpty())
<div class="wawi-card__footer text-muted small">
{{ __('Offene Bestellungen zählen erst nach gebuchtem Wareneingang zum Bestand.') }}
</div>
@endif
</div>
<div class="wawi-card">
<div class="wawi-card__header">{{ __('Verfügbare Chargen') }}</div>
<div class="table-responsive">
<table class="table wawi-table">
<thead>
<tr>
<th>{{ __('Charge') }}</th>
<th>{{ __('Lieferant') }}</th>
<th>{{ __('Lagerort') }}</th>
<th>{{ __('MHD') }}</th>
<th class="text-right">{{ __('Restbestand') }}</th>
</tr>
</thead>
<tbody>
{{-- AP-27: Anzeige je Charge (batch_number) summiert; Einzel-Einkäufe als Unterzeilen --}}
@forelse($chargeGroups as $group)
<tr>
<td>
{{ $group['batch_number'] ?: '#' . $group['entries']->first()->id }}
@if($group['entries']->count() > 1)
<span class="badge badge-light border ml-1">{{ $group['entries']->count() }} {{ __('Einkäufe') }}</span>
@endif
</td>
<td>{{ $group['suppliers']->isNotEmpty() ? $group['suppliers']->implode(', ') : '—' }}</td>
<td>{{ $group['locations']->isNotEmpty() ? $group['locations']->implode(', ') : '—' }}</td>
<td>{{ $group['best_before']?->format('d.m.Y') ?? '—' }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($group['remaining'], 0) }} g</td>
</tr>
@if($group['entries']->count() > 1)
@foreach($group['entries'] as $charge)
<tr class="text-muted small">
<td class="pl-4">↳ {{ __('Einkauf vom') }} {{ $charge->received_at?->format('d.m.Y') ?? '—' }}</td>
<td>{{ $charge->supplier?->name ?? '—' }}</td>
<td>{{ $charge->location?->name ?? '—' }}</td>
<td>{{ $charge->best_before?->format('d.m.Y') ?? '—' }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($charge->getAttribute('remaining_quantity'), 0) }} g</td>
</tr>
@endforeach
@endif
@empty
<tr>
<td colspan="5"><div class="wawi-empty">{{ __('Kein Restbestand vorhanden.') }}</div></td>
</tr>
@endforelse
</tbody>
@if($chargeGroups->isNotEmpty() && count($remainingByLocation) > 0)
<tfoot>
@foreach($locations as $location)
@if(isset($remainingByLocation[$location->id]) && $remainingByLocation[$location->id] > 0)
<tr>
<td colspan="4" class="text-right text-muted">{{ __('Bestand') }} {{ $location->name }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($remainingByLocation[$location->id], 0) }} g</td>
</tr>
@endif
@endforeach
</tfoot>
@endif
</table>
</div>
</div>
</div>
@endsection