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:
2026-04-21 13:22:01 -06:00
parent 66df616eee
commit 4abf89c57f
11 changed files with 778 additions and 24 deletions

View File

@@ -3,16 +3,31 @@
@section('title', 'Configuración')
@section('content')
<div class="row">
<div class="col-12">
<h2 class="mb-4">
<i class="bi bi-gear text-primary"></i> Configuración
</h2>
</div>
</div>
<ul class="nav nav-tabs" id="settingsTabs" role="tablist">
<li class="nav-item">
<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-briefcase"></i> Datos Laborales
</button>
</li>
<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="col-md-8">
<div class="tab-content mt-3" id="settingsTabsContent">
<div class="tab-pane fade{{ request('tab') != 'isr' ? ' show active' : '' }}" id="laboral" role="tabpanel">
<!-- Datos Laborales -->
<div class="card mb-4">
<div class="card-header bg-success text-white">
@@ -77,7 +92,9 @@
</form>
</div>
</div>
</div>
<div class="tab-pane fade" id="comision" role="tabpanel">
<!-- Configuración de Comisión -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
@@ -131,7 +148,13 @@
</form>
</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 -->
<div class="card mb-4">
<div class="card-header bg-secondary text-white">
@@ -178,8 +201,10 @@
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="row mt-4">
<div class="col-md-12">
<!-- Información del Usuario -->
<div class="card mb-4">
<div class="card-header bg-dark text-white">
@@ -189,32 +214,32 @@
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-4">Nombre:</dt>
<dd class="col-sm-8">{{ auth()->user()->name }}</dd>
<dt class="col-sm-2">Nombre:</dt>
<dd class="col-sm-10">{{ auth()->user()->name }}</dd>
<dt class="col-sm-4">Email:</dt>
<dd class="col-sm-8">{{ auth()->user()->email }}</dd>
<dt class="col-sm-2">Email:</dt>
<dd class="col-sm-10">{{ auth()->user()->email }}</dd>
@if(auth()->user()->razon_social)
<dt class="col-sm-4">Empresa:</dt>
<dd class="col-sm-8">{{ auth()->user()->razon_social }}</dd>
<dt class="col-sm-2">Empresa:</dt>
<dd class="col-sm-10">{{ auth()->user()->razon_social }}</dd>
@endif
@if(auth()->user()->fecha_ingreso)
<dt class="col-sm-4">Ingreso:</dt>
<dd class="col-sm-8">{{ auth()->user()->fecha_ingreso->format('d/m/Y') }}</dd>
<dt class="col-sm-2">Ingreso:</dt>
<dd class="col-sm-10">{{ auth()->user()->fecha_ingreso->format('d/m/Y') }}</dd>
@endif
<dt class="col-sm-4">Comisión:</dt>
<dd class="col-sm-8">
<dt class="col-sm-2">Comisión:</dt>
<dd class="col-sm-10">
<span class="badge bg-primary">{{ auth()->user()->commission_percentage }}%</span>
</dd>
<dt class="col-sm-4">Salario:</dt>
<dd class="col-sm-8">${{ number_format(auth()->user()->monthly_salary, 2) }}</dd>
<dt class="col-sm-2">Salario:</dt>
<dd class="col-sm-10">${{ number_format(auth()->user()->monthly_salary, 2) }}</dd>
<dt class="col-sm-4">Estado:</dt>
<dd class="col-sm-8">
<dt class="col-sm-2">Estado:</dt>
<dd class="col-sm-10">
@if(auth()->user()->is_active)
<span class="badge bg-success">Activo</span>
@else

View 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

View 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