Add rank management feature with CRUD and UI

Introduces a new rank management system including the Rank model, migrations for the ranks table and user IP address, Livewire components for managing and displaying ranks, and related Blade views. Updates navigation and routes to include the new 'Gestión Rangos' section, and enhances the ShareCodeComponent with user data. Also adds a reusable input field component for forms.
This commit is contained in:
Juan Felipe Duarte
2025-10-23 20:28:47 -05:00
parent aa67099449
commit a558e393f2
17 changed files with 522 additions and 6 deletions
@@ -0,0 +1,16 @@
@props(['label', 'model', 'type' => 'text', 'step' => null])
<div class="flex flex-col space-y-1">
<label class="block text-sm font-medium text-gray-700">{{ $label }}</label>
<input
type="{{ $type }}"
@if($step) step="{{ $step }}" @endif
wire:model="{{ $model }}"
class="border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500 w-full text-gray-800"
/>
@error($model)
<span class="text-xs text-red-500">{{ $message }}</span>
@enderror
</div>
@@ -182,6 +182,12 @@
</a>
@if (auth()->user()->rol->nombre == 'super')
<a href="{{ route('gestion-rangos') }}" class="flex items-center gap-2 py-2 px-3 rounded text-white text-left {{ request()->routeIs('gestion-rangos') ? 'bg-white/20' : 'hover:bg-white/10' }}">
<svg class="w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14" />
</svg>
<span>Gestión Rangos</span>
</a>
<a href="{{ route('tarifas') }}" class="flex items-center gap-2 py-2 px-3 rounded text-white text-left {{ request()->routeIs('tarifas') ? 'bg-white/20' : 'hover:bg-white/10' }}">
<svg class="w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14" />
@@ -1,3 +1,88 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 my-12">
<div class="flex items-center justify-center gap-4">
<div>
<img src="https://cdn.imgbin.com/11/2/10/imgbin-metal-bronze-copper-platinum-bronzing-wRAqc4xeeYJw5RmknN6xuYZND.jpg"
alt="Rango Diamante" class="w-24 h-24 rounded-full object-cover">
</div>
<div class="flex flex-col items-center justify-center gap-1 text-center">
<p class="text-lg">Rango actual</p>
<p class="text-3xl font-semibold">Bronce</p>
<div class="w-48 bg-gray-700 rounded-full h-2 mt-1">
<div class="bg-blue-500 h-2 rounded-full transition-all duration-1000 ease-linear"
style="width: {{ (4 / 5) * 100 }}%">
</div>
</div>
</div>
</div>
<p class="py-4 text-center px-4 text-sm">Te faltan $120,000 para llegar a Plata</p>
<div class="grid grid-cols-2 gap-4">
<!-- Tarjeta 1 -->
<div class="flex flex-col items-center justify-center p-6 rounded-xl shadow-md transition-transform duration-300">
<div class="flex gap-2 items-start">
<svg class="w-10 pt-1" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 0 0-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 0 0-16.536-1.84M7.5 14.25 5.106 5.272M6 20.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm12.75 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" />
</svg>
<p class="text-sm">Compra personal</p>
</div>
<p class="text-lg font-semibold mt-2">$70,000</p>
</div>
<!-- Tarjeta 2 -->
<div class="flex flex-col items-center justify-center p-6 rounded-xl shadow-md transition-transform duration-300">
<div class="flex gap-2 items-start">
<svg class="w-10 pt-1" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0 1 15.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 0 1 3 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 0 0-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 0 1-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 0 0 3 15h-.75M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm3 0h.008v.008H18V10.5Zm-12 0h.008v.008H6V10.5Z" />
</svg>
<p class="text-sm">Cashback actual</p>
</div>
<p class="text-lg font-semibold mt-2">$25,000</p>
</div>
<!-- Tarjeta 3 -->
<div class="flex flex-col items-center justify-center p-6 rounded-xl shadow-md transition-transform duration-300">
<div class="flex gap-2 items-start">
<svg class="w-10 pt-1" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" />
</svg>
<p class="text-sm">Volumen de equipo</p>
</div>
<p class="text-lg font-semibold mt-2">$1,250,000</p>
</div>
<!-- Tarjeta 4 -->
<div class="flex flex-col items-center justify-center p-6 rounded-xl shadow-md transition-transform duration-300">
<div class="flex gap-2 items-start">
<svg class="w-10 pt-1" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
</svg>
<p class="text-sm">Directos activos</p>
</div>
<p class="text-lg font-semibold mt-2">4</p>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 my-12">
<div>
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
@@ -0,0 +1,130 @@
<div
x-data="{
modal : @entangle('modal'),
}"
class="p-6 bg-gray-50 min-h-screen">
<div class="grid grid-cols-1 text-center sm:flex justify-between my-2 items-center text-sm">
<div class="grid grid-cols-1 sm:flex items-center gap-4">
<div class="flex text-sm">
<x-icon-arrow-right />
<h1>Rangos</h1>
</div>
</div>
<div class="flex gap-4">
<button wire:click="crear" class="justify-center gap-2 flex px-4 py-2 rounded-lg cursor-pointer text-white bg-indigo-600 hover:bg-indigo-700 mb-3 md:mb-0">
Nuevo Rango
<svg class="w-3" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z" fill="#ffffff" />
</svg>
</button>
</div>
</div>
<x-primary-table>
<x-slot name="thead">
<tr class="text-left">
<th class="px-4 py-3">Logo</th>
<th class="px-4 py-3">Nombre</th>
<th class="px-4 py-3">Cashback</th>
<th class="px-4 py-3">Bono Equipo</th>
<th class="px-4 py-3">Tope</th>
<th class="px-4 py-3">Compra Mínima</th>
<th class="px-4 py-3">Directos</th>
<th class="px-4 py-3">Niveles</th>
<th class="px-4 py-3">Volumen</th>
<th class="px-4 py-3">Por Nivel</th>
<th></th>
</tr>
</x-slot>
<x-slot name="tbody">
@forelse ($ranks as $rank)
<tr class="border-b hover:bg-gray-50 transition text-left" x-data="{ openOption: false }">
<td class="px-4 py-3">
@if($rank->logo)
<img src="{{ asset('storage/' . $rank->logo) }}" alt="{{ $rank->nombre }}" class="h-10 w-10 rounded-full object-cover">
@endif
</td>
<td class="px-4 py-3">{{ $rank->cashback }}%</td>
<td class="px-4 py-3">{{ $rank->cashback }}%</td>
<td class="px-4 py-3">{{ $rank->bono_equipo }}%</td>
<td class="px-4 py-3">${{ number_format($rank->tope_bono_equipo) }}</td>
<td class="px-4 py-3">${{ number_format($rank->compra_personal_min) }}</td>
<td class="px-4 py-3">{{ $rank->directos_activos_min }}</td>
<td class="px-4 py-3">{{ $rank->niveles_min }}</td>
<td class="px-4 py-3">${{ number_format($rank->volumen_equipo_min) }}</td>
<td class="px-4 py-3">${{ number_format($rank->volumen_por_nivel_min) }}</td>
<td>
<button x-on:click="openOption=!openOption">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 cursor-pointer">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM12.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM18.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" />
</svg>
</button>
<div x-cloak x-show="openOption" @click.away="openOption = false" class="sm:absolute bg-white text-center w-[8rem] rounded-[1rem] border-b border-gray-200 shadow-lg right-10">
<button x-on:click="openOption=false" wire:click="editar({{ $rank->id }})" class="text-gray-500 w-full py-1 hover:bg-gray-200 border-b border-gray-300 hover:rounded-t-[1rem]">Editar</button>
<button x-on:click="openOption=false" wire:click="eliminar({{ $rank->id }})" class="text-white w-full py-1 bg-red-400 rounded-b-[1rem] hover:bg-red-600 hover:rounded-b-[1rem]">Eliminar</button>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="10" class="text-center py-6 text-gray-500">No hay rangos registrados aún.</td>
</tr>
@endforelse
</x-slot>
<x-slot name="tlink">
</x-slot>
</x-primary-table>
{{-- Modal --}}
<div x-cloak x-show="modal" x-transition:enter="transition duration-350"
x-transition:enter-start="opacity-0 scale-100" x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition duration-350" x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-100" class="fixed inset-0 z-50 backdrop-blur-sm bg-black/20">
<div class="grid h-full place-items-center overflow-y-auto ">
<div class="p-6 bg-white rounded-xl">
<h2 class="text-lg font-bold mb-4">{{ $rank_id ? 'Editar Rango' : 'Nuevo Rango' }}</h2>
<form wire:submit.prevent="guardar" class="grid grid-cols-2 gap-4">
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">Logo del Rango</label>
<input type="file" wire:model="logo"
class="block w-full text-sm text-gray-700 border border-gray-300 rounded-lg cursor-pointer focus:ring-indigo-500 focus:border-indigo-500" />
@error('logo') <span class="text-xs text-red-500">{{ $message }}</span> @enderror
{{-- Preview --}}
@if ($logo)
<div class="mt-3">
<img src="{{ $logo->temporaryUrl() }}" class="h-20 rounded-lg shadow-md" accept="image/*">
</div>
@elseif ($logo_url)
<div class="mt-3">
<img src="{{ asset('storage/' . $logo_url) }}" class="h-20 rounded-lg shadow-md" accept="image/*">
</div>
@endif
</div>
<x-input-field label="Nombre" model="nombre" />
<x-input-field label="Cashback (%)" model="cashback" type="number" step="0.01" />
<x-input-field label="Bono de Equipo (%)" model="bono_equipo" type="number" step="0.01" />
<x-input-field label="Tope del Bono" model="tope_bono_equipo" type="number" />
<x-input-field label="Compra Personal Mínima" model="compra_personal_min" type="number" />
<x-input-field label="Directos Activos Mínimos" model="directos_activos_min" type="number" />
<x-input-field label="Niveles Mínimos" model="niveles_min" type="number" />
<x-input-field label="Volumen de Equipo Mínimo" model="volumen_equipo_min" type="number" />
<x-input-field label="Volumen por Nivel" model="volumen_por_nivel_min" type="number" />
</form>
<div class="flex justify-end mt-6 space-x-3">
<button wire:click="$set('modal', false)" type="button" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded-lg">
Cancelar
</button>
<button wire:click="guardar" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg">
Guardar
</button>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,3 @@
<div>
{{-- The whole world belongs to you. --}}
</div>
@@ -0,0 +1,3 @@
<x-app-layout>
<livewire:show-gestion-rangos />
</x-app-layout>