From 956b2469f9408fc64faf3d07c605c4e59a545b89 Mon Sep 17 00:00:00 2001 From: gianella Date: Sat, 4 Apr 2026 15:14:28 -0300 Subject: [PATCH 1/4] 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 + From d1467ca098897f6bcc8c8a6ed72985c6ca258582 Mon Sep 17 00:00:00 2001 From: gianella Date: Sat, 4 Apr 2026 20:57:46 -0300 Subject: [PATCH 2/4] importar productos creo que les tengo q pasar el excel y tienen que hacer php artisan migrate:fresh y php artisan importar:excel --- app/Console/Commands/ImportarProductosCsv.php | 107 ------------------ 1 file changed, 107 deletions(-) delete mode 100644 app/Console/Commands/ImportarProductosCsv.php diff --git a/app/Console/Commands/ImportarProductosCsv.php b/app/Console/Commands/ImportarProductosCsv.php deleted file mode 100644 index ee6d3c4..0000000 --- a/app/Console/Commands/ImportarProductosCsv.php +++ /dev/null @@ -1,107 +0,0 @@ -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 From b4476431e4309cafdd5d3a2d5ac54c792053685c Mon Sep 17 00:00:00 2001 From: gianella Date: Sat, 4 Apr 2026 20:59:30 -0300 Subject: [PATCH 3/4] php artisan migrate:fresh y php artisan importar:excel, les tengo que pasar el archivo csv --- app/Console/Commands/ImportarExcelLimpio.php | 83 +++++++++++++++++++ app/Http/Controllers/CatalogoController.php | 42 +++++----- app/Http/Controllers/ProductosController.php | 19 +++-- app/Http/Controllers/TallerController.php | 4 +- app/Models/Supplier.php | 2 +- ...25_12_06_182356_create_suppliers_table.php | 2 +- ...025_12_06_182358_create_products_table.php | 2 +- resources/views/catalogo/index.blade.php | 58 +++++++++++-- resources/views/productos/index.blade.php | 73 ++++++++++++---- 9 files changed, 224 insertions(+), 61 deletions(-) create mode 100644 app/Console/Commands/ImportarExcelLimpio.php diff --git a/app/Console/Commands/ImportarExcelLimpio.php b/app/Console/Commands/ImportarExcelLimpio.php new file mode 100644 index 0000000..ef2e364 --- /dev/null +++ b/app/Console/Commands/ImportarExcelLimpio.php @@ -0,0 +1,83 @@ +info("Iniciando importación del Excel limpio..."); + + // 1. Asegurarnos de que existe el proveedor comodín (para evitar el error de la llave foránea) + Supplier::firstOrCreate( + ['id' => 1], + [ + 'name' => 'Proveedor General Lauck', + 'phone' => '0000000000' + ] + ); + + $rutaCompleta = storage_path('app/imports/catalogo_limpio.csv'); + + if (!file_exists($rutaCompleta)) { + $this->error("No se encontró el archivo en: " . $rutaCompleta); + return; + } + $file = fopen($rutaCompleta, 'r'); + + // Saltamos la primera fila (porque son los títulos: tipo, descripcion, costo, precio) + fgetcsv($file); + + $contador = 0; + + while (($fila = fgetcsv($file, 1000, ',')) !== false) { + + // Asignamos las columnas del Excel (0 a 3) + $tipoRaw = strtolower(trim($fila[0] ?? '')); + $descripcion = trim($fila[1] ?? ''); + $costoRaw = trim($fila[2] ?? ''); + $precioRaw = trim($fila[3] ?? ''); + + // Si la descripción está vacía, saltamos la fila + if (empty($descripcion)) { + continue; + } + + // --- Lógica de limpieza de Tipos --- + // Si en el Excel dice "bicicleta", lo guardamos como "bike" para mantener el estándar. + $tipoFinal = ($tipoRaw === 'bicicleta') ? 'bike' : $tipoRaw; + + // --- Lógica de limpieza de Precios ("$ 7,200" -> 7200.0) --- + // Borramos el signo peso, los espacios y las comas de los miles + $costoLimpio = (float) str_replace(['$', ' ', ','], '', $costoRaw); + $precioLimpio = (float) str_replace(['$', ' ', ','], '', $precioRaw); + + // Guardamos en la base de datos + Product::updateOrCreate( + ['name' => $descripcion], // Buscamos por descripción + [ + 'type' => $tipoFinal, + 'cost' => $costoLimpio, + 'price' => $precioLimpio, + 'stock_quantity' => 20, // Al ser lista de precios, entra con stock 0 + 'min_stock_alert' => 5, + 'suppliers_id' => 1, + ] + ); + + $contador++; + } + + fclose($file); + $this->info("¡Éxito! Se importaron/actualizaron {$contador} productos."); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/CatalogoController.php b/app/Http/Controllers/CatalogoController.php index cc7ca16..e72942b 100644 --- a/app/Http/Controllers/CatalogoController.php +++ b/app/Http/Controllers/CatalogoController.php @@ -8,39 +8,39 @@ use Illuminate\Http\Request; class CatalogoController extends Controller { /** - * Muestra los registros de ventas con buscador y paginación. + * Muestra el catálogo público de productos. */ public function index(Request $request) { - // --- 1. CONSULTA PARA LA GRILLA (PAGINADA) --- - $query = Product::query(); + // 1. Atrapamos lo que el usuario escribió o seleccionó + $query = $request->input('search'); + $type = $request->input('type'); - // 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'); + // --- CONSULTA PARA LA GRILLA (PAGINADA) --- + $productsQuery = Product::query(); - $query->where(function($q) use ($searchTerm) { - $q->where('name', 'like', "%{$searchTerm}%") - ->orWhere('sku', 'like', "%{$searchTerm}%"); - }); + $productsQuery->when($query, function ($q) use ($query) { + return $q->where('name', 'like', "%{$query}%"); + }); + + if ($type) { + $productsQuery->where('type', $type); + } else { + $productsQuery->where('type', '!=', 'service'); } - // Filtros base para la grilla - $query->whereIn('type', ['bike', 'accessory', 'clothing', 'spare']); - $query->where('stock_quantity', '>', 0); // Solo mostrar si tiene stock + //mostrar si tiene stock mayor a 0 + $productsQuery->where('stock_quantity', '>', 0); // Resultados paginados - $products = $query->paginate(12)->withQueryString(); + $products = $productsQuery->orderBy('name', 'asc')->paginate(12)->withQueryString(); - // --- 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']) + $allProducts = Product::where('type', '!=', 'service') ->where('stock_quantity', '>', 0) - ->orderBy('name') // Ordenados alfabéticamente + ->orderBy('name', 'asc') ->get(); - // Devuelve la vista pasando ambas variables + // Devuelve la vista return view('catalogo.index', compact('products', 'allProducts')); } @@ -48,4 +48,4 @@ class CatalogoController extends Controller { return view('catalogo.show', compact('product')); } -} +} \ No newline at end of file diff --git a/app/Http/Controllers/ProductosController.php b/app/Http/Controllers/ProductosController.php index 3d78047..6ddeded 100644 --- a/app/Http/Controllers/ProductosController.php +++ b/app/Http/Controllers/ProductosController.php @@ -16,12 +16,15 @@ class ProductosController extends Controller { $query = $request->input('search'); $status = $request->input('stock_status'); + $type = $request->input('type'); $products = Product::query() ->when($query, function ($q) use ($query) { - // CORREGIDO: Filtramos solo por description return $q->where('name', 'like', "%{$query}%"); }) + ->when($type, function ($q) use ($type) { + return $q->where('type', $type); + }) ->when($status, function ($q) use ($status) { if ($status === 'low') { return $q->whereColumn('stock_quantity', '<', 'min_stock_alert') @@ -29,17 +32,17 @@ class ProductosController extends Controller } elseif ($status === 'medium') { return $q->whereColumn('stock_quantity', '>=', 'min_stock_alert') - ->whereRaw('stock_quantity <= (min_stock_alert + 1)') + ->whereRaw('stock_quantity <= (min_stock_alert + 5)') ->where('type', '!=', 'service'); } elseif ($status === 'ok') { - return $q->whereRaw('stock_quantity > (min_stock_alert + 1)') + return $q->whereRaw('stock_quantity > (min_stock_alert + 5)') ->where('type', '!=', 'service'); } }) - ->orderBy('stock_quantity', 'asc') - ->paginate(10) - ->withQueryString(); + ->orderBy('stock_quantity', 'asc') // Ordena primero los que tienen poco stock + ->paginate(10) // Muestra de a 10 productos + ->withQueryString(); // Mantiene los filtros al pasar a la página 2, 3, etc. return view('productos.index', compact('products')); } @@ -64,7 +67,7 @@ class ProductosController extends Controller '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,service', // CORREGIDO: agregados nuevos tipos + 'type' => 'required|in:bike,accessory,clothing,spare,service,children,skate,rollers,other', 'serial_number' => 'nullable|string|max:100', 'image' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048', ]); @@ -110,7 +113,7 @@ class ProductosController extends Controller { $validated = $request->validate([ 'name' => 'required|string|max:255', // CORREGIDO - 'type' => 'required|in:bike,accessory,clothing,spare,service', // CORREGIDO + 'type' => 'required|in:bike,accessory,clothing,spare,service,children,skate,rollers,other', // CORREGIDO 'price' => 'required|numeric|min:0', 'cost' => 'nullable|numeric|min:0', 'stock_quantity' => 'required|integer|min:0', diff --git a/app/Http/Controllers/TallerController.php b/app/Http/Controllers/TallerController.php index 2ff9ab0..07fa2b3 100644 --- a/app/Http/Controllers/TallerController.php +++ b/app/Http/Controllers/TallerController.php @@ -35,9 +35,9 @@ class TallerController extends Controller { $clients = Client::orderBy('name')->get(); - $products = Product::whereIn('type', ['accessory','spare']) + $products = Product::whereIn('type', ['accessory','spare', 'service']) ->where('stock_quantity', '>', 0) - ->get(['id', 'name', 'price', 'sku', 'stock_quantity']); // Solo campos necesarios + ->get(['id', 'name', 'price', 'stock_quantity']); // Solo campos necesarios return view('taller.create', compact('clients', 'products')); } diff --git a/app/Models/Supplier.php b/app/Models/Supplier.php index c72f08a..5690a35 100644 --- a/app/Models/Supplier.php +++ b/app/Models/Supplier.php @@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model; class Supplier extends Model { - protected $fillable = [ 'name', 'phone', 'email' ]; + protected $fillable = [ 'name', 'phone']; public function products() { diff --git a/database/migrations/2025_12_06_182356_create_suppliers_table.php b/database/migrations/2025_12_06_182356_create_suppliers_table.php index ed43605..4e9f590 100644 --- a/database/migrations/2025_12_06_182356_create_suppliers_table.php +++ b/database/migrations/2025_12_06_182356_create_suppliers_table.php @@ -15,7 +15,7 @@ return new class extends Migration $table->id(); $table->string('name'); $table->string('phone'); - $table->string('email'); + $table->string('email')->nullable(); $table->timestamps(); }); } 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 51a2f95..fe306b5 100644 --- a/database/migrations/2025_12_06_182358_create_products_table.php +++ b/database/migrations/2025_12_06_182358_create_products_table.php @@ -13,7 +13,7 @@ return new class extends Migration { Schema::create('products', function (Blueprint $table) { $table->id(); - $table->enum('type', ['bike', 'accessory', 'clothing', 'spare']); // Tipo de producto + $table->enum('type', ['bike', 'accessory', 'clothing', 'spare', 'children', 'skate', 'rollers','other']); // Tipo de producto //$table->string('name'); //$table->string('sku')->unique()->nullable(); // Código de barras o interno $table->text('name')->nullable(); diff --git a/resources/views/catalogo/index.blade.php b/resources/views/catalogo/index.blade.php index 486a4b7..7096600 100644 --- a/resources/views/catalogo/index.blade.php +++ b/resources/views/catalogo/index.blade.php @@ -35,24 +35,64 @@ @endpush - +
-
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + @if(request('search') || request('type')) + + + + @endif +
+
+ + @if ($products->count() > 0)
@foreach ($products as $product) @@ -60,7 +100,7 @@ {{-- Imagen si existe --}} @if (!empty($product->image_path)) - {{ $product->description }} @else
@@ -78,14 +118,14 @@ {{-- Botón --}} - Ver + + Ver Detalle
@endforeach
@else -

No hay productos disponibles.

+

No se encontraron productos con esos filtros.

@endif
@@ -106,12 +146,12 @@ width: '100%' }); - // 2. Evento: Cuando el usuario hace clic en una opción de la lista + // 2. Evento: Cuando el usuario hace clic en una opción de la lista Select2 $('#buscador-catalogo').on('select2:select', function (e) { let urlDestino = $(this).val(); if (urlDestino) { - // Redirigimos a la página del producto + // Redirigimos a la página del detalle del producto window.location.href = urlDestino; } }); diff --git a/resources/views/productos/index.blade.php b/resources/views/productos/index.blade.php index 6022cdc..04e37b6 100644 --- a/resources/views/productos/index.blade.php +++ b/resources/views/productos/index.blade.php @@ -5,44 +5,77 @@ - -
- -
-
+ +
+ + + + + +
+ class="block w-full p-3 ps-10 text-sm text-black dark:text-white border border-neutral-400 dark:border-neutral-700 rounded-lg bg-neutral-200 dark:bg-neutral-800 placeholder-gray-600 dark:placeholder-gray-500 focus:ring-neon-lime focus:border-neon-lime" + placeholder="Buscar por nombre">
- -
- - - - - + + + + + + + + + +
+ +
+ +
+ + + @if(request('search') || request('type') || request('stock_status')) + + @endif - + + Nuevo Producto
+
- + @@ -54,13 +87,16 @@
Producto / SKUProducto Tipo Precio Stock
{{ $product->name }}
-
{{ $product->sku }}
+
{{ $product->sku ?? 'Sin SKU' }}
@if($product->type === 'bike') Bicicleta @elseif($product->type === 'clothing') Indumentaria @elseif($product->type === 'spare') Repuesto - @else Accesorio @endif + @elseif($product->type === 'children') Infantil + @elseif($product->type === 'rollers') Rollers + @elseif($product->type === 'skate') Skate + @else Accesorio / Otro @endif ${{ number_format($product->price, 2) }} @@ -107,6 +143,7 @@
+
{{ $products->links() }} From 6abef7afd83e21376b6c96d6e218800014cbb42d Mon Sep 17 00:00:00 2001 From: gianella Date: Sun, 5 Apr 2026 01:50:59 -0300 Subject: [PATCH 4/4] arreglitos esteticos --- app/Http/Controllers/ClientController.php | 7 +++++-- resources/views/agenda.blade.php | 8 ++++++++ resources/views/catalogo/index.blade.php | 7 +++++-- resources/views/taller/create.blade.php | 10 +++++++--- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index da56e93..948ae5e 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -59,8 +59,11 @@ class ClientController extends Controller else if ($request->input('origin') === 'taller') { // Si vino de taller, volvemos a taller con el cliente nuevo - return redirect()->route('taller.create', ['new_client_id' => $client->id]) - ->with('success', 'Cliente creado. Ya puedes seleccionarlo.'); + return redirect()->route('taller.create', [ + 'new_client_id' => $client->id, + 'new_client_name' => $client->name, + 'new_client_phone' => $client->phone + ])->with('success', 'Cliente creado. Ya puedes seleccionarlo.'); } // 3. Si no, vuelvo al index de clientes diff --git a/resources/views/agenda.blade.php b/resources/views/agenda.blade.php index adea0d8..2e1185f 100644 --- a/resources/views/agenda.blade.php +++ b/resources/views/agenda.blade.php @@ -1,4 +1,12 @@ +
diff --git a/resources/views/catalogo/index.blade.php b/resources/views/catalogo/index.blade.php index 7096600..ff3bd4b 100644 --- a/resources/views/catalogo/index.blade.php +++ b/resources/views/catalogo/index.blade.php @@ -92,11 +92,13 @@
+ @if ($products->count() > 0)
@foreach ($products as $product) -
+ +
{{-- Imagen si existe --}} @if (!empty($product->image_path)) @@ -108,7 +110,8 @@
@endif -

{{ $product->name }}

+ +

{{ $product->name }}

{{-- Precio --}} @if (!empty($product->price)) diff --git a/resources/views/taller/create.blade.php b/resources/views/taller/create.blade.php index 9d54609..9b28db1 100644 --- a/resources/views/taller/create.blade.php +++ b/resources/views/taller/create.blade.php @@ -27,19 +27,23 @@
+ Nuevo Cliente
- + +
-