Warenwirtschaft: R-01 — stabile Phasen-IDs im Artikelformular, keine Zuordnung nach Reihenfolge
mfg_phase_id[] je Phasenblock; Zuordnung bestehender Phasen über ID, dann Name, dann gemeinsame Rohstoffe. Ein vorangestellter neuer Block übernimmt nicht mehr die ID einer umbenannten Phase; Produktionszeilen behalten ihre Phase. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
a439d05a6a
commit
fda37d5599
2 changed files with 64 additions and 12 deletions
|
|
@ -274,6 +274,14 @@ class ProductRepository extends BaseRepository
|
|||
// Audit H-4: Delete-then-Insert atomar — ein Fehler beim Neuaufbau darf
|
||||
// die bestehende Rezeptur nicht vernichten.
|
||||
return \DB::transaction(function () use ($data): bool {
|
||||
// R-01: bisherige Rohstoff→Phase-Zuordnung sichern (für die Phasenzuordnung ohne IDs).
|
||||
$previousRowsByPhase = ProductIngredient::where('product_id', $this->model->id)
|
||||
->where('recipe_type', 'manufacturer')
|
||||
->get(['ingredient_id', 'recipe_phase_id'])
|
||||
->groupBy(fn ($r) => (int) ($r->recipe_phase_id ?? 0))
|
||||
->map(fn ($rows) => $rows->pluck('ingredient_id')->map(fn ($id) => (int) $id)->unique()->values()->all())
|
||||
->all();
|
||||
|
||||
ProductIngredient::where('product_id', $this->model->id)
|
||||
->where('recipe_type', 'manufacturer')
|
||||
->delete();
|
||||
|
|
@ -284,7 +292,7 @@ class ProductRepository extends BaseRepository
|
|||
// löschen/neu anlegen — Produktions-Verbrauchszeilen referenzieren die Phase
|
||||
// (production_ingredients.recipe_phase_id, nullOnDelete) und verlören sonst
|
||||
// bei jedem Artikelspeichern ihre Zuordnung.
|
||||
$phaseIds = $this->persistRecipePhases($data);
|
||||
$phaseIds = $this->persistRecipePhases($data, $previousRowsByPhase);
|
||||
|
||||
if (! isset($data['mfg_ingredient_id']) || ! is_array($data['mfg_ingredient_id'])) {
|
||||
return true;
|
||||
|
|
@ -338,16 +346,28 @@ class ProductRepository extends BaseRepository
|
|||
}
|
||||
|
||||
/**
|
||||
* Legt die Rezeptur-Phasen (AP-23) an und liefert Block-Index => Phase-ID.
|
||||
* Legt/aktualisiert die Rezeptur-Phasen (AP-23) und liefert Block-Index => Phase-ID.
|
||||
* Ein einzelner Block ohne Namen und Notiz bleibt „phasenlos" (Altverhalten,
|
||||
* Zeilen behalten recipe_phase_id NULL).
|
||||
*
|
||||
* N-02/R-01: Bestehende Phasen werden aktualisiert statt neu angelegt, damit
|
||||
* Produktions-Verbrauchszeilen (recipe_phase_id) ihre Zuordnung behalten.
|
||||
* Zuordnung Formularblock → bestehende Phase:
|
||||
* 1. übermittelte Phasen-ID (mfg_phase_id[], Formular) — geprüft gegen Produkt/Typ,
|
||||
* 2. gleicher (effektiver) Name,
|
||||
* 3. gemeinsame Rohstoffe (Block enthält Rohstoffe, die die Phase bisher hatte).
|
||||
* Bewusst **keine** Zuordnung nach Reihenfolge: Ein vorangestellter neuer Block
|
||||
* würde sonst die ID einer umbenannten Phase übernehmen (Gegenprüfung R-01).
|
||||
* Nicht zugeordnete bestehende Phasen werden gelöscht (Verbrauchszeilen: nullOnDelete).
|
||||
*
|
||||
* @param array<int, array<int, int>> $previousRowsByPhase [phase_id => ingredient_ids] vor dem Neuaufbau
|
||||
* @return array<int, int>
|
||||
*/
|
||||
private function persistRecipePhases(array $data): array
|
||||
private function persistRecipePhases(array $data, array $previousRowsByPhase = []): array
|
||||
{
|
||||
$names = array_values($data['mfg_phase_name'] ?? []);
|
||||
$notes = array_values($data['mfg_phase_note'] ?? []);
|
||||
$submittedIds = array_values($data['mfg_phase_id'] ?? []);
|
||||
|
||||
$implicitSingle = count($names) <= 1
|
||||
&& trim((string) ($names[0] ?? '')) === ''
|
||||
|
|
@ -365,8 +385,6 @@ class ProductRepository extends BaseRepository
|
|||
return [];
|
||||
}
|
||||
|
||||
// N-02: Zuordnung neue → bestehende Phase zuerst über den (effektiven) Namen,
|
||||
// danach in Reihenfolge; nur wirklich entfernte Phasen werden gelöscht.
|
||||
$blocks = [];
|
||||
foreach ($names as $index => $name) {
|
||||
$name = trim((string) $name);
|
||||
|
|
@ -378,23 +396,54 @@ class ProductRepository extends BaseRepository
|
|||
];
|
||||
}
|
||||
|
||||
// Rohstoffe je Formularblock (für Zuordnung 3).
|
||||
$rowsByBlock = [];
|
||||
foreach (($data['mfg_ingredient_id'] ?? []) as $rowIndex => $ingredientId) {
|
||||
$blockIndex = (int) (($data['mfg_row_phase'] ?? [])[$rowIndex] ?? 0);
|
||||
if ((int) $ingredientId > 0) {
|
||||
$rowsByBlock[$blockIndex][] = (int) $ingredientId;
|
||||
}
|
||||
}
|
||||
|
||||
$unmatched = $existing->keyBy('id');
|
||||
$assigned = [];
|
||||
|
||||
// 1. übermittelte IDs
|
||||
foreach ($blocks as $index => $block) {
|
||||
$id = (int) ($submittedIds[$index] ?? 0);
|
||||
if ($id > 0 && $unmatched->has($id)) {
|
||||
$assigned[$index] = $unmatched->get($id);
|
||||
$unmatched->forget($id);
|
||||
}
|
||||
}
|
||||
// 2. gleicher Name
|
||||
foreach ($blocks as $index => $block) {
|
||||
if (isset($assigned[$index])) {
|
||||
continue;
|
||||
}
|
||||
$match = $unmatched->first(fn (RecipePhase $p) => $p->name === $block['name']);
|
||||
if ($match !== null) {
|
||||
$assigned[$index] = $match;
|
||||
$unmatched->forget($match->id);
|
||||
}
|
||||
}
|
||||
// 3. gemeinsame Rohstoffe (größte Überschneidung gewinnt)
|
||||
foreach ($blocks as $index => $block) {
|
||||
if (isset($assigned[$index])) {
|
||||
if (isset($assigned[$index]) || empty($rowsByBlock[$index])) {
|
||||
continue;
|
||||
}
|
||||
$next = $unmatched->first();
|
||||
if ($next !== null) {
|
||||
$assigned[$index] = $next;
|
||||
$unmatched->forget($next->id);
|
||||
$best = null;
|
||||
$bestOverlap = 0;
|
||||
foreach ($unmatched as $phase) {
|
||||
$overlap = count(array_intersect($rowsByBlock[$index], $previousRowsByPhase[(int) $phase->id] ?? []));
|
||||
if ($overlap > $bestOverlap) {
|
||||
$best = $phase;
|
||||
$bestOverlap = $overlap;
|
||||
}
|
||||
}
|
||||
if ($best !== null) {
|
||||
$assigned[$index] = $best;
|
||||
$unmatched->forget($best->id);
|
||||
}
|
||||
}
|
||||
$unmatched->each->delete();
|
||||
|
|
|
|||
|
|
@ -778,10 +778,10 @@
|
|||
$mfgBlocks = [];
|
||||
if (($mfgRowsByPhase[0] ?? collect())->isNotEmpty() || $mfgPhaseList->isEmpty()) {
|
||||
// Altbestand ohne Phase bzw. leere Rezeptur => impliziter erster Block
|
||||
$mfgBlocks[] = ['name' => '', 'note' => '', 'rows' => $mfgRowsByPhase[0] ?? collect()];
|
||||
$mfgBlocks[] = ['id' => null, 'name' => '', 'note' => '', 'rows' => $mfgRowsByPhase[0] ?? collect()];
|
||||
}
|
||||
foreach ($mfgPhaseList as $mfgPhase) {
|
||||
$mfgBlocks[] = ['name' => $mfgPhase->name, 'note' => $mfgPhase->note, 'rows' => $mfgRowsByPhase[$mfgPhase->id] ?? collect()];
|
||||
$mfgBlocks[] = ['id' => $mfgPhase->id, 'name' => $mfgPhase->name, 'note' => $mfgPhase->note, 'rows' => $mfgRowsByPhase[$mfgPhase->id] ?? collect()];
|
||||
}
|
||||
@endphp
|
||||
|
||||
|
|
@ -790,6 +790,8 @@
|
|||
<div class="card mb-3 mfg-phase-block">
|
||||
<div class="card-header d-flex align-items-center py-2">
|
||||
<span class="text-muted mfg-phase-drag-handle mr-2" style="cursor:grab" title="{{ __('Phase verschieben') }}">☰</span>
|
||||
{{-- R-01: stabile Phasen-ID — Umbenennen/Umordnen/Einfügen wechselt die Identität nicht --}}
|
||||
<input type="hidden" name="mfg_phase_id[]" class="mfg-phase-id" value="{{ $block['id'] ?? '' }}">
|
||||
<input type="text" name="mfg_phase_name[]" class="form-control form-control-sm mfg-phase-name"
|
||||
style="max-width: 16rem" placeholder="{{ __('Phase') }} {{ chr(65 + $blockIndex) }}"
|
||||
value="{{ $block['name'] }}" autocomplete="off">
|
||||
|
|
@ -920,6 +922,7 @@
|
|||
<div class="card mb-3 mfg-phase-block">
|
||||
<div class="card-header d-flex align-items-center py-2">
|
||||
<span class="text-muted mfg-phase-drag-handle mr-2" style="cursor:grab" title="{{ __('Phase verschieben') }}">☰</span>
|
||||
<input type="hidden" name="mfg_phase_id[]" class="mfg-phase-id" value="">
|
||||
<input type="text" name="mfg_phase_name[]" class="form-control form-control-sm mfg-phase-name"
|
||||
style="max-width: 16rem" placeholder="{{ __('Phase') }}" value="" autocomplete="off">
|
||||
<a class="text-danger ml-auto mfg-phase-remove" href="#" title="{{ __('Phase entfernen') }}"><i class="far fa-trash-alt"></i></a>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue