MUCHOS CAMBIOS (IMPORTANTES)
This commit is contained in:
@@ -3,11 +3,23 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\Product;
|
||||||
|
|
||||||
class HomeController extends Controller
|
class HomeController extends Controller
|
||||||
{
|
{
|
||||||
public function __invoke() // * Controlador con un unico metodo se usa __invoke
|
public function __invoke(Request $request)
|
||||||
{
|
{
|
||||||
return view('welcome');
|
// Si tuvieras un campo 'sales_count', podrías usar ->orderByDesc('sales_count')
|
||||||
|
$destacados = Product::where('type', 'bike')
|
||||||
|
->latest() // Las más nuevas
|
||||||
|
->take(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Si no hay bicis, traemos cualquier cosa para que no se rompa
|
||||||
|
if ($destacados->isEmpty()) {
|
||||||
|
$destacados = Product::take(5)->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('welcome', compact('destacados'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
|
|||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\Rule; // Necesario para validar unicidad al editar
|
use Illuminate\Validation\Rule; // Necesario para validar unicidad al editar
|
||||||
|
use Illuminate\Support\Facades\Storage; // <--- IMPORTANTE: Agregar esto arriba
|
||||||
|
|
||||||
class ProductosController extends Controller
|
class ProductosController extends Controller
|
||||||
{
|
{
|
||||||
@@ -54,6 +55,7 @@ class ProductosController extends Controller
|
|||||||
'min_stock_alert' => 'required|integer|min:0',
|
'min_stock_alert' => 'required|integer|min:0',
|
||||||
'type' => 'required|in:bike,accessory,service', // Solo permite estos 3 valores
|
'type' => 'required|in:bike,accessory,service', // Solo permite estos 3 valores
|
||||||
'serial_number' => 'nullable|string|max:100',
|
'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)
|
// 2. Si no viene SKU, generamos uno automático (Opcional pero útil)
|
||||||
@@ -61,6 +63,17 @@ class ProductosController extends Controller
|
|||||||
$validated['sku'] = 'GEN-' . strtoupper(uniqid());
|
$validated['sku'] = 'GEN-' . strtoupper(uniqid());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. 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
|
||||||
|
$validated['suppliers_id'] = 1;
|
||||||
// 3. Creamos el producto
|
// 3. Creamos el producto
|
||||||
Product::create($validated);
|
Product::create($validated);
|
||||||
|
|
||||||
@@ -102,8 +115,26 @@ class ProductosController extends Controller
|
|||||||
'min_stock_alert' => 'required|integer|min:0',
|
'min_stock_alert' => 'required|integer|min:0',
|
||||||
'type' => 'required|in:bike,accessory,service',
|
'type' => 'required|in:bike,accessory,service',
|
||||||
'serial_number' => 'nullable|string|max:100',
|
'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);
|
$product->update($validated);
|
||||||
|
|
||||||
return redirect()->route('productos.index')
|
return redirect()->route('productos.index')
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class ProductFactory extends Factory
|
|||||||
'type' => $type,
|
'type' => $type,
|
||||||
// Nro de serie si es bicicleta
|
// Nro de serie si es bicicleta
|
||||||
'serial_number' => $type === 'bike' ? strtoupper($this->faker->bothify('##??##??')) : null,
|
'serial_number' => $type === 'bike' ? strtoupper($this->faker->bothify('##??##??')) : null,
|
||||||
|
'suppliers_id'=> 1
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ return new class extends Migration
|
|||||||
$table->decimal('cost', 10, 2)->nullable(); // Costo (solo admin)
|
$table->decimal('cost', 10, 2)->nullable(); // Costo (solo admin)
|
||||||
|
|
||||||
$table->integer('stock_quantity')->default(0);
|
$table->integer('stock_quantity')->default(0);
|
||||||
$table->integer('min_stock_alert')->default(5); // Alerta
|
$table->integer('min_stock_alert'); // Alerta
|
||||||
|
|
||||||
$table->enum('type', ['bike', 'accessory', 'service']);
|
$table->enum('type', ['bike', 'accessory', 'service']);
|
||||||
$table->string('serial_number')->nullable(); // Solo para bicis
|
$table->string('serial_number')->nullable(); // Solo para bicis
|
||||||
|
|
||||||
$table->foreignId('suppliers_id')->constrained();
|
$table->foreignId('suppliers_id')->constrained()->default(1);
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('products', function (Blueprint $table) {
|
||||||
|
$table->string('image_path')->nullable()->after('type');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('products', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('image_path');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@ use Illuminate\Database\Seeder;
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
|
use App\Models\Supplier;
|
||||||
use App\Models\Appointment;
|
use App\Models\Appointment;
|
||||||
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
|
|
||||||
@@ -29,6 +30,12 @@ class DatabaseSeeder extends Seeder
|
|||||||
'role' => 'employee',
|
'role' => 'employee',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
Supplier::create([
|
||||||
|
'name' => 'Cámara 29 Válvula Auto',
|
||||||
|
'phone' => '3434567890',
|
||||||
|
'email' => 'suplier@suplier.com'
|
||||||
|
]);
|
||||||
|
|
||||||
//Crear Productos
|
//Crear Productos
|
||||||
Product::create([
|
Product::create([
|
||||||
'name' => 'Cámara 29 Válvula Auto',
|
'name' => 'Cámara 29 Válvula Auto',
|
||||||
@@ -37,7 +44,8 @@ class DatabaseSeeder extends Seeder
|
|||||||
'cost' => 2500,
|
'cost' => 2500,
|
||||||
'stock_quantity' => 20,
|
'stock_quantity' => 20,
|
||||||
'min_stock_alert' => 5,
|
'min_stock_alert' => 5,
|
||||||
'type' => 'accessory'
|
'type' => 'accessory',
|
||||||
|
'suppliers_id' => 1
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Product::create([
|
Product::create([
|
||||||
@@ -48,7 +56,8 @@ class DatabaseSeeder extends Seeder
|
|||||||
'stock_quantity' => 2,
|
'stock_quantity' => 2,
|
||||||
'min_stock_alert' => 1,
|
'min_stock_alert' => 1,
|
||||||
'type' => 'bike',
|
'type' => 'bike',
|
||||||
'serial_number' => 'VZ998877'
|
'serial_number' => 'VZ998877',
|
||||||
|
'suppliers_id' => 1
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Generar 10 productos aleatorios más
|
// Generar 10 productos aleatorios más
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
@props(['items'])
|
||||||
|
|
||||||
|
<div class="relative w-full overflow-hidden rounded-xl border border-neutral-800 bg-panel-bg shadow-xl group" id="lauck-carousel">
|
||||||
|
|
||||||
|
<!-- Título Flotante (Opcional) -->
|
||||||
|
<div class="absolute top-4 left-6 z-10 bg-black/50 backdrop-blur-sm px-3 py-1 rounded border border-neon-lime/30">
|
||||||
|
<span class="text-xs font-bold text-neon-lime uppercase tracking-widest">Destacados</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contenedor de Slides (Track) -->
|
||||||
|
<div class="flex transition-transform duration-500 ease-in-out h-[400px]" id="carousel-track">
|
||||||
|
@forelse($items as $item)
|
||||||
|
<div class="w-full flex-shrink-0 flex flex-col md:flex-row h-full relative">
|
||||||
|
|
||||||
|
<!-- Imagen / Visual (Izquierda o Fondo) -->
|
||||||
|
<div class="w-full md:w-1/2 bg-neutral-800 flex items-center justify-center relative overflow-hidden">
|
||||||
|
<!-- Decoración de fondo -->
|
||||||
|
<div class="absolute inset-0 bg-grid-pattern opacity-10"></div>
|
||||||
|
|
||||||
|
<!-- Icono Gigante (Placeholder de Bici) -->
|
||||||
|
<div class="text-neutral-700 transform group-hover:scale-110 transition-transform duration-700">
|
||||||
|
@if($item->image_path)
|
||||||
|
<img src="{{ asset('storage/' . $item->image_path) }}" alt="{{ $item->name }}" class="w-full h-full object-cover">
|
||||||
|
@else
|
||||||
|
<!-- Placeholder si no tiene foto -->
|
||||||
|
<div class="w-full h-full flex items-center justify-center bg-neutral-800">
|
||||||
|
<svg class="w-48 h-48" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="0.5" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path></svg>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Precio Flotante -->
|
||||||
|
<div class="absolute bottom-4 left-4 bg-neon-lime text-neutral-900 font-black px-4 py-2 rounded-lg text-xl shadow-lg shadow-neon-lime/20">
|
||||||
|
${{ number_format($item->price, 0, ',', '.') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info (Derecha) -->
|
||||||
|
<div class="w-full md:w-1/2 p-8 md:p-12 flex flex-col justify-center bg-gradient-to-br from-panel-bg to-neutral-900">
|
||||||
|
<h3 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-2">{{ $item->sku }}</h3>
|
||||||
|
<h2 class="text-3xl md:text-4xl font-black text-white italic mb-4 leading-tight">
|
||||||
|
{{ $item->name }}
|
||||||
|
</h2>
|
||||||
|
<p class="text-gray-400 text-sm md:text-base mb-8 line-clamp-3">
|
||||||
|
{{ $item->description ?? 'Una bicicleta diseñada para el máximo rendimiento en todo terreno. Consultar especificaciones técnicas en el catálogo.' }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<a href="{{ route('catalogo', ['search' => $item->sku]) }}" class="px-6 py-3 bg-white text-neutral-900 font-bold uppercase tracking-wider rounded hover:bg-gray-200 transition-colors">
|
||||||
|
Ver Detalles
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<!-- Slide de Fallback por si no hay datos -->
|
||||||
|
<div class="w-full flex-shrink-0 flex items-center justify-center h-full bg-neutral-800 text-gray-500">
|
||||||
|
<p>No hay productos destacados disponibles.</p>
|
||||||
|
</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controles (Flechas) -->
|
||||||
|
<button id="prevBtn" class="absolute top-1/2 left-4 -translate-y-1/2 bg-black/30 hover:bg-neon-lime hover:text-neutral-900 text-white p-3 rounded-full backdrop-blur-sm transition-all border border-white/10 z-20">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="nextBtn" class="absolute top-1/2 right-4 -translate-y-1/2 bg-black/30 hover:bg-neon-lime hover:text-neutral-900 text-white p-3 rounded-full backdrop-blur-sm transition-all border border-white/10 z-20">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Indicadores (Puntos) -->
|
||||||
|
<div class="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex space-x-2 z-20">
|
||||||
|
@foreach($items as $index => $item)
|
||||||
|
<button class="carousel-dot w-3 h-3 rounded-full bg-white/20 hover:bg-neon-lime transition-all" data-index="{{ $index }}"></button>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
$(document).ready(function() {
|
||||||
|
const $track = $('#carousel-track');
|
||||||
|
const $slides = $track.children();
|
||||||
|
const slideCount = $slides.length;
|
||||||
|
let currentIndex = 0;
|
||||||
|
let autoPlayInterval;
|
||||||
|
|
||||||
|
function updateCarousel() {
|
||||||
|
const translateX = -(currentIndex * 100);
|
||||||
|
$track.css('transform', `translateX(${translateX}%)`);
|
||||||
|
|
||||||
|
// Actualizar puntos
|
||||||
|
$('.carousel-dot').removeClass('bg-neon-lime scale-125').addClass('bg-white/20');
|
||||||
|
$(`.carousel-dot[data-index="${currentIndex}"]`).addClass('bg-neon-lime scale-125').removeClass('bg-white/20');
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextSlide() {
|
||||||
|
currentIndex = (currentIndex + 1) % slideCount;
|
||||||
|
updateCarousel();
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevSlide() {
|
||||||
|
currentIndex = (currentIndex - 1 + slideCount) % slideCount;
|
||||||
|
updateCarousel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
$('#nextBtn').click(function() {
|
||||||
|
nextSlide();
|
||||||
|
resetAutoPlay();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#prevBtn').click(function() {
|
||||||
|
prevSlide();
|
||||||
|
resetAutoPlay();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.carousel-dot').click(function() {
|
||||||
|
currentIndex = $(this).data('index');
|
||||||
|
updateCarousel();
|
||||||
|
resetAutoPlay();
|
||||||
|
});
|
||||||
|
|
||||||
|
// AutoPlay (cada 5 segundos)
|
||||||
|
function startAutoPlay() {
|
||||||
|
autoPlayInterval = setInterval(nextSlide, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetAutoPlay() {
|
||||||
|
clearInterval(autoPlayInterval);
|
||||||
|
startAutoPlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iniciar
|
||||||
|
if(slideCount > 0) {
|
||||||
|
updateCarousel();
|
||||||
|
startAutoPlay();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<div class="w-full max-w-4xl mx-auto bg-panel-bg border border-neutral-800 rounded-xl p-8 shadow-lg">
|
<div class="w-full max-w-4xl mx-auto bg-panel-bg border border-neutral-800 rounded-xl p-8 shadow-lg">
|
||||||
|
|
||||||
<form action="{{ route('productos.store') }}" method="POST">
|
<form action="{{ route('productos.store') }}" method="POST" enctype="multipart/form-data">
|
||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
<!-- Grid Layout -->
|
<!-- Grid Layout -->
|
||||||
@@ -59,6 +59,14 @@
|
|||||||
<x-forms.label for="description" value="Descripción / Notas" />
|
<x-forms.label for="description" value="Descripción / Notas" />
|
||||||
<textarea id="description" name="description" rows="3" class="bg-neutral-800 border-neutral-700 text-white text-sm rounded-lg focus:ring-neon-lime focus:border-neon-lime block w-full p-2.5 placeholder-gray-500">{{ old('description') }}</textarea>
|
<textarea id="description" name="description" rows="3" class="bg-neutral-800 border-neutral-700 text-white text-sm rounded-lg focus:ring-neon-lime focus:border-neon-lime block w-full p-2.5 placeholder-gray-500">{{ old('description') }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<x-forms.label for="image" value="Imagen del Producto" />
|
||||||
|
<x-forms.input type="file" name="image" class="block w-full text-sm text-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded-full
|
||||||
|
file:border-0 file:text-sm file:font-semibold file:bg-neon-lime file:text-neutral-900 hover:file:bg-[#b3e600]
|
||||||
|
" :error="$errors->first('image')"/>
|
||||||
|
{{-- @error('image') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror --}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Botones Acción -->
|
<!-- Botones Acción -->
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
<x-layout title="Home - Lauck">
|
<x-layout title="Home - Lauck">
|
||||||
<x-section-header subtitle="Inicio" title="Bicicleteria " highlight="Lauck"/>
|
<x-section-header subtitle="Inicio" title="Bicicleteria " highlight="Lauck"/>
|
||||||
|
|
||||||
<!-- Grid de Tarjetas -->
|
<!-- CARROUSEL DESTACADO -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 w-full mb-12">
|
<div class="w-full mb-12">
|
||||||
|
<!-- datos del controlador -->
|
||||||
<!-- Tarjeta Catálogo -->
|
<x-ui.carrousel :items="$destacados" />
|
||||||
<x-ui.card href="{{ route('catalogo') }}" title="Catálogo" description="Ver bicicletas y productos." linkText="Ir al catalogo">
|
|
||||||
<svg class="w-12 h-12 text-neon-lime" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" /></svg>
|
|
||||||
</x-ui.card>
|
|
||||||
|
|
||||||
<!-- Tarjeta Catálogo -->
|
|
||||||
<x-ui.card href="{{-- url('/taller') --}}#" title="Se va a cambiar por Carrousel de + frec" description="." linkText="Ir">
|
|
||||||
</x-ui.card>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</x-layout>
|
</x-layout>
|
||||||
|
|||||||
Reference in New Issue
Block a user