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ó
|
||||
$query = $request->input('search');
|
||||
$type = $request->input('type');
|
||||
$tagFilter = $request->input('tag');
|
||||
|
||||
// --- CONSULTA PARA LA GRILLA (PAGINADA) ---
|
||||
$productsQuery = Product::query();
|
||||
$productsQuery = Product::with('tags');
|
||||
|
||||
$productsQuery->when($query, function ($q) use ($query) {
|
||||
return $q->where('name', 'like', "%{$query}%");
|
||||
@@ -29,6 +30,12 @@ class CatalogoController extends Controller
|
||||
$productsQuery->where('type', '!=', 'service');
|
||||
}
|
||||
|
||||
if ($tagFilter) {
|
||||
$productsQuery->whereHas('tags', function($q) use ($tagFilter) {
|
||||
$q->where('name', $tagFilter);
|
||||
});
|
||||
}
|
||||
|
||||
//mostrar si tiene stock mayor a 0
|
||||
$productsQuery->where('stock_quantity', '>', 0);
|
||||
|
||||
@@ -40,8 +47,10 @@ class CatalogoController extends Controller
|
||||
->orderBy('name', 'asc')
|
||||
->get();
|
||||
|
||||
$allTags = \App\Models\Tag::orderBy('name')->get();
|
||||
|
||||
// Devuelve la vista
|
||||
return view('catalogo.index', compact('products', 'allProducts'));
|
||||
return view('catalogo.index', compact('products', 'allProducts', 'allTags'));
|
||||
}
|
||||
|
||||
public function show(Product $product)
|
||||
|
||||
@@ -9,17 +9,18 @@ class HomeController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
// 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();
|
||||
// Traer todas las etiquetas con sus productos
|
||||
$tagsConProductos = \App\Models\Tag::with('products')->get()
|
||||
->filter(function ($tag) {
|
||||
// Solo mantener etiquetas que tengan al menos 1 producto
|
||||
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
|
||||
if ($destacados->isEmpty()) {
|
||||
$destacados = Product::take(5)->get();
|
||||
}
|
||||
|
||||
return view('welcome', compact('destacados'));
|
||||
return view('welcome', compact('tagsConProductos'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Models\Supplier;
|
||||
use App\Models\Tag;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@@ -55,7 +56,8 @@ class ProductosController extends Controller
|
||||
public function create()
|
||||
{
|
||||
$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)
|
||||
@@ -79,7 +81,11 @@ class ProductosController extends Controller
|
||||
}
|
||||
unset($validated['image']);
|
||||
|
||||
Product::create($validated);
|
||||
$product = Product::create($validated);
|
||||
|
||||
if ($request->has('tags')) {
|
||||
$product->tags()->sync($request->tags);
|
||||
}
|
||||
|
||||
return redirect()->route('productos.index')
|
||||
->with('success', 'Producto creado correctamente.');
|
||||
@@ -93,7 +99,8 @@ class ProductosController extends Controller
|
||||
public function edit(Product $product)
|
||||
{
|
||||
$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)
|
||||
@@ -123,6 +130,12 @@ class ProductosController extends Controller
|
||||
|
||||
$product->update($validated);
|
||||
|
||||
if ($request->has('tags')) {
|
||||
$product->tags()->sync($request->tags);
|
||||
} else {
|
||||
$product->tags()->detach();
|
||||
}
|
||||
|
||||
return redirect()->route('productos.index')
|
||||
->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');
|
||||
}
|
||||
|
||||
public function tags()
|
||||
{
|
||||
return $this->belongsToMany(Tag::class);
|
||||
}
|
||||
}
|
||||
/**$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">
|
||||
Filtrar
|
||||
</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">
|
||||
<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"/>
|
||||
@@ -90,8 +90,25 @@
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="tag" value="{{ request('tag') }}">
|
||||
</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 -->
|
||||
@if ($products->count() > 0)
|
||||
@@ -113,6 +130,17 @@
|
||||
<!-- 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>
|
||||
|
||||
{{-- 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 --}}
|
||||
@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>
|
||||
|
||||
@@ -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>
|
||||
<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 -->
|
||||
<div class="flex items-center gap-2 mt-4">
|
||||
@if($product->stock_quantity > 5)
|
||||
|
||||
@@ -41,6 +41,12 @@
|
||||
Stock
|
||||
</a>
|
||||
</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>
|
||||
<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' }}">
|
||||
|
||||
@@ -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) -->
|
||||
<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>
|
||||
|
||||
<!-- 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)
|
||||
<div class="w-full flex-shrink-0 flex flex-col md:flex-row h-full relative">
|
||||
|
||||
@@ -61,11 +65,11 @@
|
||||
</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">
|
||||
<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>
|
||||
</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>
|
||||
</button>
|
||||
|
||||
@@ -80,7 +84,8 @@
|
||||
|
||||
<script type="module">
|
||||
$(document).ready(function() {
|
||||
const $track = $('#carousel-track');
|
||||
const $container = $('#carousel-{{ $id }}');
|
||||
const $track = $container.find('.carousel-track');
|
||||
const $slides = $track.children();
|
||||
const slideCount = $slides.length;
|
||||
let currentIndex = 0;
|
||||
@@ -91,8 +96,8 @@
|
||||
$track.css('transform', `translateX(${translateX}%)`);
|
||||
|
||||
// Actualizar puntos
|
||||
$('.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').removeClass('bg-lime-500 dark:bg-neon-lime scale-125').addClass('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() {
|
||||
@@ -106,17 +111,17 @@
|
||||
}
|
||||
|
||||
// Event Listeners
|
||||
$('#nextBtn').click(function() {
|
||||
$container.find('.nextBtn').click(function() {
|
||||
nextSlide();
|
||||
resetAutoPlay();
|
||||
});
|
||||
|
||||
$('#prevBtn').click(function() {
|
||||
$container.find('.prevBtn').click(function() {
|
||||
prevSlide();
|
||||
resetAutoPlay();
|
||||
});
|
||||
|
||||
$('.carousel-dot').click(function() {
|
||||
$container.find('.carousel-dot').click(function() {
|
||||
currentIndex = $(this).data('index');
|
||||
updateCarousel();
|
||||
resetAutoPlay();
|
||||
|
||||
@@ -35,6 +35,17 @@
|
||||
<option value="spare" {{ old('type') == 'spare' ? 'selected' : '' }}>Repuesto</option>
|
||||
</select>
|
||||
</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 -->
|
||||
<div class="">
|
||||
<x-forms.label for="price" value="Precio Venta ($)" />
|
||||
@@ -72,4 +83,83 @@
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
@@ -34,6 +34,17 @@
|
||||
<option value="spare" {{ old('type', $product->type) == 'spare' ? 'selected' : '' }}>Repuesto</option>
|
||||
</select>
|
||||
</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 -->
|
||||
<div class="">
|
||||
<x-forms.label for="price" value="Precio Venta ($)" />
|
||||
@@ -68,4 +79,83 @@
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
@@ -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-section-header subtitle="Inicio" title="Bicicleteria " highlight="Lauck"/>
|
||||
|
||||
<!-- CARROUSEL DESTACADO -->
|
||||
<div class="w-full mb-12">
|
||||
<!-- datos del controlador -->
|
||||
<x-ui.carrousel :items="$destacados" />
|
||||
<!-- CARROUSELES POR ETIQUETA -->
|
||||
<div class="w-full mb-12 space-y-16">
|
||||
@forelse($tagsConProductos as $tag)
|
||||
<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>
|
||||
|
||||
</x-layout>
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Http\Controllers\AppointmentController;
|
||||
use App\Http\Controllers\SupplierController;
|
||||
use App\Http\Controllers\PasswordResetLinkController;
|
||||
use App\Http\Controllers\NewPasswordController;
|
||||
use App\Http\Controllers\TagController;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
@@ -61,6 +62,7 @@ Route::middleware(['auth'])->group(function () {
|
||||
Route::resource('suppliers', SupplierController::class);
|
||||
Route::resource('expenses', \App\Http\Controllers\ExpenseController::class);
|
||||
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([
|
||||
'productos' => 'product'
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user