NEW:
- Implementada nueva funcion para crear productos destacados por etiquetas, mostrando carrusel con los mismos en la pagina welcome.
This commit is contained in:
@@ -15,9 +15,10 @@ class CatalogoController extends Controller
|
|||||||
// 1. Atrapamos lo que el usuario escribió o seleccionó
|
// 1. Atrapamos lo que el usuario escribió o seleccionó
|
||||||
$query = $request->input('search');
|
$query = $request->input('search');
|
||||||
$type = $request->input('type');
|
$type = $request->input('type');
|
||||||
|
$tagFilter = $request->input('tag');
|
||||||
|
|
||||||
// --- CONSULTA PARA LA GRILLA (PAGINADA) ---
|
// --- CONSULTA PARA LA GRILLA (PAGINADA) ---
|
||||||
$productsQuery = Product::query();
|
$productsQuery = Product::with('tags');
|
||||||
|
|
||||||
$productsQuery->when($query, function ($q) use ($query) {
|
$productsQuery->when($query, function ($q) use ($query) {
|
||||||
return $q->where('name', 'like', "%{$query}%");
|
return $q->where('name', 'like', "%{$query}%");
|
||||||
@@ -29,6 +30,12 @@ class CatalogoController extends Controller
|
|||||||
$productsQuery->where('type', '!=', 'service');
|
$productsQuery->where('type', '!=', 'service');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($tagFilter) {
|
||||||
|
$productsQuery->whereHas('tags', function($q) use ($tagFilter) {
|
||||||
|
$q->where('name', $tagFilter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//mostrar si tiene stock mayor a 0
|
//mostrar si tiene stock mayor a 0
|
||||||
$productsQuery->where('stock_quantity', '>', 0);
|
$productsQuery->where('stock_quantity', '>', 0);
|
||||||
|
|
||||||
@@ -40,8 +47,10 @@ class CatalogoController extends Controller
|
|||||||
->orderBy('name', 'asc')
|
->orderBy('name', 'asc')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
$allTags = \App\Models\Tag::orderBy('name')->get();
|
||||||
|
|
||||||
// Devuelve la vista
|
// Devuelve la vista
|
||||||
return view('catalogo.index', compact('products', 'allProducts'));
|
return view('catalogo.index', compact('products', 'allProducts', 'allTags'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show(Product $product)
|
public function show(Product $product)
|
||||||
|
|||||||
@@ -9,17 +9,18 @@ class HomeController extends Controller
|
|||||||
{
|
{
|
||||||
public function __invoke(Request $request)
|
public function __invoke(Request $request)
|
||||||
{
|
{
|
||||||
// Si tuvieras un campo 'sales_count', podrías usar ->orderByDesc('sales_count')
|
// Traer todas las etiquetas con sus productos
|
||||||
$destacados = Product::where('type', 'bike')
|
$tagsConProductos = \App\Models\Tag::with('products')->get()
|
||||||
->latest() // Las más nuevas
|
->filter(function ($tag) {
|
||||||
->take(5)
|
// Solo mantener etiquetas que tengan al menos 1 producto
|
||||||
->get();
|
return $tag->products->count() > 0;
|
||||||
|
})
|
||||||
|
->map(function ($tag) {
|
||||||
|
// Limitar a 7 productos como máximo para el carrousel
|
||||||
|
$tag->setRelation('products', $tag->products->take(7));
|
||||||
|
return $tag;
|
||||||
|
});
|
||||||
|
|
||||||
// Si no hay bicis, traemos cualquier cosa para que no se rompa
|
return view('welcome', compact('tagsConProductos'));
|
||||||
if ($destacados->isEmpty()) {
|
|
||||||
$destacados = Product::take(5)->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('welcome', compact('destacados'));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
|||||||
|
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use App\Models\Supplier;
|
use App\Models\Supplier;
|
||||||
|
use App\Models\Tag;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
@@ -55,7 +56,8 @@ class ProductosController extends Controller
|
|||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
$suppliers = Supplier::orderBy('name')->get();
|
$suppliers = Supplier::orderBy('name')->get();
|
||||||
return view('productos.create', compact('suppliers'));
|
$tags = Tag::orderBy('name')->get();
|
||||||
|
return view('productos.create', compact('suppliers', 'tags'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
@@ -79,7 +81,11 @@ class ProductosController extends Controller
|
|||||||
}
|
}
|
||||||
unset($validated['image']);
|
unset($validated['image']);
|
||||||
|
|
||||||
Product::create($validated);
|
$product = Product::create($validated);
|
||||||
|
|
||||||
|
if ($request->has('tags')) {
|
||||||
|
$product->tags()->sync($request->tags);
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()->route('productos.index')
|
return redirect()->route('productos.index')
|
||||||
->with('success', 'Producto creado correctamente.');
|
->with('success', 'Producto creado correctamente.');
|
||||||
@@ -93,7 +99,8 @@ class ProductosController extends Controller
|
|||||||
public function edit(Product $product)
|
public function edit(Product $product)
|
||||||
{
|
{
|
||||||
$suppliers = Supplier::orderBy('name')->get();
|
$suppliers = Supplier::orderBy('name')->get();
|
||||||
return view('productos.edit', compact('product', 'suppliers'));
|
$tags = Tag::orderBy('name')->get();
|
||||||
|
return view('productos.edit', compact('product', 'suppliers', 'tags'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(Request $request, Product $product)
|
public function update(Request $request, Product $product)
|
||||||
@@ -123,6 +130,12 @@ class ProductosController extends Controller
|
|||||||
|
|
||||||
$product->update($validated);
|
$product->update($validated);
|
||||||
|
|
||||||
|
if ($request->has('tags')) {
|
||||||
|
$product->tags()->sync($request->tags);
|
||||||
|
} else {
|
||||||
|
$product->tags()->detach();
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()->route('productos.index')
|
return redirect()->route('productos.index')
|
||||||
->with('success', 'Producto actualizado exitosamente.');
|
->with('success', 'Producto actualizado exitosamente.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
use App\Models\Tag;
|
||||||
|
|
||||||
|
class TagController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$tags = Tag::orderBy('name')->get();
|
||||||
|
return view('tags.index', compact('tags'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:50|unique:tags,name'
|
||||||
|
]);
|
||||||
|
|
||||||
|
Tag::create($validated);
|
||||||
|
|
||||||
|
return redirect()->route('tags.index')->with('success', 'Etiqueta creada correctamente.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Tag $tag)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:50|unique:tags,name,' . $tag->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tag->update($validated);
|
||||||
|
|
||||||
|
return redirect()->route('tags.index')->with('success', 'Etiqueta actualizada correctamente.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Tag $tag)
|
||||||
|
{
|
||||||
|
$tag->delete();
|
||||||
|
return redirect()->route('tags.index')->with('success', 'Etiqueta eliminada.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,11 @@ class Product extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsToMany(Sale::class, 'sale_details');
|
return $this->belongsToMany(Sale::class, 'sale_details');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function tags()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Tag::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/**$user->sales: Te da la lista de todas las compras de ese usuario.
|
/**$user->sales: Te da la lista de todas las compras de ese usuario.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
|
class Tag extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = ['name'];
|
||||||
|
|
||||||
|
public function products()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Product::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('tags', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name')->unique();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('tags');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('product_tag', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('product_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('tag_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('product_tag');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
<button type="submit" class="px-5 py-3 text-sm font-bold text-neutral-900 bg-neon-lime rounded-lg hover:bg-[#b3e600] transition-colors">
|
<button type="submit" class="px-5 py-3 text-sm font-bold text-neutral-900 bg-neon-lime rounded-lg hover:bg-[#b3e600] transition-colors">
|
||||||
Filtrar
|
Filtrar
|
||||||
</button>
|
</button>
|
||||||
@if(request('search') || request('type'))
|
@if(request('search') || request('type') || request('tag'))
|
||||||
<a href="{{ route('catalogo.index') }}" title="Limpiar Filtros" class="flex items-center justify-center p-3 text-sm font-bold text-red-600 bg-red-100 dark:bg-red-900/30 rounded-lg hover:bg-red-200 transition-colors">
|
<a href="{{ route('catalogo.index') }}" title="Limpiar Filtros" class="flex items-center justify-center p-3 text-sm font-bold text-red-600 bg-red-100 dark:bg-red-900/30 rounded-lg hover:bg-red-200 transition-colors">
|
||||||
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18 17.94 6M18 18 6.06 6"/>
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18 17.94 6M18 18 6.06 6"/>
|
||||||
@@ -90,8 +90,25 @@
|
|||||||
</a>
|
</a>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="tag" value="{{ request('tag') }}">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- Filtro de Etiquetas -->
|
||||||
|
@if($allTags->count() > 0)
|
||||||
|
<div class="mb-8 flex flex-wrap gap-2">
|
||||||
|
@foreach($allTags as $t)
|
||||||
|
<a href="{{ route('catalogo.index', array_merge(request()->query(), ['tag' => $t->name])) }}"
|
||||||
|
class="px-3 py-1.5 text-xs font-bold uppercase rounded-full border transition-colors
|
||||||
|
{{ request('tag') == $t->name
|
||||||
|
? 'bg-neon-lime border-neon-lime text-black dark:text-black'
|
||||||
|
: 'bg-transparent border-neutral-400 dark:border-neutral-600 text-gray-700 dark:text-gray-300 hover:border-neon-lime hover:text-neon-lime dark:hover:border-neon-lime dark:hover:text-neon-lime' }}">
|
||||||
|
#{{ $t->name }}
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<!-- GRILLA DE PRODUCTOS -->
|
<!-- GRILLA DE PRODUCTOS -->
|
||||||
<!-- GRILLA DE PRODUCTOS -->
|
<!-- GRILLA DE PRODUCTOS -->
|
||||||
@if ($products->count() > 0)
|
@if ($products->count() > 0)
|
||||||
@@ -113,6 +130,17 @@
|
|||||||
<!-- ACÁ ESTÁ EL SEGUNDO CAMBIO: Agregamos "flex-1" al título -->
|
<!-- ACÁ ESTÁ EL SEGUNDO CAMBIO: Agregamos "flex-1" al título -->
|
||||||
<h2 class="text-lg font-semibold text-black dark:text-white flex-1">{{ $product->name }}</h2>
|
<h2 class="text-lg font-semibold text-black dark:text-white flex-1">{{ $product->name }}</h2>
|
||||||
|
|
||||||
|
{{-- Etiquetas --}}
|
||||||
|
@if($product->tags->count() > 0)
|
||||||
|
<div class="flex flex-wrap gap-1 mt-2">
|
||||||
|
@foreach($product->tags as $t)
|
||||||
|
<span class="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-full border border-neon-lime text-neon-lime bg-neon-lime/10">
|
||||||
|
#{{ $t->name }}
|
||||||
|
</span>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
{{-- Precio --}}
|
{{-- Precio --}}
|
||||||
@if (!empty($product->price))
|
@if (!empty($product->price))
|
||||||
<p class="text-neutral-900 dark:text-white group-hover:text-lime-700 group-hover:dark:text-neon-lime font-bold mt-3 transition-colors">${{ number_format($product->price, 2, ',', '.') }}</p>
|
<p class="text-neutral-900 dark:text-white group-hover:text-lime-700 group-hover:dark:text-neon-lime font-bold mt-3 transition-colors">${{ number_format($product->price, 2, ',', '.') }}</p>
|
||||||
|
|||||||
@@ -50,6 +50,17 @@
|
|||||||
<p class="text-stone-500 dark:text-neon-lime text-sm font-bold uppercase tracking-widest mb-2">SKU: {{ $product->sku ?? 'N/A' }}</p>
|
<p class="text-stone-500 dark:text-neon-lime text-sm font-bold uppercase tracking-widest mb-2">SKU: {{ $product->sku ?? 'N/A' }}</p>
|
||||||
<h1 class="text-4xl font-black text-black dark:text-white italic mb-2 leading-tight">{{ $product->name }}</h1>
|
<h1 class="text-4xl font-black text-black dark:text-white italic mb-2 leading-tight">{{ $product->name }}</h1>
|
||||||
|
|
||||||
|
{{-- Etiquetas --}}
|
||||||
|
@if($product->tags->count() > 0)
|
||||||
|
<div class="flex flex-wrap gap-2 mt-2 mb-4">
|
||||||
|
@foreach($product->tags as $t)
|
||||||
|
<span class="text-xs font-bold uppercase tracking-wider px-3 py-1 rounded-full border border-neon-lime text-neon-lime bg-neon-lime/10">
|
||||||
|
#{{ $t->name }}
|
||||||
|
</span>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<!-- Stock Status -->
|
<!-- Stock Status -->
|
||||||
<div class="flex items-center gap-2 mt-4">
|
<div class="flex items-center gap-2 mt-4">
|
||||||
@if($product->stock_quantity > 5)
|
@if($product->stock_quantity > 5)
|
||||||
|
|||||||
@@ -41,6 +41,12 @@
|
|||||||
Stock
|
Stock
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ route('tags.index') }}"
|
||||||
|
class="block py-2 px-3 md:p-0 transition-colors {{ request()->is('tags*') ? 'text-lime-600 dark:text-neon-lime border-b-2 border-lime-600 dark:border-neon-lime' : 'dark:text-white hover:text-lime-600 hover:dark:text-neon-lime' }}">
|
||||||
|
Etiquetas
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ route('taller.index') }}"
|
<a href="{{ route('taller.index') }}"
|
||||||
class="block py-2 px-3 md:p-0 transition-colors {{ request()->is('taller*') ? 'text-lime-600 dark:text-neon-lime border-b-2 border-lime-600 dark:border-neon-lime' : 'dark:text-white hover:text-lime-600 hover:dark:text-neon-lime' }}">
|
class="block py-2 px-3 md:p-0 transition-colors {{ request()->is('taller*') ? 'text-lime-600 dark:text-neon-lime border-b-2 border-lime-600 dark:border-neon-lime' : 'dark:text-white hover:text-lime-600 hover:dark:text-neon-lime' }}">
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
@props(['items'])
|
@props(['items', 'title' => 'Destacados'])
|
||||||
|
|
||||||
<div class="relative w-full overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-800 bg-slate-50 dark:bg-panel-bg shadow-xl group" id="lauck-carousel">
|
@php
|
||||||
|
$id = Str::random(8);
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="relative w-full overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-800 bg-slate-50 dark:bg-panel-bg shadow-xl group carousel-container" id="carousel-{{ $id }}">
|
||||||
|
|
||||||
<!-- Título Flotante (Opcional) -->
|
<!-- Título Flotante (Opcional) -->
|
||||||
<div class="absolute top-4 left-6 z-10 bg-neon-lime/50 dark:bg-black/50 backdrop-blur-sm px-3 py-1 rounded border border-black/30 dark:border-neon-lime/30">
|
<div class="absolute top-4 left-6 z-10 bg-neon-lime/50 dark:bg-black/50 backdrop-blur-sm px-3 py-1 rounded border border-black/30 dark:border-neon-lime/30">
|
||||||
<span class="text-xs font-bold text-black dark:text-neon-lime uppercase tracking-widest">Destacados</span>
|
<span class="text-xs font-bold text-black dark:text-neon-lime uppercase tracking-widest">{{ $title }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contenedor de Slides (Track) -->
|
<!-- Contenedor de Slides (Track) -->
|
||||||
<div class="flex transition-transform duration-500 ease-in-out h-[400px]" id="carousel-track">
|
<div class="flex transition-transform duration-500 ease-in-out h-[400px] carousel-track">
|
||||||
@forelse($items as $item)
|
@forelse($items as $item)
|
||||||
<div class="w-full flex-shrink-0 flex flex-col md:flex-row h-full relative">
|
<div class="w-full flex-shrink-0 flex flex-col md:flex-row h-full relative">
|
||||||
|
|
||||||
@@ -61,11 +65,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Controles (Flechas) -->
|
<!-- 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">
|
<button class="prevBtn 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>
|
<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>
|
||||||
|
|
||||||
<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">
|
<button class="nextBtn 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>
|
<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>
|
</button>
|
||||||
|
|
||||||
@@ -80,7 +84,8 @@
|
|||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
const $track = $('#carousel-track');
|
const $container = $('#carousel-{{ $id }}');
|
||||||
|
const $track = $container.find('.carousel-track');
|
||||||
const $slides = $track.children();
|
const $slides = $track.children();
|
||||||
const slideCount = $slides.length;
|
const slideCount = $slides.length;
|
||||||
let currentIndex = 0;
|
let currentIndex = 0;
|
||||||
@@ -91,8 +96,8 @@
|
|||||||
$track.css('transform', `translateX(${translateX}%)`);
|
$track.css('transform', `translateX(${translateX}%)`);
|
||||||
|
|
||||||
// Actualizar puntos
|
// Actualizar puntos
|
||||||
$('.carousel-dot').removeClass('bg-lime-500 dark:bg-neon-lime scale-125').addClass('bg-black/20 dark:bg-white/20');
|
$container.find('.carousel-dot').removeClass('bg-lime-500 dark:bg-neon-lime scale-125').addClass('bg-black/20 dark:bg-white/20');
|
||||||
$(`.carousel-dot[data-index="${currentIndex}"]`).addClass('bg-lime-500 dark:bg-neon-lime scale-125').removeClass('bg-black/20 dark:bg-white/20');
|
$container.find(`.carousel-dot[data-index="${currentIndex}"]`).addClass('bg-lime-500 dark:bg-neon-lime scale-125').removeClass('bg-black/20 dark:bg-white/20');
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextSlide() {
|
function nextSlide() {
|
||||||
@@ -106,17 +111,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Event Listeners
|
// Event Listeners
|
||||||
$('#nextBtn').click(function() {
|
$container.find('.nextBtn').click(function() {
|
||||||
nextSlide();
|
nextSlide();
|
||||||
resetAutoPlay();
|
resetAutoPlay();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#prevBtn').click(function() {
|
$container.find('.prevBtn').click(function() {
|
||||||
prevSlide();
|
prevSlide();
|
||||||
resetAutoPlay();
|
resetAutoPlay();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.carousel-dot').click(function() {
|
$container.find('.carousel-dot').click(function() {
|
||||||
currentIndex = $(this).data('index');
|
currentIndex = $(this).data('index');
|
||||||
updateCarousel();
|
updateCarousel();
|
||||||
resetAutoPlay();
|
resetAutoPlay();
|
||||||
|
|||||||
@@ -35,6 +35,17 @@
|
|||||||
<option value="spare" {{ old('type') == 'spare' ? 'selected' : '' }}>Repuesto</option>
|
<option value="spare" {{ old('type') == 'spare' ? 'selected' : '' }}>Repuesto</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Etiquetas -->
|
||||||
|
<div class="md:col-span-4">
|
||||||
|
<x-forms.label for="tags" value="Etiquetas" />
|
||||||
|
<select id="tags" name="tags[]" multiple="multiple" class="select2-tags w-full">
|
||||||
|
@foreach($tags as $tag)
|
||||||
|
<option value="{{ $tag->id }}" {{ in_array($tag->id, old('tags', [])) ? 'selected' : '' }}>
|
||||||
|
{{ $tag->name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<!-- Precios -->
|
<!-- Precios -->
|
||||||
<div class="">
|
<div class="">
|
||||||
<x-forms.label for="price" value="Precio Venta ($)" />
|
<x-forms.label for="price" value="Precio Venta ($)" />
|
||||||
@@ -72,4 +83,83 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@push('styles')
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||||
|
<style>
|
||||||
|
/* Dark Theme & Neon Lime overrides for Select2 */
|
||||||
|
.select2-container--default .select2-selection--multiple {
|
||||||
|
background-color: rgb(38 38 38); /* neutral-800 */
|
||||||
|
border-color: rgb(64 64 64); /* neutral-700 */
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
.select2-container--default.select2-container--focus .select2-selection--multiple {
|
||||||
|
border-color: #b3e600; /* neon-lime */
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: 0 0 0 1px #b3e600;
|
||||||
|
}
|
||||||
|
.select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||||
|
background-color: #b3e600;
|
||||||
|
border-color: #99c200;
|
||||||
|
color: #171717; /* neutral-900 */
|
||||||
|
}
|
||||||
|
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
||||||
|
color: #171717;
|
||||||
|
}
|
||||||
|
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||||
|
background-color: #99c200;
|
||||||
|
color: #171717;
|
||||||
|
}
|
||||||
|
.select2-dropdown {
|
||||||
|
background-color: rgb(38 38 38);
|
||||||
|
border-color: rgb(64 64 64);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.select2-search--dropdown .select2-search__field {
|
||||||
|
background-color: rgb(23 23 23);
|
||||||
|
border-color: rgb(64 64 64);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.select2-container--default .select2-results__option--selected {
|
||||||
|
background-color: rgb(64 64 64);
|
||||||
|
}
|
||||||
|
.select2-container--default .select2-results__option--highlighted.select2-results__option--selectable {
|
||||||
|
background-color: #b3e600;
|
||||||
|
color: #171717;
|
||||||
|
}
|
||||||
|
.select2-container--default .select2-selection--multiple .select2-search--inline .select2-search__field {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
html:not(.dark) .select2-container--default .select2-selection--multiple {
|
||||||
|
background-color: rgb(209 213 219); /* gray-300 */
|
||||||
|
border-color: rgb(156 163 175); /* neutral-400 */
|
||||||
|
}
|
||||||
|
html:not(.dark) .select2-dropdown {
|
||||||
|
background-color: rgb(209 213 219);
|
||||||
|
border-color: rgb(156 163 175);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
html:not(.dark) .select2-search--dropdown .select2-search__field {
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
html:not(.dark) .select2-container--default .select2-selection--multiple .select2-search--inline .select2-search__field {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('.select2-tags').select2({
|
||||||
|
placeholder: "Buscar y seleccionar etiquetas...",
|
||||||
|
allowClear: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
</x-layout>
|
</x-layout>
|
||||||
@@ -34,6 +34,17 @@
|
|||||||
<option value="spare" {{ old('type', $product->type) == 'spare' ? 'selected' : '' }}>Repuesto</option>
|
<option value="spare" {{ old('type', $product->type) == 'spare' ? 'selected' : '' }}>Repuesto</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Etiquetas -->
|
||||||
|
<div class="md:col-span-4">
|
||||||
|
<x-forms.label for="tags" value="Etiquetas" />
|
||||||
|
<select id="tags" name="tags[]" multiple="multiple" class="select2-tags w-full">
|
||||||
|
@foreach($tags as $tag)
|
||||||
|
<option value="{{ $tag->id }}" {{ in_array($tag->id, old('tags', $product->tags->pluck('id')->toArray())) ? 'selected' : '' }}>
|
||||||
|
{{ $tag->name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<!-- Precios -->
|
<!-- Precios -->
|
||||||
<div class="">
|
<div class="">
|
||||||
<x-forms.label for="price" value="Precio Venta ($)" />
|
<x-forms.label for="price" value="Precio Venta ($)" />
|
||||||
@@ -68,4 +79,83 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@push('styles')
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||||
|
<style>
|
||||||
|
/* Dark Theme & Neon Lime overrides for Select2 */
|
||||||
|
.select2-container--default .select2-selection--multiple {
|
||||||
|
background-color: rgb(38 38 38); /* neutral-800 */
|
||||||
|
border-color: rgb(64 64 64); /* neutral-700 */
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
.select2-container--default.select2-container--focus .select2-selection--multiple {
|
||||||
|
border-color: #b3e600; /* neon-lime */
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: 0 0 0 1px #b3e600;
|
||||||
|
}
|
||||||
|
.select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||||
|
background-color: #b3e600;
|
||||||
|
border-color: #99c200;
|
||||||
|
color: #171717; /* neutral-900 */
|
||||||
|
}
|
||||||
|
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
||||||
|
color: #171717;
|
||||||
|
}
|
||||||
|
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||||
|
background-color: #99c200;
|
||||||
|
color: #171717;
|
||||||
|
}
|
||||||
|
.select2-dropdown {
|
||||||
|
background-color: rgb(38 38 38);
|
||||||
|
border-color: rgb(64 64 64);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.select2-search--dropdown .select2-search__field {
|
||||||
|
background-color: rgb(23 23 23);
|
||||||
|
border-color: rgb(64 64 64);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.select2-container--default .select2-results__option--selected {
|
||||||
|
background-color: rgb(64 64 64);
|
||||||
|
}
|
||||||
|
.select2-container--default .select2-results__option--highlighted.select2-results__option--selectable {
|
||||||
|
background-color: #b3e600;
|
||||||
|
color: #171717;
|
||||||
|
}
|
||||||
|
.select2-container--default .select2-selection--multiple .select2-search--inline .select2-search__field {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
html:not(.dark) .select2-container--default .select2-selection--multiple {
|
||||||
|
background-color: rgb(209 213 219); /* gray-300 */
|
||||||
|
border-color: rgb(156 163 175); /* neutral-400 */
|
||||||
|
}
|
||||||
|
html:not(.dark) .select2-dropdown {
|
||||||
|
background-color: rgb(209 213 219);
|
||||||
|
border-color: rgb(156 163 175);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
html:not(.dark) .select2-search--dropdown .select2-search__field {
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
html:not(.dark) .select2-container--default .select2-selection--multiple .select2-search--inline .select2-search__field {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('.select2-tags').select2({
|
||||||
|
placeholder: "Buscar y seleccionar etiquetas...",
|
||||||
|
allowClear: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
</x-layout>
|
</x-layout>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<x-layout title="Lauck - Etiquetas">
|
||||||
|
<x-section-header subtitle="Administración" title="Gestión de " highlight="Etiquetas" />
|
||||||
|
|
||||||
|
<div class="w-full max-w-4xl mx-auto space-y-6">
|
||||||
|
<!-- Create Form -->
|
||||||
|
<div class="bg-gray-200 dark:bg-panel-bg border border-neutral-400 dark:border-neutral-800 rounded-xl p-6 shadow-lg">
|
||||||
|
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-4">Nueva Etiqueta</h3>
|
||||||
|
<form action="{{ route('tags.store') }}" method="POST" class="flex flex-col md:flex-row gap-4 items-end">
|
||||||
|
@csrf
|
||||||
|
<div class="w-full md:flex-1">
|
||||||
|
<x-forms.label for="name" value="Nombre de la Etiqueta" />
|
||||||
|
<x-forms.input id="name" name="name" type="text" required placeholder="Ej: Oferta, Temporada Verano" :error="$errors->first('name')" />
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-full md:w-auto px-6 py-2.5 h-[42px] bg-neutral-900 dark:bg-neon-lime text-neon-lime dark:text-neutral-900 font-bold rounded-lg hover:bg-neutral-800 hover:dark:bg-[#b3e600] transition-colors shadow-lg">
|
||||||
|
Guardar
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tags List -->
|
||||||
|
<div class="bg-gray-200 dark:bg-panel-bg border border-neutral-400 dark:border-neutral-800 rounded-xl shadow-lg overflow-hidden">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||||
|
<thead class="text-xs text-gray-700 uppercase bg-gray-300 dark:bg-neutral-800/50 dark:text-gray-400">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="px-6 py-3">Nombre</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-right">Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($tags as $tag)
|
||||||
|
<tr class="bg-gray-200 dark:bg-neutral-900/50 border-b border-gray-300 dark:border-neutral-800 hover:bg-gray-300 dark:hover:bg-neutral-800/80 transition-colors">
|
||||||
|
<td class="px-6 py-4 font-medium text-gray-900 dark:text-white">
|
||||||
|
<form action="{{ route('tags.update', $tag) }}" method="POST" class="flex gap-2 items-center" id="form-edit-{{ $tag->id }}">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
<input type="text" name="name" value="{{ $tag->name }}" class="bg-transparent border-b border-transparent focus:border-neon-lime focus:ring-0 text-sm p-1 rounded-sm text-gray-900 dark:text-white w-full max-w-xs transition-colors" required>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-right space-x-2 whitespace-nowrap">
|
||||||
|
<button type="button" onclick="document.getElementById('form-edit-{{ $tag->id }}').submit()" class="font-medium text-blue-600 dark:text-blue-500 hover:underline">Guardar</button>
|
||||||
|
<form action="{{ route('tags.destroy', $tag) }}" method="POST" class="inline-block">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="font-medium text-red-600 dark:text-red-500 hover:underline" onclick="return confirm('¿Seguro que deseas eliminar esta etiqueta?')">Eliminar</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||||
|
No hay etiquetas creadas.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-layout>
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
<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"/>
|
||||||
|
|
||||||
<!-- CARROUSEL DESTACADO -->
|
<!-- CARROUSELES POR ETIQUETA -->
|
||||||
<div class="w-full mb-12">
|
<div class="w-full mb-12 space-y-16">
|
||||||
<!-- datos del controlador -->
|
@forelse($tagsConProductos as $tag)
|
||||||
<x-ui.carrousel :items="$destacados" />
|
<div>
|
||||||
|
<x-ui.carrousel :items="$tag->products" title="{{ $tag->name }}" />
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="text-center py-12 bg-slate-100 dark:bg-panel-bg rounded-xl border border-neutral-200 dark:border-neutral-800">
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 font-medium">Aún no hay productos etiquetados para mostrar.</p>
|
||||||
|
</div>
|
||||||
|
@endforelse
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</x-layout>
|
</x-layout>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use App\Http\Controllers\AppointmentController;
|
|||||||
use App\Http\Controllers\SupplierController;
|
use App\Http\Controllers\SupplierController;
|
||||||
use App\Http\Controllers\PasswordResetLinkController;
|
use App\Http\Controllers\PasswordResetLinkController;
|
||||||
use App\Http\Controllers\NewPasswordController;
|
use App\Http\Controllers\NewPasswordController;
|
||||||
|
use App\Http\Controllers\TagController;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Session;
|
use Illuminate\Support\Facades\Session;
|
||||||
@@ -61,6 +62,7 @@ Route::middleware(['auth'])->group(function () {
|
|||||||
Route::resource('suppliers', SupplierController::class);
|
Route::resource('suppliers', SupplierController::class);
|
||||||
Route::resource('expenses', \App\Http\Controllers\ExpenseController::class);
|
Route::resource('expenses', \App\Http\Controllers\ExpenseController::class);
|
||||||
Route::get('reports', [\App\Http\Controllers\ReportController::class, 'index'])->name('reports.index');
|
Route::get('reports', [\App\Http\Controllers\ReportController::class, 'index'])->name('reports.index');
|
||||||
|
Route::resource('tags', TagController::class)->except(['create', 'show']);
|
||||||
Route::resource('productos', ProductosController::class)->parameters([
|
Route::resource('productos', ProductosController::class)->parameters([
|
||||||
'productos' => 'product'
|
'productos' => 'product'
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user