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() }}