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 82eecb7..e72942b 100644 --- a/app/Http/Controllers/CatalogoController.php +++ b/app/Http/Controllers/CatalogoController.php @@ -8,34 +8,44 @@ 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) { - // Consulta base - $query = Product::query(); + // 1. Atrapamos lo que el usuario escribió o seleccionó + $query = $request->input('search'); + $type = $request->input('type'); - // Lógica del Buscador: Si recibimos algo en el input "search" - 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'); } - $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(); // withQueryString mantiene la búsqueda al cambiar de página + $products = $productsQuery->orderBy('name', 'asc')->paginate(12)->withQueryString(); + + $allProducts = Product::where('type', '!=', 'service') + ->where('stock_quantity', '>', 0) + ->orderBy('name', 'asc') + ->get(); // Devuelve la vista - return view('catalogo.index', compact('products')); + return view('catalogo.index', compact('products', 'allProducts')); } public function show(Product $product) { return view('catalogo.show', compact('product')); } -} +} \ No newline at end of file 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/app/Http/Controllers/ProductosController.php b/app/Http/Controllers/ProductosController.php index c7e37e7..6ddeded 100644 --- a/app/Http/Controllers/ProductosController.php +++ b/app/Http/Controllers/ProductosController.php @@ -14,35 +14,35 @@ 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'); + $type = $request->input('type'); - // 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}%"); + 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') { - // 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 + 5)') + ->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 + 5)') + ->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') // 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')); } @@ -62,30 +62,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,children,skate,rollers,other', '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 +85,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 +112,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,children,skate,rollers,other', // 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 +146,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/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 326973c..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,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', 'children', 'skate', 'rollers','other']); // 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/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 b50f89f..ff3bd4b 100644 --- a/resources/views/catalogo/index.blade.php +++ b/resources/views/catalogo/index.blade.php @@ -1,14 +1,105 @@ - - + @push('styles') + + + @endpush -
+ +
+ + +
+ + +
+ + +
+ +
+ +
+ +
+ +
+ +
+ + @if(request('search') || request('type')) + + + + @endif +
+
+ + + @if ($products->count() > 0)
@foreach ($products as $product) -
+ +
+ {{-- Imagen si existe --}} @if (!empty($product->image_path)) {{ $product->name }} @endif - {{-- Nombre --}} -

{{ $product->name }}

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

{{ $product->description }}

- @endif -
+ +

{{ $product->name }}

{{-- Precio --}} @if (!empty($product->price)) @@ -35,20 +119,47 @@ @endif {{-- Botón --}} - - Ver + + Ver Detalle
@endforeach
@else -

No hay productos disponibles.

+

No se encontraron productos con esos filtros.

@endif -
+
{{ $products->links() }}
+ + @push('scripts') + + + + + @endpush + 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() }} 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
- + +
-