vat_wms/app/Http/Controllers/Api/StockEntryController.php
2025-06-02 07:36:24 +02:00

409 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\OriginCountry;
use App\Models\StockEntry;
use App\Models\StockEntrySection;
use App\Models\StockEntryStatus;
use App\Models\StockPosition;
use App\Models\PhysicalItem;
use App\Models\StockSection;
use App\Models\Supplier;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use OwenIt\Auditing\Models\Audit;
class StockEntryController extends Controller
{
/**
* Display a paginated listing of stock entries.
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function index(Request $request)
{
$query = StockEntry::query()
->with(['physicalItem', 'supplier', 'sections', 'stockBatch', 'physicalItem.manufacturer', 'countryOfOrigin']);
// Apply filters if provided
if ($request->has('search')) {
$search = $request->search;
$query->whereHas('physicalItem', function($q) use ($search) {
$q->where('name', 'like', "%{$search}%");
});
}
// Sort
$sortField = $request->input('sort_field', 'updated_at');
$sortDirection = $request->input('sort_direction', 'desc');
$query->orderBy($sortField, $sortDirection);
// Paginate
$perPage = $request->input('per_page', 10);
$page = $request->input('page', 1);
$entries = $query->paginate($perPage, ['*'], 'page', $page);
return response()->json([
'data' => $entries->items(),
'meta' => [
'total' => $entries->total(),
'per_page' => $entries->perPage(),
'current_page' => $entries->currentPage(),
'last_page' => $entries->lastPage(),
],
]);
}
public function fetchOnTheWay(Request $request)
{
$query = StockEntry::query()
->with(['physicalItem', 'supplier', 'sections', 'stockBatch', 'physicalItem.manufacturer', 'countryOfOrigin'])->where('on_the_way', 1);
// Paginate
$perPage = $request->input('per_page', 10);
$page = $request->input('page', 1);
$entries = $query->paginate($perPage, ['*'], 'page', $page);
return response()->json([
'data' => $entries->items(),
'meta' => [
'total' => $entries->total(),
'per_page' => $entries->perPage(),
'current_page' => $entries->currentPage(),
'last_page' => $entries->lastPage(),
],
]);
}
public function addData(Request $request)
{
// build base rules
$rules = [
'physical_item_id' => 'required|integer|exists:vat_warehouse.physical_item,id',
'supplier_id' => 'required|integer|exists:vat_warehouse.supplier,id',
'count' => 'required|integer|min:0',
'price' => 'nullable|numeric|min:0',
'bought' => 'nullable|date',
'description' => 'nullable|string',
'note' => 'nullable|string',
'stock_batch_id' => 'nullable|integer|exists:stock_batch,id',
'country_of_origin_id' => 'required|integer|exists:vat_warehouse.country_of_origin,id',
'on_the_way' => 'boolean',
];
// condition for requiring section + count
$needsSection = function() use ($request) {
return ! $request->boolean('on_the_way')
&& !is_null($request->input('stock_batch_id'));
};
// add conditional rules
$rules['stock_position_id'] = [
Rule::requiredIf($needsSection),
'integer',
'exists:stock_section,section_id'
];
$rules['section_count'] = [
Rule::requiredIf($needsSection),
'integer',
'min:1'
];
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
return response()->json(['errors' => $validator->errors()], 422);
}
// 1) create the main stock entry
$entry = StockEntry::create($request->only([
'physical_item_id',
'supplier_id',
'count',
'price',
'bought',
'description',
'note',
'country_of_origin_id',
'on_the_way',
'stock_batch_id',
]) + [
'created_by' => auth()->id() ?? 1,
]);
// 2) only attach to section pivot if needed
if ($needsSection()) {
$entry->sections()->attach(
$request->input('stock_position_id'),
['count' => $request->input('section_count')]
);
}
// 3) eager-load relations (including the full address hierarchy)
$entry->load([
'physicalItem',
'supplier',
'stockBatch',
'sections.position.shelf.rack.line.room',
]);
return response()->json([
'message' => 'Stock entry created successfully',
'data' => $entry,
], 201);
}
/**
* Display the specified stock entry.
*
* @param int $id
* @return \Illuminate\Http\JsonResponse
*/
public function show($id)
{
$entry = StockEntry::with(['physicalItem', 'supplier', 'stockPosition', 'stockBatch'])
->findOrFail($id);
return response()->json(['data' => $entry]);
}
/**
* Update the specified stock entry.
*
* @param Request $request
* @param int $id
* @return \Illuminate\Http\JsonResponse
*/
public function updateData(Request $request, $id)
{
// 1) pull apart
$entryForm = $request->input('entryForm', []);
$sections = $request->input('sections', []);
// 2) validation rules as before…
$rules = [
'entryForm.physical_item_id' => ['sometimes','integer','exists:vat_warehouse.physical_item,id'],
'entryForm.supplier_id' => ['sometimes','integer','exists:vat_warehouse.supplier,id'],
'entryForm.count' => ['sometimes','integer','min:0'],
'entryForm.price' => ['nullable','numeric','min:0'],
'entryForm.bought' => ['nullable','date'],
'entryForm.description' => ['nullable','string'],
'entryForm.note' => ['nullable','string'],
'entryForm.stock_batch_id' => ['nullable','integer','exists:stock_batch,id'],
'entryForm.country_of_origin_id' => ['sometimes','integer','exists:vat_warehouse.country_of_origin,id'],
'entryForm.on_the_way' => ['sometimes','boolean'],
];
// determine if sections are required
$needsSection = function() use ($request) {
return ! $request->input('entryForm.on_the_way', false)
&& ! is_null($request->input('entryForm.stock_batch_id'));
};
// validate sections array if needed
$rules['sections'] = [Rule::requiredIf($needsSection), 'array'];
$rules['sections.*.stock_position_id'] = [Rule::requiredIf($needsSection), 'integer', 'exists:stock_section,section_id'];
$rules['sections.*.count'] = [Rule::requiredIf($needsSection), 'integer', 'min:1'];
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
return response()->json(['errors'=>$validator->errors()], 422);
}
/** @var StockEntry $entry */
$entry = StockEntry::with('sections')->findOrFail($id);
// 3) Update the main row (this is autoaudited by OwenIt)
$entry->update(array_merge(
array_filter($entryForm, fn($v) => !is_null($v)),
['updated_by'=> auth()->id() ?? 1]
));
// 4) Manual pivotaudit:
// a) snapshot what was there
$oldPivots = $entry->sections
->pluck('pivot.count','pivot.section_id')
->toArray(); // [ section_id => oldCount, … ]
// b) perform the sync
$syncData = [];
foreach ($sections as $sec) {
if (!empty($sec['stock_position_id']) && !empty($sec['count'])) {
$syncData[(int)$sec['stock_position_id']] = ['count'=>(int)$sec['count']];
}
}
// $entry->sections()->sync($syncData);
// b) perform the sync, but dont let the AuditableObserver fire here…
StockEntrySection::withoutAuditing(function() use ($entry, $syncData) {
$entry->sections()->sync($syncData);
});
// c) now compare old↔new and write audit rows
$userId = auth()->id() ?? null;
foreach ($syncData as $sectionId => $pivotData) {
$newCount = $pivotData['count'];
if (! array_key_exists($sectionId, $oldPivots)) {
// *** attached ***
Audit::create([
'user_id' => $userId,
'auditable_type'=> StockEntrySection::class,
'auditable_id' => $entry->id, // pivot has no single PK; we use entry ID
'event' => 'created',
'old_values' => [],
'new_values' => ['section_id'=>$sectionId,'count'=>$newCount],
]);
} elseif ($oldPivots[$sectionId] !== $newCount) {
// *** updated ***
Audit::create([
'user_id' => $userId,
'auditable_type'=> StockEntrySection::class,
'auditable_id' => $entry->id,
'event' => 'updated',
'old_values' => ['count'=>$oldPivots[$sectionId]],
'new_values' => ['count'=>$newCount],
]);
}
}
// d) any removed?
foreach (array_diff_key($oldPivots, $syncData) as $sectionId => $oldCount) {
Audit::create([
'user_id' => $userId,
'auditable_type'=> StockEntrySection::class,
'auditable_id' => $entry->id,
'event' => 'deleted',
'old_values' => ['section_id'=>$sectionId,'count'=>$oldCount],
'new_values' => [],
]);
}
// 5) reload and return
$entry->load([
'physicalItem',
'supplier',
'stockBatch',
'sections.position.shelf.rack.line.room',
]);
return response()->json([
'message'=>'Stock entry updated successfully',
'data' =>$entry,
]);
}
/**
* Remove the specified stock entry.
*
* @param int $id
* @return \Illuminate\Http\JsonResponse
*/
public function destroy($id)
{
$entry = StockEntry::findOrFail($id);
$entry->delete();
return response()->json([
'message' => 'Stock entry deleted successfully'
]);
}
/**
* Get options for dropdown lists.
*
* @return \Illuminate\Http\JsonResponse
*/
public function getOptions()
{
$stockPositions = StockSection::with('position.shelf.rack.line.room')
->get()
->map(function (StockSection $section) {
$pos = $section->position;
$shelf = $pos->shelf;
$rack = $shelf->rack;
$line = $rack->line;
$room = $line->room;
return [
'id' => $section->section_id,
'name' => sprintf(
'%s-%s-%s-%s-%s-%s',
$room->room_symbol,
$line->line_symbol,
$rack->rack_symbol,
$shelf->shelf_symbol,
$pos->position_symbol,
$section->section_symbol
),
'capacity' => $section->capacity,
'occupied' => $section->occupied()
];
});
// Get suppliers from warehouse DB
$suppliers = Supplier::select('id', 'name')->get();
// Get physical items from warehouse DB
$countriesOrigin = OriginCountry::select('id', 'code as name')->get();
return response()->json([
'stockPositions' => $stockPositions,
'suppliers' => $suppliers,
'countriesOrigin' => $countriesOrigin,
]);
}
/**
* Get options for dropdown lists.
*
* @return \Illuminate\Http\JsonResponse
*/
public function getItems(Request $request)
{
// Get physical items from warehouse DB
$physicalItems = PhysicalItem::select('id', 'name')
->where('name', 'like', '%' . $request->input('item_name', '') . '%')->limit(100)
->get();
return response()->json([
'physicalItems' => $physicalItems,
]);
}
public function audit(Request $request, $id)
{
// 1) Load the entry (so we can get its audits)
$entry = StockEntry::findOrFail($id);
// 2) Get audits for the entry model itself
$entryAudits = $entry->audits()->orderBy('created_at')->get();
// 3) Get audits for all pivot changes you logged under the pivot model
$pivotAudits = Audit::query()
->where('auditable_type', StockEntrySection::class)
->where('auditable_id', $id) // we used entry->id when creating these
->orderBy('created_at')
->get();
return response()->json([
'entry_audits' => $entryAudits,
'pivot_audits' => $pivotAudits,
]);
}
public function getStatusList(Request $request)
{
$entry = StockEntryStatus::all();
return response()->json([
'statuses' => $entry,
]);
}
}