diff --git a/app/Http/Controllers/App/InventoryController.php b/app/Http/Controllers/App/InventoryController.php index 9a93c6e..94c4967 100644 --- a/app/Http/Controllers/App/InventoryController.php +++ b/app/Http/Controllers/App/InventoryController.php @@ -8,6 +8,7 @@ use App\Services\ProductService; use App\Imports\ProductsImport; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; use Notsoweb\ApiResponse\Enums\ApiResponse; use Maatwebsite\Excel\Facades\Excel; use Maatwebsite\Excel\Validators\ValidationException; @@ -27,6 +28,7 @@ public function index(Request $request) ->where('is_active', true); + // Filtro por búsqueda de texto (nombre, SKU, código de barras) if ($request->has('q') && $request->q) { $products->where(function($query) use ($request) { $query->where('name', 'like', "%{$request->q}%") @@ -35,11 +37,22 @@ public function index(Request $request) }); } + // Filtro por categoría (independiente de la búsqueda de texto) + if ($request->has('category_id') && $request->category_id) { + $products->where('category_id', $request->category_id); + } + + // Calcular el valor total del inventario + $totalInventoryValue = Inventory::join('prices', 'inventories.id', '=', 'prices.inventory_id') + ->where('inventories.is_active', true) + ->sum(DB::raw('inventories.stock * prices.cost')); + $products = $products->orderBy('name') ->paginate(config('app.pagination')); return ApiResponse::OK->response([ - 'products' => $products + 'products' => $products, + 'total_inventory_value' => round($totalInventoryValue, 2) ]); } diff --git a/app/Http/Controllers/App/ReportController.php b/app/Http/Controllers/App/ReportController.php index 561d7a6..d1f1277 100644 --- a/app/Http/Controllers/App/ReportController.php +++ b/app/Http/Controllers/App/ReportController.php @@ -61,11 +61,9 @@ public function topSellingProduct(Request $request) public function productsWithoutMovement(Request $request) { $request->validate([ - 'include_stock_value' => ['nullable', 'boolean'], 'from_date' => ['required', 'date_format:Y-m-d'], 'to_date' => ['required', 'date_format:Y-m-d', 'after_or_equal:from_date'], ], [ - 'include_stock_value.boolean' => 'El parámetro de valor de stock debe ser verdadero o falso.', 'from_date.required' => 'La fecha inicial es obligatoria.', 'from_date.date_format' => 'La fecha inicial debe tener el formato Y-m-d (ejemplo: 2025-01-01).', 'to_date.required' => 'La fecha final es obligatoria.', @@ -75,14 +73,12 @@ public function productsWithoutMovement(Request $request) try { $products = $this->reportService->getProductsWithoutMovement( - includeStockValue: (bool)($request->input('include_stock_value', true)), fromDate: $request->input('from_date'), toDate: $request->input('to_date') ); return ApiResponse::OK->response([ 'products' => $products, - 'total_products' => count($products), 'message' => 'Reporte de productos sin movimiento generado exitosamente.' ]); } catch (\Exception $e) { diff --git a/app/Models/Inventory.php b/app/Models/Inventory.php index 1b9455f..3dcfe13 100644 --- a/app/Models/Inventory.php +++ b/app/Models/Inventory.php @@ -30,7 +30,7 @@ class Inventory extends Model 'track_serials' => 'boolean', ]; - protected $appends = ['has_serials']; + protected $appends = ['has_serials', 'inventory_value']; public function category() { @@ -78,4 +78,12 @@ public function getHasSerialsAttribute(): bool { return isset($this->attributes['serials_count']) && $this->attributes['serials_count'] > 0; } + + /** + * Calcular el valor total del inventario para este producto (stock * costo) + */ + public function getInventoryValueAttribute(): float + { + return $this->stock * ($this->price?->cost ?? 0); + } } diff --git a/app/Services/ReportService.php b/app/Services/ReportService.php index 37cb3b4..0f35707 100644 --- a/app/Services/ReportService.php +++ b/app/Services/ReportService.php @@ -51,12 +51,8 @@ public function getTopSellingProduct(?string $fromDate = null, ?string $toDate = /** * Obtener productos sin movimiento - * - * @param int $daysThreshold Días mínimos sin movimiento (default: 30) - * @param bool $includeStockValue Incluir valor del inventario (default: true) - * @return array Lista de productos sin ventas */ - public function getProductsWithoutMovement(bool $includeStockValue = true, ?string $fromDate = null, ?string $toDate = null): array + public function getProductsWithoutMovement(?string $fromDate = null, ?string $toDate = null) { // Obtener IDs de productos que SÍ tienen ventas $inventoriesWithSales = SaleDetail::query() @@ -68,33 +64,13 @@ public function getProductsWithoutMovement(bool $includeStockValue = true, ?stri ->pluck('sale_details.inventory_id') ->toArray(); - // Construir query para productos SIN ventas + // Construir query para productos SIN ventas usando relaciones de Eloquent $query = Inventory::query() - ->selectRaw(' - inventories.id, - inventories.name, - inventories.sku, - inventories.stock, - categories.name as category_name, - inventories.created_at as date_added, - prices.retail_price - '); + ->with(['category', 'price']) + ->where('is_active', true) + ->whereNotIn('id', $inventoriesWithSales) + ->orderBy('created_at'); - // Agregar valor del inventario si se solicita - if ($includeStockValue) { - $query->addSelect( - DB::raw('(inventories.stock * COALESCE(prices.cost, 0)) as inventory_value') - ); - } - - return $query - ->leftJoin('categories', 'inventories.category_id', '=', 'categories.id') - ->leftJoin('prices', 'inventories.id', '=', 'prices.inventory_id') - ->where('inventories.is_active', true) - ->whereNull('inventories.deleted_at') - ->whereNotIn('inventories.id', $inventoriesWithSales) - ->orderBy('inventories.created_at') - ->get() - ->toArray(); + return $query->paginate(config('app.pagination', 10)); } }