Warenwirtschaft: AP-09 bis AP-13 (Produktbestand, Set-Produkte, Ausschuss, Konzepte)

- AP-09 Produktbestand inkl. Bewegungshistorie (product_stock_movements, ProductStockService)
- AP-10 Rohstoffbestand-Ansicht je Lager (RawMaterialStockController)
- AP-11 Bestandsschwellen / Out-of-Stock-Handling fuer Produkte und Shop
- AP-12 Ausgang/Ausschuss (stock_disposals, StockDisposalController, InventoryService)
- Set-Produkte (product_set_items) inkl. Aufloesung
- Produktentwicklung & Hinweise-Verwaltung (Notices)
- AP-13 Entwicklungskonzept Shop-Bestandsabzug im Plan dokumentiert
- Feature-Tests fuer neue Module + aktualisierter Entwicklungsplan

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin Adametz 2026-06-03 11:04:22 +00:00
parent 78679e0c55
commit 3ee2d756e9
63 changed files with 5968 additions and 901 deletions

View file

@ -0,0 +1,144 @@
@extends('layouts.layout-2')
@section('content')
@include('admin.inventory.partials.table-actions-style')
<div class="card">
<div class="card-header">
<div class="d-flex flex-wrap justify-content-between align-items-center">
<h6 class="mb-0">{{ __('Produktbestand') }}</h6>
<label class="form-check d-inline-flex align-items-center mb-0">
<input type="checkbox" id="ps-only-critical" class="form-check-input mr-2">
<span>{{ __('nur kritische anzeigen') }}</span>
</label>
</div>
<div class="form-group row mt-3 mb-0 align-items-center">
<label for="ps-search" class="col-sm-1 col-form-label">{{ __('Suchen') }}</label>
<div class="col-sm-5">
<input type="text" id="ps-search" class="form-control" autocomplete="off">
</div>
</div>
</div>
<div class="card-datatable table-responsive">
<table class="table table-striped wawi-table mb-0" id="ps-table">
<thead>
<tr>
<th style="width:4rem"></th>
<th>{{ __('Name') }}</th>
<th class="text-right">{{ __('Bestand') }}</th>
<th style="width:18rem">{{ __('Aktion') }}</th>
</tr>
</thead>
<tbody>
@forelse($rows as $row)
@php
$product = $row['product'];
$rowClass = $row['status'] === 'critical' ? 'table-danger' : ($row['status'] === 'warning' ? 'table-warning' : '');
$stockClass = $row['status'] === 'critical' ? 'text-danger font-weight-bold' : ($row['status'] === 'warning' ? 'text-warning font-weight-bold' : '');
@endphp
<tr class="ps-row {{ $rowClass }}" data-name="{{ Str::lower($product->name) }} {{ Str::lower($product->number ?? '') }}" data-status="{{ $row['status'] }}">
<td>
@if ($product->images->count())
<img class="img-fluid" alt="" style="max-height: 42px"
src="{{ route('product_image', [$product->images->first()->slug]) }}">
@endif
</td>
<td>{{ $product->name }}</td>
<td class="text-right {{ $stockClass }}">{{ \App\Services\Util::formatNumber($row['stock'], 0) }} {{ __('Stück') }}</td>
<td>
@if (Auth::user()->isAdmin())
<button type="button" class="btn icon-btn btn-sm btn-success js-ps-move"
data-product="{{ $product->id }}" data-name="{{ $product->name }}" data-direction="in"
title="{{ __('Eingang buchen') }}"><span class="fas fa-plus"></span></button>
<button type="button" class="btn icon-btn btn-sm btn-outline-danger js-ps-move"
data-product="{{ $product->id }}" data-name="{{ $product->name }}" data-direction="out"
title="{{ __('Ausgang buchen') }}"><span class="fas fa-minus"></span></button>
@endif
<a href="{{ route('admin.inventory.productions.create', ['product_id' => $product->id]) }}"
class="btn btn-sm btn-dark">{{ __('Produzieren') }}</a>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="text-center text-muted py-4">{{ __('Keine Produkte vorhanden.') }}</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
@if (Auth::user()->isAdmin())
<div class="modal fade" id="ps-move-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<form method="post" id="ps-move-form">
@csrf
<input type="hidden" name="direction" id="ps-move-direction">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="ps-move-title">{{ __('Bestandsbewegung') }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<p class="text-muted mb-3" id="ps-move-product"></p>
<div class="form-group">
<label for="ps-move-quantity">{{ __('Stückzahl') }} <span class="text-danger">*</span></label>
<input type="number" min="1" step="1" name="quantity" id="ps-move-quantity" class="form-control" required>
</div>
<div class="form-group">
<label for="ps-move-reason">{{ __('Grund') }} <span class="text-danger">*</span></label>
<select name="reason" id="ps-move-reason" class="form-control" required>
@foreach ($reasons as $reason)
<option value="{{ $reason }}">{{ $reason }}</option>
@endforeach
</select>
</div>
<div class="form-group mb-0">
<label for="ps-move-note">{{ __('Hinweis') }}</label>
<input type="text" name="note" id="ps-move-note" class="form-control" maxlength="255" autocomplete="off">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{ __('Abbrechen') }}</button>
<button type="submit" class="btn btn-primary">{{ __('Buchen') }}</button>
</div>
</div>
</form>
</div>
</div>
@endif
<script>
$(function () {
var $rows = $('#ps-table tbody tr.ps-row');
function applyFilter() {
var term = ($('#ps-search').val() || '').toLowerCase().trim();
var onlyCritical = $('#ps-only-critical').is(':checked');
$rows.each(function () {
var $row = $(this);
var matchesTerm = term === '' || ($row.data('name') || '').toString().indexOf(term) !== -1;
var status = $row.data('status');
var matchesCritical = !onlyCritical || (status === 'critical' || status === 'warning');
$row.toggle(matchesTerm && matchesCritical);
});
}
$('#ps-search').on('keyup', applyFilter);
$('#ps-only-critical').on('change', applyFilter);
var moveTemplate = "{{ route('admin.inventory.product-stock.movement', ['product' => '__ID__']) }}";
$('.js-ps-move').on('click', function () {
var id = $(this).data('product');
var name = $(this).data('name');
var direction = $(this).data('direction');
$('#ps-move-form').attr('action', moveTemplate.replace('__ID__', id));
$('#ps-move-direction').val(direction);
$('#ps-move-product').text(name);
$('#ps-move-title').text(direction === 'in' ? '{{ __('Eingang buchen') }}' : '{{ __('Ausgang buchen') }}');
$('#ps-move-quantity').val('');
$('#ps-move-note').val('');
$('#ps-move-modal').modal('show');
});
});
</script>
@endsection