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 auto‐audited by OwenIt) $entry->update(array_merge( array_filter($entryForm, fn($v) => !is_null($v)), ['updated_by'=> auth()->id() ?? 1] )); // 4) Manual pivot‐audit: // 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 don’t 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, ]); } }