From a558e393f208d1291aab4e8c1c9b82a4a478cb92 Mon Sep 17 00:00:00 2001 From: Juan Felipe Duarte <70235683+DuarteJFelipe@users.noreply.github.com> Date: Thu, 23 Oct 2025 20:28:47 -0500 Subject: [PATCH] Add rank management feature with CRUD and UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- app/Http/Controllers/MenuController.php | 4 + app/Http/Livewire/ShareCodeComponent.php | 7 + app/Http/Livewire/ShowGestionRangos.php | 132 ++++++++++++++++++ app/Http/Livewire/ShowRangos.php | 13 ++ app/Http/Livewire/ShowSavingIp.php | 2 +- app/Models/Rank.php | 29 ++++ app/Models/User.php | 10 +- app/View/Components/InputField.php | 26 ++++ ...025_10_18_114653_add_ip_to_users_table.php | 22 +++ .../2025_10_23_130227_create_ranks_table.php | 39 ++++++ .../views/components/input-field.blade.php | 16 +++ resources/views/layouts/navigation.blade.php | 6 + .../livewire/share-code-component.blade.php | 85 +++++++++++ .../livewire/show-gestion-rangos.blade.php | 130 +++++++++++++++++ .../views/livewire/show-rangos.blade.php | 3 + resources/views/menu/gestion-rangos.blade.php | 3 + routes/web.php | 1 + 17 files changed, 522 insertions(+), 6 deletions(-) create mode 100644 app/Http/Livewire/ShowGestionRangos.php create mode 100644 app/Http/Livewire/ShowRangos.php create mode 100644 app/Models/Rank.php create mode 100644 app/View/Components/InputField.php create mode 100644 database/migrations/2025_10_18_114653_add_ip_to_users_table.php create mode 100644 database/migrations/2025_10_23_130227_create_ranks_table.php create mode 100644 resources/views/components/input-field.blade.php create mode 100644 resources/views/livewire/show-gestion-rangos.blade.php create mode 100644 resources/views/livewire/show-rangos.blade.php create mode 100644 resources/views/menu/gestion-rangos.blade.php diff --git a/app/Http/Controllers/MenuController.php b/app/Http/Controllers/MenuController.php index 86abec5..5795220 100755 --- a/app/Http/Controllers/MenuController.php +++ b/app/Http/Controllers/MenuController.php @@ -42,6 +42,10 @@ class MenuController extends Controller { return view('menu/tarifas'); } + public function showgestionrangos() + { + return view('menu/gestion-rangos'); + } public function showclientes() { return view('menu/clientes'); diff --git a/app/Http/Livewire/ShareCodeComponent.php b/app/Http/Livewire/ShareCodeComponent.php index 3b06132..3f8b40e 100644 --- a/app/Http/Livewire/ShareCodeComponent.php +++ b/app/Http/Livewire/ShareCodeComponent.php @@ -7,6 +7,13 @@ use Livewire\Component; class ShareCodeComponent extends Component { + public $user; + + public function mount() + { + $this->user = Auth::user(); + } + public function render() { $referrals = Auth::user()->children() diff --git a/app/Http/Livewire/ShowGestionRangos.php b/app/Http/Livewire/ShowGestionRangos.php new file mode 100644 index 0000000..8bbe8bc --- /dev/null +++ b/app/Http/Livewire/ShowGestionRangos.php @@ -0,0 +1,132 @@ + 'required|string|max:100', + 'cashback' => 'required|numeric|min:0', + 'bono_equipo' => 'required|numeric|min:0', + 'tope_bono_equipo' => 'required|numeric|min:0', + 'compra_personal_min' => 'required|numeric|min:0', + 'directos_activos_min' => 'required|integer|min:0', + 'niveles_min' => 'required|integer|min:0', + 'volumen_equipo_min' => 'required|numeric|min:0', + 'volumen_por_nivel_min' => 'required|numeric|min:0', + 'logo' => 'nullable|image|max:2048', + ]; + + protected $messages = [ + 'nombre.required' => 'El nombre del rango es obligatorio.', + 'nombre.string' => 'El nombre debe ser una cadena de texto válida.', + 'nombre.max' => 'El nombre no puede tener más de 100 caracteres.', + + 'cashback.required' => 'El porcentaje de cashback es obligatorio.', + 'cashback.numeric' => 'El cashback debe ser un valor numérico.', + 'cashback.min' => 'El cashback no puede ser menor a 0.', + + 'bono_equipo.required' => 'El porcentaje de bono de equipo es obligatorio.', + 'bono_equipo.numeric' => 'El bono de equipo debe ser un valor numérico.', + 'bono_equipo.min' => 'El bono de equipo no puede ser menor a 0.', + + 'tope_bono_equipo.required' => 'El tope del bono de equipo es obligatorio.', + 'tope_bono_equipo.numeric' => 'El tope del bono de equipo debe ser un valor numérico.', + 'tope_bono_equipo.min' => 'El tope del bono de equipo no puede ser menor a 0.', + + 'compra_personal_min.required' => 'La compra personal mínima es obligatoria.', + 'compra_personal_min.numeric' => 'La compra personal mínima debe ser un número.', + 'compra_personal_min.min' => 'La compra personal mínima no puede ser menor a 0.', + + 'directos_activos_min.required' => 'El número de directos activos mínimos es obligatorio.', + 'directos_activos_min.integer' => 'Los directos activos deben ser un número entero.', + 'directos_activos_min.min' => 'Los directos activos no pueden ser menores a 0.', + + 'niveles_min.required' => 'El número de niveles mínimos es obligatorio.', + 'niveles_min.integer' => 'Los niveles mínimos deben ser un número entero.', + 'niveles_min.min' => 'Los niveles mínimos no pueden ser menores a 0.', + + 'volumen_equipo_min.required' => 'El volumen mínimo de equipo es obligatorio.', + 'volumen_equipo_min.numeric' => 'El volumen mínimo de equipo debe ser un número.', + 'volumen_equipo_min.min' => 'El volumen mínimo de equipo no puede ser menor a 0.', + + 'volumen_por_nivel_min.required' => 'El volumen por nivel mínimo es obligatorio.', + 'volumen_por_nivel_min.numeric' => 'El volumen por nivel mínimo debe ser un número.', + 'volumen_por_nivel_min.min' => 'El volumen por nivel mínimo no puede ser menor a 0.', + + 'logo.image' => 'El archivo del logo debe ser una imagen válida (JPG, PNG, etc.).', + 'logo.max' => 'El logo no puede superar los 2 MB.', + ]; + + public function render() + { + $this->ranks = Rank::orderBy('volumen_equipo_min')->get(); + + return view('livewire.show-gestion-rangos'); + } + + public function crear() + { + $this->resetExcept('ranks'); + $this->modal = true; + } + + public function editar($id) + { + $rank = Rank::findOrFail($id); + $this->fill($rank->toArray()); + $this->rank_id = $rank->id; + $this->modal = true; + } + + public function guardar() + { + $this->validate(); + + $rutaLogo = null; + + if ($this->logo) { + $rutaLogo = $this->logo->store('photos'); + } + + Rank::updateOrCreate( + ['id' => $this->rank_id], + [ + 'nombre' => $this->nombre, + 'cashback' => $this->cashback, + 'bono_equipo' => $this->bono_equipo, + 'tope_bono_equipo' => $this->tope_bono_equipo, + 'compra_personal_min' => $this->compra_personal_min, + 'directos_activos_min' => $this->directos_activos_min, + 'niveles_min' => $this->niveles_min, + 'volumen_equipo_min' => $this->volumen_equipo_min, + 'volumen_por_nivel_min' => $this->volumen_por_nivel_min, + 'logo' => $rutaLogo ?? $this->logo_url, + ] + ); + + $this->modal = false; + $this->resetExcept('ranks'); + } + + public function eliminar($id) + { + Rank::findOrFail($id)->delete(); + } +} diff --git a/app/Http/Livewire/ShowRangos.php b/app/Http/Livewire/ShowRangos.php new file mode 100644 index 0000000..1b6fa49 --- /dev/null +++ b/app/Http/Livewire/ShowRangos.php @@ -0,0 +1,13 @@ +hasMany(User::class, 'rank_id'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 9b024df..06c117e 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -23,11 +23,11 @@ class User extends Authenticatable 'rol_id', 'subvendedor_id', 'subvendedor', + 'ip_address', 'referral_code', 'parent_id', - 'rank_id', ]; @@ -126,10 +126,10 @@ class User extends Authenticatable return $this->hasMany(User::class, 'parent_id')->orderBy('created_at', 'desc'); } - // public function rank() - // { - // return $this->belongsTo(Rank::class); - // } + public function rank() + { + return $this->belongsTo(Rank::class, 'rank_id'); + } protected static function boot() { diff --git a/app/View/Components/InputField.php b/app/View/Components/InputField.php new file mode 100644 index 0000000..5ac5de9 --- /dev/null +++ b/app/View/Components/InputField.php @@ -0,0 +1,26 @@ +label = $label; + $this->model = $model; + $this->type = $type; + $this->step = $step; + } + + public function render() + { + return view('components.input-field'); + } +} diff --git a/database/migrations/2025_10_18_114653_add_ip_to_users_table.php b/database/migrations/2025_10_18_114653_add_ip_to_users_table.php new file mode 100644 index 0000000..4f3c3da --- /dev/null +++ b/database/migrations/2025_10_18_114653_add_ip_to_users_table.php @@ -0,0 +1,22 @@ +string('ip_address')->nullable(); + }); + } + + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('ip_address'); + }); + } +}; diff --git a/database/migrations/2025_10_23_130227_create_ranks_table.php b/database/migrations/2025_10_23_130227_create_ranks_table.php new file mode 100644 index 0000000..3b1b734 --- /dev/null +++ b/database/migrations/2025_10_23_130227_create_ranks_table.php @@ -0,0 +1,39 @@ +id(); + + $table->string('nombre'); // Bronce, Plata, Oro, Diamante + + $table->string('logo')->nullable(); + + $table->decimal('cashback', 5, 2)->default(0); // % de cashback personal + $table->decimal('bono_equipo', 5, 2)->default(0); // % de bono sobre ventas del equipo + $table->integer('tope_bono_equipo')->default(0); // límite máximo del bono + + // Requisitos + $table->integer('compra_personal_min')->default(0); + $table->integer('directos_activos_min')->default(0); + $table->integer('niveles_min')->default(0); + $table->integer('volumen_equipo_min')->default(0); + + // Valor mínimo por nivel (ej: cada nivel >= 300.000) + $table->integer('volumen_por_nivel_min')->default(0); + + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('ranks'); + } +}; diff --git a/resources/views/components/input-field.blade.php b/resources/views/components/input-field.blade.php new file mode 100644 index 0000000..2ce1142 --- /dev/null +++ b/resources/views/components/input-field.blade.php @@ -0,0 +1,16 @@ +@props(['label', 'model', 'type' => 'text', 'step' => null]) + +
+ + + + + @error($model) + {{ $message }} + @enderror +
diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index 5fd7925..5f700ff 100755 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -182,6 +182,12 @@ @if (auth()->user()->rol->nombre == 'super') + + + + + Gestión Rangos + diff --git a/resources/views/livewire/share-code-component.blade.php b/resources/views/livewire/share-code-component.blade.php index b7d8315..3a7b9b9 100644 --- a/resources/views/livewire/share-code-component.blade.php +++ b/resources/views/livewire/share-code-component.blade.php @@ -1,3 +1,88 @@ +
+
+
+ Rango Diamante +
+ +
+

Rango actual

+

Bronce

+
+
+
+
+
+
+ +

Te faltan $120,000 para llegar a Plata

+ + +
+ +
+
+ + + +

Compra personal

+
+

$70,000

+
+ + +
+
+ + + + +

Cashback actual

+
+

$25,000

+
+ + +
+
+ + + + +

Volumen de equipo

+
+

$1,250,000

+
+ + +
+
+ + + +

Directos activos

+
+

4

+
+
+ +
+ + + + + + + + + + + + +
diff --git a/resources/views/livewire/show-gestion-rangos.blade.php b/resources/views/livewire/show-gestion-rangos.blade.php new file mode 100644 index 0000000..d94d36d --- /dev/null +++ b/resources/views/livewire/show-gestion-rangos.blade.php @@ -0,0 +1,130 @@ +
+
+
+
+ +

Rangos

+
+
+ +
+ +
+
+ + + + + Logo + Nombre + Cashback + Bono Equipo + Tope + Compra Mínima + Directos + Niveles + Volumen + Por Nivel + + + + + @forelse ($ranks as $rank) + + + @if($rank->logo) + {{ $rank->nombre }} + @endif + + {{ $rank->cashback }}% + {{ $rank->cashback }}% + {{ $rank->bono_equipo }}% + ${{ number_format($rank->tope_bono_equipo) }} + ${{ number_format($rank->compra_personal_min) }} + {{ $rank->directos_activos_min }} + {{ $rank->niveles_min }} + ${{ number_format($rank->volumen_equipo_min) }} + ${{ number_format($rank->volumen_por_nivel_min) }} + + + +
+ + +
+ + + @empty + + No hay rangos registrados aún. + + @endforelse +
+ + +
+ + {{-- Modal --}} +
+
+ +
+

{{ $rank_id ? 'Editar Rango' : 'Nuevo Rango' }}

+ +
+
+ + + @error('logo') {{ $message }} @enderror + + {{-- Preview --}} + @if ($logo) +
+ +
+ @elseif ($logo_url) +
+ +
+ @endif +
+ + + + + + + + + + + +
+ + +
+
+
+
+
diff --git a/resources/views/livewire/show-rangos.blade.php b/resources/views/livewire/show-rangos.blade.php new file mode 100644 index 0000000..15e5660 --- /dev/null +++ b/resources/views/livewire/show-rangos.blade.php @@ -0,0 +1,3 @@ +
+ {{-- The whole world belongs to you. --}} +
diff --git a/resources/views/menu/gestion-rangos.blade.php b/resources/views/menu/gestion-rangos.blade.php new file mode 100644 index 0000000..d357e67 --- /dev/null +++ b/resources/views/menu/gestion-rangos.blade.php @@ -0,0 +1,3 @@ + + + diff --git a/routes/web.php b/routes/web.php index 45411a2..d69b70d 100755 --- a/routes/web.php +++ b/routes/web.php @@ -71,6 +71,7 @@ Route::middleware(["auth", "solo_usuario_administrador"])->group(function () { Route::any('/log-historial', [MenuController::class, 'showLogHistorial'])->name('logHistorial'); Route::get('/utilidades', [MenuController::class, 'showutilidad'])->name('utilidades'); Route::get('/mantenimiento', [MenuController::class, 'mantenimiento'])->name('mantenimiento'); + Route::get('/rank-management', [MenuController::class, 'showgestionrangos'])->name('gestion-rangos'); }); Route::post('/registrar-accion-compra', [MenuController::class, 'click_compra'])->name('click_compra');