From 956b2469f9408fc64faf3d07c605c4e59a545b89 Mon Sep 17 00:00:00 2001 From: gianella Date: Sat, 4 Apr 2026 15:14:28 -0300 Subject: [PATCH] g --- app/Console/Commands/ImportarProductosCsv.php | 107 ++++++++++++++++++ app/Http/Controllers/CatalogoController.php | 22 +++- app/Http/Controllers/ProductosController.php | 59 ++++------ ...025_12_06_182358_create_products_table.php | 10 +- database/seeders/ProductosInicialesSeeder.php | 85 ++++++++++++++ resources/views/catalogo/index.blade.php | 94 ++++++++++++--- 6 files changed, 316 insertions(+), 61 deletions(-) create mode 100644 app/Console/Commands/ImportarProductosCsv.php create mode 100644 database/seeders/ProductosInicialesSeeder.php diff --git a/app/Console/Commands/ImportarProductosCsv.php b/app/Console/Commands/ImportarProductosCsv.php new file mode 100644 index 0000000..ee6d3c4 --- /dev/null +++ b/app/Console/Commands/ImportarProductosCsv.php @@ -0,0 +1,107 @@ +info("Iniciando importación masiva de Lauck..."); + + // Definimos los archivos, el tipo de producto, y cuántas filas saltar (los encabezados) + $archivos = [ + [ + 'ruta' => 'imports/LISTA DE PRECIOS.xlsx - LISTA.csv', + 'tipo' => 'accessory', + 'saltar' => 1 // Asumimos 1 fila de encabezado + ], + [ + 'ruta' => 'imports/LISTA DE PRECIOS.xlsx - BICICLETAS.csv', + 'tipo' => 'bike', + 'saltar' => 11 // Metadatos dicen que hay ~10 filas arriba del encabezado + ], + [ + 'ruta' => 'imports/LISTA DE PRECIOS.xlsx - INDUMENTARIA.csv', + 'tipo' => 'clothing', + 'saltar' => 8 // Metadatos dicen que hay ~7 filas arriba + ], + ]; + + foreach ($archivos as $archivo) { + $this->procesarCsv($archivo['ruta'], $archivo['tipo'], $archivo['saltar']); + } + + $this->info("¡Importación finalizada con éxito!"); + } + + private function procesarCsv($rutaArchivo, $tipoProducto, $filasASaltar) + { + // Verificamos si el archivo existe en storage/app/imports/ + if (!Storage::exists($rutaArchivo)) { + $this->error("No se encontró el archivo: {$rutaArchivo}"); + return; + } + + $this->line("Procesando: {$rutaArchivo}..."); + + // Abrimos el archivo + $rutaCompleta = storage_path('app/' . $rutaArchivo); + $file = fopen($rutaCompleta, 'r'); + + // 1. Saltamos las filas "basura" del principio + for ($i = 0; $i < $filasASaltar; $i++) { + fgetcsv($file); + } + + $contador = 0; + + // 2. Leemos fila por fila (para no saturar la memoria RAM) + while (($fila = fgetcsv($file, 1000, ',')) !== false) { + // Ignorar filas completamente vacías + if (empty(array_filter($fila))) { + continue; + } + + // ========================================================= + // ¡ATENCIÓN ACÁ! Tenés que mapear las columnas de tu Excel. + // En programación, la primera columna (A) es la 0, la (B) es 1, etc. + // ========================================================= + + $sku = $fila[0] ?? null; // Ajustar número de columna + $name = $fila[1] ?? 'Sin Nombre'; // Ajustar número + + // Limpiamos los precios (sacar el signo $ y separar miles si es necesario) + $precioRaw = $fila[2] ?? 0; // Ajustar número + $precioLimpio = (float) filter_var($precioRaw, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION); + + $stock = $fila[3] ?? 0; // Ajustar número + + // 3. Guardar en la Base de Datos + // Usamos updateOrCreate para que si corrés el comando 2 veces, no duplique cosas + Product::updateOrCreate( + ['sku' => $sku], // Busca por SKU. Si existe, actualiza. Si no, crea. + [ + 'name' => $name, + 'price' => $precioLimpio, + 'stock_quantity' => (int) $stock, + 'type' => $tipoProducto, + 'min_stock_alert' => 5, // Valor por defecto + 'suppliers_id' => 1, // Valor por defecto + ] + ); + + $contador++; + } + + fclose($file); + $this->info("✔ {$contador} productos importados de tipo {$tipoProducto}."); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/CatalogoController.php b/app/Http/Controllers/CatalogoController.php index 82eecb7..cc7ca16 100644 --- a/app/Http/Controllers/CatalogoController.php +++ b/app/Http/Controllers/CatalogoController.php @@ -12,26 +12,36 @@ class CatalogoController extends Controller */ public function index(Request $request) { - // Consulta base + // --- 1. CONSULTA PARA LA GRILLA (PAGINADA) --- $query = Product::query(); - // Lógica del Buscador: Si recibimos algo en el input "search" + // Lógica del Buscador tradicional (por si seguís usando un input de texto además de Select2) if ($request->has('search')) { $searchTerm = $request->input('search'); $query->where(function($q) use ($searchTerm) { $q->where('name', 'like', "%{$searchTerm}%") - ->orWhere('sku', 'like', "%{$searchTerm}%"); + ->orWhere('sku', 'like', "%{$searchTerm}%"); }); } + + // Filtros base para la grilla $query->whereIn('type', ['bike', 'accessory', 'clothing', 'spare']); $query->where('stock_quantity', '>', 0); // Solo mostrar si tiene stock // Resultados paginados - $products = $query->paginate(12)->withQueryString(); // withQueryString mantiene la búsqueda al cambiar de página + $products = $query->paginate(12)->withQueryString(); - // Devuelve la vista - return view('catalogo.index', compact('products')); + // --- 2. CONSULTA PARA EL BUSCADOR SELECT2 (TODOS) --- + // Traemos todos los productos válidos para llenar el desplegable de búsqueda rápida. + // Hacemos la misma validación de stock y tipo para no mostrar cosas agotadas. + $allProducts = Product::whereIn('type', ['bike', 'accessory', 'clothing', 'spare']) + ->where('stock_quantity', '>', 0) + ->orderBy('name') // Ordenados alfabéticamente + ->get(); + + // Devuelve la vista pasando ambas variables + return view('catalogo.index', compact('products', 'allProducts')); } public function show(Product $product) diff --git a/app/Http/Controllers/ProductosController.php b/app/Http/Controllers/ProductosController.php index c7e37e7..3d78047 100644 --- a/app/Http/Controllers/ProductosController.php +++ b/app/Http/Controllers/ProductosController.php @@ -14,35 +14,32 @@ class ProductosController extends Controller */ public function index(Request $request) { - // Recuperamos lo escrito en el buscador (si aplica) $query = $request->input('search'); $status = $request->input('stock_status'); - // Construimos la consulta $products = Product::query() ->when($query, function ($q) use ($query) { - // Filtra por nombre o SKU - return $q->where('name', 'like', "%{$query}%") - ->orWhere('sku', 'like', "%{$query}%"); + // CORREGIDO: Filtramos solo por description + return $q->where('name', 'like', "%{$query}%"); }) ->when($status, function ($q) use ($status) { if ($status === 'low') { - // Rojo: Menor o igual a la alerta - return $q->whereColumn('stock_quantity', '<', 'min_stock_alert'); + return $q->whereColumn('stock_quantity', '<', 'min_stock_alert') + ->where('type', '!=', 'service'); } elseif ($status === 'medium') { - // Amarillo: Mayor al min y menor o igual al min + 5 return $q->whereColumn('stock_quantity', '>=', 'min_stock_alert') - ->whereRaw('stock_quantity <= (min_stock_alert + 5)'); + ->whereRaw('stock_quantity <= (min_stock_alert + 1)') + ->where('type', '!=', 'service'); } elseif ($status === 'ok') { - // Verde: Stock saludable - return $q->whereRaw('stock_quantity > (min_stock_alert + 5)'); + return $q->whereRaw('stock_quantity > (min_stock_alert + 1)') + ->where('type', '!=', 'service'); } }) - ->orderBy('stock_quantity', 'asc') // Ordenamos primero los que tienen poco stock (Alerta visual) - ->paginate(10) // Paginamos de a 10 - ->withQueryString(); // Mantiene el filtro de búsqueda al cambiar de página + ->orderBy('stock_quantity', 'asc') + ->paginate(10) + ->withQueryString(); return view('productos.index', compact('products')); } @@ -62,30 +59,21 @@ class ProductosController extends Controller { // 1. Validamos los datos con las nuevas columnas $validated = $request->validate([ - 'name' => 'required|string|max:255', - 'sku' => 'nullable|string|unique:products,sku|max:50', // SKU único - 'description' => 'nullable|string', + 'name' => 'required|string|max:255', // CORREGIDO: description en lugar de name 'price' => 'required|numeric|min:0', - 'cost' => 'nullable|numeric|min:0', // Costo opcional + 'cost' => 'nullable|numeric|min:0', 'stock_quantity' => 'required|integer|min:0', 'min_stock_alert' => 'required|integer|min:0', - 'type' => 'required|in:bike,accessory,clothing,spare', // Solo permite estos valores + 'type' => 'required|in:bike,accessory,clothing,spare,service', // CORREGIDO: agregados nuevos tipos 'serial_number' => 'nullable|string|max:100', 'image' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048', ]); - // 2. Si no viene SKU, generamos uno automático (Opcional pero útil) - if (empty($validated['sku'])) { - $validated['sku'] = 'GEN-' . strtoupper(uniqid()); - } - - // 3. Validacion de imagenes + // 2. Validacion de imagenes if ($request->hasFile('image')) { - // Guarda el archivo en storage/app/public/products y devuelve la ruta $path = $request->file('image')->store('products', 'public'); $validated['image_path'] = $path; } - unset($validated['image']); // Para no romper la logica del supplier @@ -94,7 +82,7 @@ class ProductosController extends Controller // 3. Creamos el producto Product::create($validated); - // 4. Redireccionamos con mensaje de éxito (Necesitas el componente Alert en el layout) + // 4. Redireccionamos return redirect()->route('productos.index') ->with('success', 'Producto creado correctamente.'); } @@ -121,34 +109,27 @@ class ProductosController extends Controller public function update(Request $request, Product $product) { $validated = $request->validate([ - 'name' => 'required|string|max:255', - // Validamos que el SKU sea único PERO ignoramos el ID de este producto actual - 'sku' => ['nullable', 'string', Rule::unique('products')->ignore($product->id)], - 'description' => 'nullable|string', + 'name' => 'required|string|max:255', // CORREGIDO + 'type' => 'required|in:bike,accessory,clothing,spare,service', // CORREGIDO 'price' => 'required|numeric|min:0', 'cost' => 'nullable|numeric|min:0', 'stock_quantity' => 'required|integer|min:0', 'min_stock_alert' => 'required|integer|min:0', - 'type' => 'required|in:bike,accessory,clothing,spare', 'serial_number' => 'nullable|string|max:100', - 'image' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048', ]); // 2. Manejo de imagen al actualizar if ($request->hasFile('image')) { - // Borrar la imagen anterior if ($product->image_path) { Storage::disk('public')->delete($product->image_path); } - // Guardar la nueva $path = $request->file('image')->store('products', 'public'); $validated['image_path'] = $path; } unset($validated['image']); - // Para no romper la logica del supplier $validated['suppliers_id'] = 1; $product->update($validated); @@ -162,6 +143,10 @@ class ProductosController extends Controller */ public function destroy(Product $product) { + if ($product->image_path) { + Storage::disk('public')->delete($product->image_path); + } + $product->delete(); return redirect()->route('productos.index') ->with('success', 'Producto eliminado.'); diff --git a/database/migrations/2025_12_06_182358_create_products_table.php b/database/migrations/2025_12_06_182358_create_products_table.php index 326973c..51a2f95 100644 --- a/database/migrations/2025_12_06_182358_create_products_table.php +++ b/database/migrations/2025_12_06_182358_create_products_table.php @@ -13,18 +13,18 @@ return new class extends Migration { Schema::create('products', function (Blueprint $table) { $table->id(); - $table->enum('type', ['bike', 'accessory', 'clothing', 'service']); + $table->enum('type', ['bike', 'accessory', 'clothing', 'spare']); // Tipo de producto //$table->string('name'); //$table->string('sku')->unique()->nullable(); // Código de barras o interno - $table->text('description')->nullable(); + $table->text('name')->nullable(); $table->decimal('price', 10, 2); // Precio venta $table->decimal('cost', 10, 2)->nullable(); // Costo (solo admin) - $table->integer('stock_quantity')->default(0); - $table->integer('min_stock_alert'); // Alerta + $table->integer('stock_quantity')->default(100); + $table->integer('min_stock_alert')->default(5); // Alerta + - $table->enum('type', ['bike', 'accessory', 'clothing', 'spare']); // Tipo de producto $table->string('serial_number')->nullable(); // Solo para bicis $table->foreignId('suppliers_id')->constrained()->default(1); diff --git a/database/seeders/ProductosInicialesSeeder.php b/database/seeders/ProductosInicialesSeeder.php new file mode 100644 index 0000000..25cdd60 --- /dev/null +++ b/database/seeders/ProductosInicialesSeeder.php @@ -0,0 +1,85 @@ + 1], + [ + 'name' => 'Proveedor 1', + 'phone' => '123456789', + 'email' => 'proveedor1@example.com' + ] + ); + + // 2. Tus datos convertidos a un array nativo de PHP + $productos = [ + ['type' => 'clothing', 'name' => 'MALLAS CORTAS indubike', 'cost' => 3920, 'price' => 4900], + ['type' => 'clothing', 'name' => 'Malla corta ITOKU', 'cost' => 3920, 'price' => 4900], + ['type' => 'clothing', 'name' => 'MALLAS CON TIRADORES', 'cost' => 4640, 'price' => 5800], + ['type' => 'clothing', 'name' => 'MALLA SQUADRA', 'cost' => 4640, 'price' => 5800], + ['type' => 'clothing', 'name' => 'CALZA LARGA C/BADANA TERMICA', 'cost' => 3280, 'price' => 4100], + ['type' => 'clothing', 'name' => 'CALZA LARGA S/ BADANA', 'cost' => 3280, 'price' => 4100], + ['type' => 'clothing', 'name' => 'ABRIGO DE PIERNAS', 'cost' => 1504, 'price' => 1880], + ['type' => 'clothing', 'name' => 'CHALECO ITOKU', 'cost' => 1520, 'price' => 1900], + ['type' => 'clothing', 'name' => 'Camiseta argentina azul', 'cost' => 1680, 'price' => 2100], + ['type' => 'clothing', 'name' => 'Camiseta itoko', 'cost' => 1680, 'price' => 2100], + ['type' => 'clothing', 'name' => 'CAMISETA INDUBIKE', 'cost' => 1680, 'price' => 2100], + ['type' => 'clothing', 'name' => 'CAMISETA SIDI', 'cost' => 3120, 'price' => 3900], + ['type' => 'clothing', 'name' => 'CAMISETA LIMA', 'cost' => 3120, 'price' => 3900], + ['type' => 'clothing', 'name' => 'CAMPERA SIDI INDUBIQUE', 'cost' => 3040, 'price' => 3800], + ['type' => 'clothing', 'name' => 'CAMPERA SQUADRA', 'cost' => 3040, 'price' => 3800], + ['type' => 'clothing', 'name' => 'CAMPERAS solbike', 'cost' => 3040, 'price' => 3800], + ['type' => 'clothing', 'name' => 'CAMPERA ITUKO', 'cost' => 3040, 'price' => 3800], + ['type' => 'clothing', 'name' => 'campera licra especial indubike', 'cost' => 3040, 'price' => 3800], + ['type' => 'clothing', 'name' => 'ROMPEBIENTO ITUKO', 'cost' => 1520, 'price' => 1900], + ['type' => 'clothing', 'name' => 'REMERA TERMICA NEGRA', 'cost' => 1520, 'price' => 1900], + ['type' => 'clothing', 'name' => 'GUANTES LICRA ECONOMICOS', 'cost' => 1520, 'price' => 1900], + ['type' => 'clothing', 'name' => 'GUANTES EXTREMO', 'cost' => 2240, 'price' => 2800], + ['type' => 'clothing', 'name' => 'GUANTES', 'cost' => 2240, 'price' => 2800], + ['type' => 'clothing', 'name' => 'GUANTES COACH', 'cost' => 2240, 'price' => 2800], + ['type' => 'clothing', 'name' => 'GUANTES DE NIÑO RECREO', 'cost' => 2240, 'price' => 2800], + ['type' => 'clothing', 'name' => 'GUANTES VAIRO', 'cost' => 2240, 'price' => 2800], + ['type' => 'clothing', 'name' => 'GUANTES CON DEDOS', 'cost' => 2640, 'price' => 3300], + ['type' => 'accessory', 'name' => 'BOLSA BAJO ASIENTO EXT CHICA', 'cost' => 2640, 'price' => 3300], + ['type' => 'accessory', 'name' => 'BOLSA BAJO ASIENTO GRANDE', 'cost' => 2640, 'price' => 3300], + ['type' => 'accessory', 'name' => 'BOLSA TRIANGULO', 'cost' => 2640, 'price' => 3300], + ['type' => 'accessory', 'name' => 'PORTA CELULAR', 'cost' => 2640, 'price' => 3300], + ['type' => 'clothing', 'name' => 'CASCO ECONOMICO 10V', 'cost' => 2640, 'price' => 3300], + ['type' => 'clothing', 'name' => 'CASCO 20 V', 'cost' => 2640, 'price' => 3300], + ['type' => 'clothing', 'name' => 'CASCO 22V D CARRERA', 'cost' => 2640, 'price' => 3300], + ['type' => 'clothing', 'name' => 'CASCO DE NIÑO', 'cost' => 2640, 'price' => 3300], + ['type' => 'clothing', 'name' => 'casco masz1', 'cost' => 2640, 'price' => 3300], + ['type' => 'clothing', 'name' => 'Cascos de niña con rodilleras rosa', 'cost' => 2640, 'price' => 3300], + ['type' => 'clothing', 'name' => 'CASCO BMK ASFALTOCCCC', 'cost' => 2640, 'price' => 3300], + ['type' => 'clothing', 'name' => 'Casco de BMX roler/scate', 'cost' => 2640, 'price' => 3300], + ['type' => 'clothing', 'name' => 'Cascos de niño chico (3 años)', 'cost' => 2640, 'price' => 3300], + ['type' => 'accessory', 'name' => 'ALFORJA', 'cost' => 2640, 'price' => 3300], + ]; + + // 3. Insertamos usando updateOrCreate para no duplicar si lo corrés dos veces + foreach ($productos as $item) { + Product::updateOrCreate( + ['name' => $item['name']], // Busca por descripcion + [ + 'type' => $item['type'], + 'cost' => $item['cost'], + 'price' => $item['price'], + 'stock_quantity' => 100, // Inicia con stock por defecto + 'min_stock_alert' => 5, // Alerta por defecto + 'suppliers_id' => 1, + ] + ); + } + + $this->command->info('¡Productos y proveedor insertados correctamente!'); + } +} \ No newline at end of file diff --git a/resources/views/catalogo/index.blade.php b/resources/views/catalogo/index.blade.php index b50f89f..486a4b7 100644 --- a/resources/views/catalogo/index.blade.php +++ b/resources/views/catalogo/index.blade.php @@ -1,17 +1,66 @@ - + @push('styles') + + + @endpush + -
+
+ +
+ + +
@if ($products->count() > 0)
@foreach ($products as $product)
+ {{-- Imagen si existe --}} @if (!empty($product->image_path)) - {{ $product->name }} @else
@@ -19,23 +68,15 @@
@endif - {{-- Nombre --}}

{{ $product->name }}

- {{-- Descripción corta --}} -
- @if (!empty($product->description)) -

{{ $product->description }}

- @endif -
- {{-- Precio --}} @if (!empty($product->price))

${{ number_format($product->price, 2, ',', '.') }}

@endif {{-- Botón --}} - Ver + @@ -47,8 +88,35 @@

No hay productos disponibles.

@endif -
+
{{ $products->links() }}
+ + @push('scripts') + + + + + @endpush +