Kundennachtrag 09.09.2026: Ist-Mengen je Charge in der Produktion sollen auf das Milligramm eingegeben werden können (z. B. 0,125 g). Bisher rundeten Datenbank (decimal 12,2), Bestandslogik, Anzeige und Toleranzsockel auf zwei Stellen; „0,125" wurde still zu 0,13, „0.125" nach der Punkt-Regel zu 125 g. - Migration 2026_09_09_100000: production_ingredients.quantity_used, stock_entries.ordered_quantity/received_quantity, stock_disposals.quantity → decimal(12,3); Casts decimal:3. Auf der Dev-DB (Live-Kopie) ausgeführt, 53/78/7 Zeilen unverändert. - InventoryService::QUANTITY_DECIMALS = 3; roundWawiQuantity() ersetzt alle round(…, 2) in InventoryService, ProductionService, StockDisposalController/-Request, StockEntryRepository/-Controller, RawMaterialStockController. - formatWawiQuantity($v, $min = 2): bis drei Stellen, mindestens zwei — Produktionsdetail, Einkauf Liste/Detail/Formular, Ausschuss-Liste, Chargen-Rest im Rohstoff-Detail. - Eingaberegel (reFormatWawiNumber, JS parseGerman/parseNumber): zusätzlich „0.xxx" als Dezimalzahl; „1.500" bleibt 1500. Produktions-JS: fmt() bis drei Stellen, fmtInput() schreibt gespeicherte Zahlen deutsch ins Feld (1,125 statt 1.125 = 1125). - Soll-Ist-Toleranz: Sockel 0,05 → 0,005 g; Ausschuss min 0,001. - Doku: Hinweise (Neu/geändert), Leitfaden Version 3.1 mit Abschnitt 26, Plan V7 AP-53, README (Deploy-Checkliste, Suite 378). Tests: QuantityPrecisionTest (6) — Parsing, Anzeige, Produktion 0,125 g exakt + Rest 9,875 g, Entwurf 1,125 wiederöffnen, Toleranz Kleinmengen, Ausschuss 0,005 g. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
305 lines
17 KiB
PHP
305 lines
17 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">{{ __('Anteil (%)') }}</th>
|
||
<th class="text-right" title="{{ __('Anteil × Füllgewicht × Überschuss-Faktor — wie in der Produktion gerechnet') }}">{{ __('Verbrauch (g/Stück)') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@forelse($ingredient->products as $product)
|
||
@php $usage = $recipeUsage[$product->id] ?? null; @endphp
|
||
<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($usage)
|
||
{{ \App\Services\Util::formatNumber($usage['percent'], 2) }} %
|
||
@else
|
||
<span class="text-muted">—</span>
|
||
@endif
|
||
</td>
|
||
<td class="text-right">
|
||
@if($usage && $usage['consumption_per_piece'] !== null)
|
||
{{ \App\Services\Util::formatNumber($usage['consumption_per_piece'], 2) }} g
|
||
<span class="text-muted small">({{ __('netto') }} {{ \App\Services\Util::formatNumber($usage['grams_per_piece'], 2) }} g)</span>
|
||
@else
|
||
<span class="text-muted" title="{{ __('Kein Füllgewicht hinterlegt') }}">—</span>
|
||
@endif
|
||
</td>
|
||
</tr>
|
||
@empty
|
||
<tr>
|
||
<td colspan="4"><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>
|
||
@if(Auth::user()->isAdmin())
|
||
<th class="text-right">{{ __('Letzter Einkauf (Netto/kg)') }}</th>
|
||
@endif
|
||
<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>
|
||
@if(Auth::user()->isAdmin())
|
||
<td class="text-right">
|
||
@if($lastPrice !== null)
|
||
{{ \App\Services\Util::formatNumber($lastPrice, 2) }} €
|
||
@else
|
||
<span class="text-muted">—</span>
|
||
@endif
|
||
</td>
|
||
@endif
|
||
<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">{{ formatWawiQuantity($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
|