Feat: Agregada gestión de tablas ISR en settings
- Nueva tabla isr_tables y isr_brackets en BD - Controlador IsrController para CRUD de tablas ISR - Integración con pestaña ISR en settings - Soporte para importación via CSV - Captura manual de brackets
This commit is contained in:
228
app/Http/Controllers/IsrController.php
Normal file
228
app/Http/Controllers/IsrController.php
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\IsrBracket;
|
||||||
|
use App\Models\IsrTable;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
class IsrController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Lista todas las tablas ISR
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$isrTables = IsrTable::with('brackets')->orderBy('year', 'desc')->get();
|
||||||
|
return view('settings.isr.index', compact('isrTables'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea nueva tabla por año
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'year' => 'required|integer|min:2000|max:2100|unique:isr_tables,year',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return redirect()->route('settings.index', ['tab' => 'isr'])
|
||||||
|
->withErrors($validator)
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$isrTable = IsrTable::create([
|
||||||
|
'year' => $request->input('year'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('settings.index', ['tab' => 'isr'])
|
||||||
|
->with('success', 'Tabla ISR para ' . $isrTable->year . ' creada. Ahora agrega los brackets.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formulario edición de brackets
|
||||||
|
*/
|
||||||
|
public function edit(IsrTable $isrTable)
|
||||||
|
{
|
||||||
|
$isrTable->load('brackets');
|
||||||
|
return view('settings.isr.edit', compact('isrTable'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guarda brackets manuales
|
||||||
|
*/
|
||||||
|
public function updateBrackets(Request $request, IsrTable $isrTable)
|
||||||
|
{
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'brackets' => 'required|array|min:1',
|
||||||
|
'brackets.*.lower_limit' => 'required|numeric|min:0',
|
||||||
|
'brackets.*.upper_limit' => 'nullable|numeric|min:0',
|
||||||
|
'brackets.*.fixed_fee' => 'required|numeric|min:0',
|
||||||
|
'brackets.*.rate' => 'required|numeric|min:0|max:100',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return redirect()->route('settings.isr.edit', $isrTable)
|
||||||
|
->withErrors($validator)
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminar brackets existentes
|
||||||
|
$isrTable->brackets()->delete();
|
||||||
|
|
||||||
|
// Crear nuevos brackets
|
||||||
|
$bracketsData = $request->input('brackets');
|
||||||
|
$order = 0;
|
||||||
|
|
||||||
|
// Ordenar brackets por lower_limit antes de guardar
|
||||||
|
usort($bracketsData, function ($a, $b) {
|
||||||
|
return floatval($a['lower_limit']) - floatval($b['lower_limit']);
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach ($bracketsData as $bracketData) {
|
||||||
|
$isrTable->brackets()->create([
|
||||||
|
'lower_limit' => floatval($bracketData['lower_limit']),
|
||||||
|
'upper_limit' => isset($bracketData['upper_limit']) && $bracketData['upper_limit'] !== ''
|
||||||
|
? floatval($bracketData['upper_limit'])
|
||||||
|
: null,
|
||||||
|
'fixed_fee' => floatval($bracketData['fixed_fee']),
|
||||||
|
'rate' => floatval($bracketData['rate']),
|
||||||
|
'order' => $order++,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('settings.index', ['tab' => 'isr'])
|
||||||
|
->with('success', 'Brackets actualizados correctamente.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Importa CSV
|
||||||
|
*/
|
||||||
|
public function uploadCsv(Request $request, IsrTable $isrTable)
|
||||||
|
{
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'csv_file' => 'required|file|mimes:csv,txt',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return redirect()->route('settings.isr.edit', $isrTable)
|
||||||
|
->withErrors($validator)
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $request->file('csv_file');
|
||||||
|
$content = file_get_contents($file->getRealPath());
|
||||||
|
$lines = explode("\n", $content);
|
||||||
|
|
||||||
|
$brackets = [];
|
||||||
|
$firstLine = true;
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
|
||||||
|
// Ignorar primera línea (encabezados vacíos)
|
||||||
|
if ($firstLine) {
|
||||||
|
$firstLine = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($line)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parsear línea CSV
|
||||||
|
$values = str_getcsv($line);
|
||||||
|
|
||||||
|
if (count($values) < 4) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lowerLimit = $this->parseCsvValue($values[0]);
|
||||||
|
$upperLimit = $this->parseCsvValue($values[1]);
|
||||||
|
$fixedFee = $this->parseCsvValue($values[2]);
|
||||||
|
$rate = $this->parseCsvValue($values[3]);
|
||||||
|
|
||||||
|
// Ignorar si no hay lower_limit válido
|
||||||
|
if ($lowerLimit === null || $lowerLimit < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default null values to 0
|
||||||
|
$fixedFee = $fixedFee ?? 0;
|
||||||
|
$rate = $rate ?? 0;
|
||||||
|
|
||||||
|
$brackets[] = [
|
||||||
|
'lower_limit' => $lowerLimit,
|
||||||
|
'upper_limit' => $upperLimit,
|
||||||
|
'fixed_fee' => $fixedFee,
|
||||||
|
'rate' => $rate,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordenar brackets por lower_limit
|
||||||
|
usort($brackets, function ($a, $b) {
|
||||||
|
return $a['lower_limit'] - $b['lower_limit'];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (empty($brackets)) {
|
||||||
|
return redirect()->route('settings.index', ['tab' => 'isr'])
|
||||||
|
->with('error', 'No se encontraron brackets válidos en el archivo CSV.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminar brackets existentes
|
||||||
|
$isrTable->brackets()->delete();
|
||||||
|
|
||||||
|
// Crear nuevos brackets
|
||||||
|
$order = 0;
|
||||||
|
foreach ($brackets as $bracket) {
|
||||||
|
$isrTable->brackets()->create([
|
||||||
|
'lower_limit' => $bracket['lower_limit'],
|
||||||
|
'upper_limit' => $bracket['upper_limit'],
|
||||||
|
'fixed_fee' => $bracket['fixed_fee'],
|
||||||
|
'rate' => $bracket['rate'],
|
||||||
|
'order' => $order++,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('settings.index', ['tab' => 'isr'])
|
||||||
|
->with('success', 'Brackets importados correctamente desde el CSV.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elimina tabla ISR
|
||||||
|
*/
|
||||||
|
public function destroy(IsrTable $isrTable)
|
||||||
|
{
|
||||||
|
$year = $isrTable->year;
|
||||||
|
$isrTable->delete();
|
||||||
|
|
||||||
|
return redirect()->route('settings.index', ['tab' => 'isr'])
|
||||||
|
->with('success', 'Tabla ISR para ' . $year . ' eliminada.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convierte valor CSV como "3,537.15" a 3537.15
|
||||||
|
*/
|
||||||
|
private function parseCsvValue(string $val): ?float
|
||||||
|
{
|
||||||
|
// Limpiar comillas y espacios
|
||||||
|
$val = trim($val, '"\' ');
|
||||||
|
|
||||||
|
// Manejar "En adelante" como null
|
||||||
|
if (in_array(strtolower($val), ['en adelante', 'en adelante ', 'null', ''])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminar comas de miles
|
||||||
|
$val = str_replace(',', '', $val);
|
||||||
|
|
||||||
|
// Convertir a número
|
||||||
|
$num = floatval($val);
|
||||||
|
|
||||||
|
// Permitir 0 como valor válido, pero null para valores negativos o no numéricos
|
||||||
|
return is_numeric($val) ? $num : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
app/Models/IsrBracket.php
Normal file
36
app/Models/IsrBracket.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class IsrBracket extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'isr_brackets';
|
||||||
|
|
||||||
|
protected $fillable = ['isr_table_id', 'lower_limit', 'upper_limit', 'fixed_fee', 'rate', 'order'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'lower_limit' => 'decimal:2',
|
||||||
|
'upper_limit' => 'decimal:2',
|
||||||
|
'fixed_fee' => 'decimal:2',
|
||||||
|
'rate' => 'decimal:2',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relación con la tabla ISR
|
||||||
|
*/
|
||||||
|
public function isrTable(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(IsrTable::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/Models/IsrTable.php
Normal file
21
app/Models/IsrTable.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class IsrTable extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'isr_tables';
|
||||||
|
|
||||||
|
protected $fillable = ['year'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relación con los brackets de ISR
|
||||||
|
*/
|
||||||
|
public function brackets(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(IsrBracket::class)->orderBy('order');
|
||||||
|
}
|
||||||
|
}
|
||||||
12
csv/ISR2026.csv
Normal file
12
csv/ISR2026.csv
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
,,,
|
||||||
|
0.01,416.7,0,1.92
|
||||||
|
416.71,"3,537.15",7.95,6.4
|
||||||
|
"3,537.16","6,216.15",207.75,10.88
|
||||||
|
"6,216.16","7,225.95",499.2,16
|
||||||
|
"7,225.96","8,651.40",660.75,17.92
|
||||||
|
"8,651.41","17,448.75",916.2,21.36
|
||||||
|
"17,448.76","27,501.60","2,795.25",23.52
|
||||||
|
"27,501.61","52,505.25","5,159.70",30
|
||||||
|
"52,505.26","70,006.95","12,660.75",32
|
||||||
|
"70,006.96","210,020.70","18,261.30",34
|
||||||
|
"210,020.71",En adelante,"65,866.05",35
|
||||||
|
@@ -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('isr_tables', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->year('year')->unique();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('isr_tables');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?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('isr_brackets', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('isr_table_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->decimal('lower_limit', 15, 2);
|
||||||
|
$table->decimal('upper_limit', 15, 2)->nullable()->comment('null significa "En adelante"');
|
||||||
|
$table->decimal('fixed_fee', 12, 2)->default(0);
|
||||||
|
$table->decimal('rate', 5, 2)->comment('porcentaje');
|
||||||
|
$table->unsignedInteger('order')->default(0);
|
||||||
|
$table->index('isr_table_id');
|
||||||
|
$table->index('order');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('isr_brackets');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('isr_brackets', function (Blueprint $table) {
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('isr_brackets', function (Blueprint $table) {
|
||||||
|
$table->dropTimestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -3,16 +3,31 @@
|
|||||||
@section('title', 'Configuración')
|
@section('title', 'Configuración')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="row">
|
<ul class="nav nav-tabs" id="settingsTabs" role="tablist">
|
||||||
<div class="col-12">
|
<li class="nav-item">
|
||||||
<h2 class="mb-4">
|
<button class="nav-link{{ request('tab') != 'isr' ? ' active' : '' }}" id="laboral-tab" data-bs-toggle="tab" data-bs-target="#laboral" type="button" role="tab">
|
||||||
<i class="bi bi-gear text-primary"></i> Configuración
|
<i class="bi bi-briefcase"></i> Datos Laborales
|
||||||
</h2>
|
</button>
|
||||||
</div>
|
</li>
|
||||||
</div>
|
<li class="nav-item">
|
||||||
|
<button class="nav-link" id="comision-tab" data-bs-toggle="tab" data-bs-target="#comision" type="button" role="tab">
|
||||||
|
<i class="bi bi-percent"></i> Comisión
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link{{ request('tab') == 'isr' ? ' active' : '' }}" id="isr-tab" data-bs-toggle="tab" data-bs-target="#isr" type="button" role="tab">
|
||||||
|
<i class="bi bi-calculator"></i> ISR
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link" id="password-tab" data-bs-toggle="tab" data-bs-target="#password" type="button" role="tab">
|
||||||
|
<i class="bi bi-key"></i> Contraseña
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<div class="row">
|
<div class="tab-content mt-3" id="settingsTabsContent">
|
||||||
<div class="col-md-8">
|
<div class="tab-pane fade{{ request('tab') != 'isr' ? ' show active' : '' }}" id="laboral" role="tabpanel">
|
||||||
<!-- Datos Laborales -->
|
<!-- Datos Laborales -->
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header bg-success text-white">
|
<div class="card-header bg-success text-white">
|
||||||
@@ -77,7 +92,9 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="comision" role="tabpanel">
|
||||||
<!-- Configuración de Comisión -->
|
<!-- Configuración de Comisión -->
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header bg-primary text-white">
|
<div class="card-header bg-primary text-white">
|
||||||
@@ -131,7 +148,13 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane fade{{ request('tab') == 'isr' ? ' show active' : '' }}" id="isr" role="tabpanel">
|
||||||
|
@include('settings.isr.index', ['isrTables' => \App\Models\IsrTable::with('brackets')->get()])
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="password" role="tabpanel">
|
||||||
<!-- Cambiar Contraseña -->
|
<!-- Cambiar Contraseña -->
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header bg-secondary text-white">
|
<div class="card-header bg-secondary text-white">
|
||||||
@@ -178,8 +201,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-12">
|
||||||
<!-- Información del Usuario -->
|
<!-- Información del Usuario -->
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header bg-dark text-white">
|
<div class="card-header bg-dark text-white">
|
||||||
@@ -189,32 +214,32 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-sm-4">Nombre:</dt>
|
<dt class="col-sm-2">Nombre:</dt>
|
||||||
<dd class="col-sm-8">{{ auth()->user()->name }}</dd>
|
<dd class="col-sm-10">{{ auth()->user()->name }}</dd>
|
||||||
|
|
||||||
<dt class="col-sm-4">Email:</dt>
|
<dt class="col-sm-2">Email:</dt>
|
||||||
<dd class="col-sm-8">{{ auth()->user()->email }}</dd>
|
<dd class="col-sm-10">{{ auth()->user()->email }}</dd>
|
||||||
|
|
||||||
@if(auth()->user()->razon_social)
|
@if(auth()->user()->razon_social)
|
||||||
<dt class="col-sm-4">Empresa:</dt>
|
<dt class="col-sm-2">Empresa:</dt>
|
||||||
<dd class="col-sm-8">{{ auth()->user()->razon_social }}</dd>
|
<dd class="col-sm-10">{{ auth()->user()->razon_social }}</dd>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if(auth()->user()->fecha_ingreso)
|
@if(auth()->user()->fecha_ingreso)
|
||||||
<dt class="col-sm-4">Ingreso:</dt>
|
<dt class="col-sm-2">Ingreso:</dt>
|
||||||
<dd class="col-sm-8">{{ auth()->user()->fecha_ingreso->format('d/m/Y') }}</dd>
|
<dd class="col-sm-10">{{ auth()->user()->fecha_ingreso->format('d/m/Y') }}</dd>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<dt class="col-sm-4">Comisión:</dt>
|
<dt class="col-sm-2">Comisión:</dt>
|
||||||
<dd class="col-sm-8">
|
<dd class="col-sm-10">
|
||||||
<span class="badge bg-primary">{{ auth()->user()->commission_percentage }}%</span>
|
<span class="badge bg-primary">{{ auth()->user()->commission_percentage }}%</span>
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt class="col-sm-4">Salario:</dt>
|
<dt class="col-sm-2">Salario:</dt>
|
||||||
<dd class="col-sm-8">${{ number_format(auth()->user()->monthly_salary, 2) }}</dd>
|
<dd class="col-sm-10">${{ number_format(auth()->user()->monthly_salary, 2) }}</dd>
|
||||||
|
|
||||||
<dt class="col-sm-4">Estado:</dt>
|
<dt class="col-sm-2">Estado:</dt>
|
||||||
<dd class="col-sm-8">
|
<dd class="col-sm-10">
|
||||||
@if(auth()->user()->is_active)
|
@if(auth()->user()->is_active)
|
||||||
<span class="badge bg-success">Activo</span>
|
<span class="badge bg-success">Activo</span>
|
||||||
@else
|
@else
|
||||||
|
|||||||
168
resources/views/settings/isr/edit.blade.php
Normal file
168
resources/views/settings/isr/edit.blade.php
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Editar Tabla ISR ' . $isrTable->year . ' - Configuración')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4>
|
||||||
|
<i class="bi bi-calculator text-primary"></i> Tabla ISR {{ $isrTable->year }}
|
||||||
|
</h4>
|
||||||
|
<a href="{{ route('settings.index') }}#isr" class="btn btn-secondary btn-sm">
|
||||||
|
<i class="bi bi-arrow-left"></i> Volver
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Errores de Validación -->
|
||||||
|
@if($errors->any())
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<h6><i class="bi bi-exclamation-triangle"></i> Error(es):</h6>
|
||||||
|
<ul class="mb-0 small">
|
||||||
|
@foreach($errors->all() as $error)
|
||||||
|
<li>{{ $error }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Cerrar"></button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- Formulario de Brackets -->
|
||||||
|
<form method="POST" action="{{ route('settings.isr.brackets.update', $isrTable) }}" id="bracketsForm">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="bi bi-table"></i> Brackets ISR - Año {{ $isrTable->year }}
|
||||||
|
</h6>
|
||||||
|
<span class="badge bg-light text-dark" id="bracketCount">0 rango(s)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover mb-0" id="bracketsTable">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Límite Inferior</th>
|
||||||
|
<th>Límite Superior</th>
|
||||||
|
<th>Cuota Fija</th>
|
||||||
|
<th>Tasa (%)</th>
|
||||||
|
<th style="width: 50px">Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="bracketsBody">
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr class="table-light">
|
||||||
|
<td colspan="5">
|
||||||
|
<button type="button" class="btn btn-success btn-sm" onclick="addBracket()">
|
||||||
|
<i class="bi bi-plus-circle"></i> Agregar Fila
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-light">
|
||||||
|
<div class="d-flex justify-content-end gap-2">
|
||||||
|
<a href="{{ route('settings.index') }}#isr" class="btn btn-secondary btn-sm">
|
||||||
|
<i class="bi bi-arrow-left"></i> Volver
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm">
|
||||||
|
<i class="bi bi-save"></i> Guardar Cambios
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Información Adicional -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="bi bi-info-circle"></i> Información
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted small mb-2">
|
||||||
|
<strong>Cómo funciona la tabla ISR:</strong>
|
||||||
|
</p>
|
||||||
|
<ul class="text-muted small mb-0">
|
||||||
|
<li><strong>Límite Inferior:</strong> Monto mínimo del rango.</li>
|
||||||
|
<li><strong>Límite Superior:</strong> Monto máximo (dejar vacío para "En adelante").</li>
|
||||||
|
<li><strong>Cuota Fija:</strong> Impuesto fijo.</li>
|
||||||
|
<li><strong>Tasa (%):</strong> Porcentaje sobre el excedente.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
let bracketIndex = 0;
|
||||||
|
|
||||||
|
function updateBracketCount() {
|
||||||
|
const count = document.querySelectorAll('#bracketsBody tr').length;
|
||||||
|
document.getElementById('bracketCount').textContent = count + ' rango(s)';
|
||||||
|
}
|
||||||
|
|
||||||
|
function addBracket(lowerLimit = '', upperLimit = '', fixedFee = '', rate = '') {
|
||||||
|
const tbody = document.getElementById('bracketsBody');
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>
|
||||||
|
<input type="number" class="form-control form-control-sm" name="brackets[${bracketIndex}][lower_limit]"
|
||||||
|
value="${lowerLimit}" min="0" step="0.01" placeholder="0.00" required>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="number" class="form-control form-control-sm" name="brackets[${bracketIndex}][upper_limit]"
|
||||||
|
value="${upperLimit}" min="0" step="0.01" placeholder="En adelante">
|
||||||
|
<small class="text-muted">Vacío = En adelante</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="number" class="form-control form-control-sm" name="brackets[${bracketIndex}][fixed_fee]"
|
||||||
|
value="${fixedFee}" min="0" step="0.01" placeholder="0.00" required>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="number" class="form-control form-control-sm" name="brackets[${bracketIndex}][rate]"
|
||||||
|
value="${rate}" min="0" max="100" step="0.01" placeholder="0.00" required>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-danger btn-sm" onclick="removeBracket(this)" title="Eliminar">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(row);
|
||||||
|
bracketIndex++;
|
||||||
|
updateBracketCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeBracket(button) {
|
||||||
|
const row = button.closest('tr');
|
||||||
|
row.remove();
|
||||||
|
updateBracketCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
@foreach($isrTable->brackets as $bracket)
|
||||||
|
addBracket(
|
||||||
|
{{ $bracket->lower_limit }},
|
||||||
|
{{ $bracket->upper_limit ?? 'null' }},
|
||||||
|
{{ $bracket->fixed_fee }},
|
||||||
|
{{ $bracket->rate }}
|
||||||
|
);
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
if (document.querySelectorAll('#bracketsBody tr').length === 0) {
|
||||||
|
addBracket();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
@endsection
|
||||||
169
resources/views/settings/isr/index.blade.php
Normal file
169
resources/views/settings/isr/index.blade.php
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h4 class="mb-3">
|
||||||
|
<i class="bi bi-calculator text-primary"></i> Tablas ISR
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Lista de Tablas ISR -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="bi bi-table"></i> Tablas ISR Existentes
|
||||||
|
</h6>
|
||||||
|
<span class="badge bg-light text-dark">{{ $isrTables->count() }} tabla(s)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@if($isrTables->isEmpty())
|
||||||
|
<div class="alert alert-info mb-0">
|
||||||
|
<i class="bi bi-info-circle"></i> No hay tablas ISR configuradas. Crea una nueva tabla para comenzar.
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Año</th>
|
||||||
|
<th>Brackets</th>
|
||||||
|
<th>Última Actualización</th>
|
||||||
|
<th class="text-center">Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($isrTables as $table)
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-primary fs-6">{{ $table->year }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="text-muted">{{ $table->brackets->count() }} rango(s)</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<small class="text-muted">
|
||||||
|
@if($table->brackets->isNotEmpty())
|
||||||
|
{{ $table->brackets->max('updated_at')->format('d/m/Y H:i') }}
|
||||||
|
@else
|
||||||
|
<em>Sin datos</em>
|
||||||
|
@endif
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<a href="{{ route('settings.isr.edit', $table) }}" class="btn btn-primary" title="Editar">
|
||||||
|
<i class="bi bi-pencil"></i> Editar
|
||||||
|
</a>
|
||||||
|
<form method="POST" action="{{ route('settings.isr.destroy', $table) }}" class="d-inline" onsubmit="return confirm('¿Estás seguro de eliminar la tabla ISR {{ $table->year }}?')">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-danger" title="Eliminar">
|
||||||
|
<i class="bi bi-trash"></i> Eliminar
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<button type="button" class="btn btn-success" onclick="showUploadModal({{ $table->id }}, {{ $table->year }})" title="Subir CSV">
|
||||||
|
<i class="bi bi-upload"></i> Subir CSV
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Crear Nueva Tabla ISR -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="bi bi-plus-circle"></i> Nueva Tabla ISR
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ route('settings.isr.store') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="year" class="form-label">Año</label>
|
||||||
|
<input type="number" class="form-control @error('year') is-invalid @enderror"
|
||||||
|
id="isr_year" name="year"
|
||||||
|
value="{{ old('year', now()->year) }}"
|
||||||
|
min="2000" max="2100"
|
||||||
|
placeholder="Ej: 2026" required>
|
||||||
|
@error('year')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
<small class="text-muted">Año para el cual aplica la tabla de ISR</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 d-flex align-items-end">
|
||||||
|
<div class="mb-3">
|
||||||
|
<button type="submit" class="btn btn-success">
|
||||||
|
<i class="bi bi-plus-circle"></i> Nueva Tabla ISR
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Subir CSV -->
|
||||||
|
<div class="modal fade" id="uploadCsvModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-success text-white">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="bi bi-upload"></i> Subir CSV - Año <span id="modalYear"></span>
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Cerrar"></button>
|
||||||
|
</div>
|
||||||
|
<form method="POST" id="uploadCsvForm" enctype="multipart/form-data">
|
||||||
|
@csrf
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="csv_file" class="form-label">Archivo CSV</label>
|
||||||
|
<input type="file" class="form-control @error('csv_file') is-invalid @enderror"
|
||||||
|
id="csv_file" name="csv_file"
|
||||||
|
accept=".csv,.txt" required>
|
||||||
|
@error('csv_file')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
<small class="text-muted">
|
||||||
|
Formato esperado: Límite Inferior, Límite Superior, Cuota Fija, Tasa (%)<br>
|
||||||
|
La primera línea puede contener encabezados (se ignorará).
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-info mb-0">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
<strong>Nota:</strong> Si la tabla ya tiene datos, estos serán reemplazados por el contenido del CSV.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" class="btn btn-success">
|
||||||
|
<i class="bi bi-upload"></i> Importar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
function showUploadModal(tableId, year) {
|
||||||
|
document.getElementById('modalYear').textContent = year;
|
||||||
|
document.getElementById('uploadCsvForm').action = '/settings/isr/' + tableId + '/upload';
|
||||||
|
|
||||||
|
var modal = new bootstrap.Modal(document.getElementById('uploadCsvModal'));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
@@ -4,6 +4,7 @@ use App\Http\Controllers\AuthController;
|
|||||||
use App\Http\Controllers\CalendarController;
|
use App\Http\Controllers\CalendarController;
|
||||||
use App\Http\Controllers\DashboardController;
|
use App\Http\Controllers\DashboardController;
|
||||||
use App\Http\Controllers\ExpenseController;
|
use App\Http\Controllers\ExpenseController;
|
||||||
|
use App\Http\Controllers\IsrController;
|
||||||
use App\Http\Controllers\MonthController;
|
use App\Http\Controllers\MonthController;
|
||||||
use App\Http\Controllers\ReportController;
|
use App\Http\Controllers\ReportController;
|
||||||
use App\Http\Controllers\SaleController;
|
use App\Http\Controllers\SaleController;
|
||||||
@@ -66,6 +67,16 @@ Route::middleware(['auth'])->group(function () {
|
|||||||
// Settings
|
// Settings
|
||||||
Route::get('/settings', [SettingsController::class, 'index'])->name('settings.index');
|
Route::get('/settings', [SettingsController::class, 'index'])->name('settings.index');
|
||||||
Route::put('/settings', [SettingsController::class, 'update'])->name('settings.update');
|
Route::put('/settings', [SettingsController::class, 'update'])->name('settings.update');
|
||||||
|
|
||||||
|
// ISR Tables
|
||||||
|
Route::prefix('settings/isr')->name('settings.isr.')->group(function () {
|
||||||
|
Route::get('/', [IsrController::class, 'index'])->name('index');
|
||||||
|
Route::post('/', [IsrController::class, 'store'])->name('store');
|
||||||
|
Route::delete('/{isrTable}', [IsrController::class, 'destroy'])->name('destroy');
|
||||||
|
Route::get('/{isrTable}/edit', [IsrController::class, 'edit'])->name('edit');
|
||||||
|
Route::put('/{isrTable}/brackets', [IsrController::class, 'updateBrackets'])->name('brackets.update');
|
||||||
|
Route::post('/{isrTable}/upload', [IsrController::class, 'uploadCsv'])->name('upload');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Webhook de Telegram (público, sin auth)
|
// Webhook de Telegram (público, sin auth)
|
||||||
|
|||||||
Reference in New Issue
Block a user