Major Refactor: Refactor the codebase to improve readability and maintainability

This commit is contained in:
2026-06-21 23:38:49 -04:00
parent 9c4753cd1f
commit 801b0b97fc
46 changed files with 2378 additions and 2031 deletions

View File

@@ -1,29 +1,20 @@
{% extends "macros/base.html" %}
{% from 'macros/modals.html' import confirm_modal, edit_product_modal %}
{% from "macros/ui.html" import flashed_messages %}
{% block title %}Catálogo de Productos{% endblock %}
{% block head %}
<!-- HEAD -->
{% endblock %}
{% block content %}
<h2 class="mb-4">Catálogo de Productos por Zona</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message|safe }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{{ flashed_messages() }}
{{ edit_product_modal(zonas) }}
<div class="card mb-4 shadow-sm">
<div class="card-header bg-primary text-white">Agregar Producto Maestro</div>
<div class="card-body">
<form method="POST" action="{{ url_for('manage_products') }}">
<form method="POST" action="{{ url_for('admin.manage_products') }}">
<div class="row g-3">
<div class="col-md-10">
<input type="text" class="form-control" name="name" placeholder="Nombre del Producto" required>
@@ -69,7 +60,7 @@
id='deleteProd' ~ prod.id,
title='Eliminar Producto',
message='¿Eliminar "' ~ prod.name ~ '"? Esto fallará si el producto ya tiene ventas registradas.',
action_url=url_for('delete_product', id=prod.id),
action_url=url_for('admin.delete_product', id=prod.id),
btn_class='btn-danger',
btn_text='Eliminar'
) }}
@@ -85,7 +76,7 @@
<div class="modal fade" id="pricesModal{{ prod.id }}" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form method="POST" action="{{ url_for('update_product_prices', id=prod.id) }}">
<form method="POST" action="{{ url_for('admin.update_product_prices', id=prod.id) }}">
<div class="modal-header">
<h5 class="modal-title">Precios: {{ prod.name }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
@@ -156,7 +147,7 @@
<td class="align-middle">${{ "{:,.0f}".format(futuro.price).replace(',', '.') }}</td>
<td class="align-middle">${{ "{:,.0f}".format(futuro.commission).replace(',', '.') }}</td>
<td class="align-middle">
<form action="{{ url_for('cancel_scheduled_price', id=futuro.id) }}" method="POST" class="d-inline">
<form action="{{ url_for('admin.cancel_scheduled_price', id=futuro.id) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-danger btn-sm py-0 px-2" title="Cancelar este cambio">
<i class="bi bi-x-lg"></i>
</button>
@@ -196,132 +187,6 @@
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
let priceChartInstance = null;
async function showHistory(prodId, prodName) {
// Abrir el modal inmediatamente para que no parezca que la app murió
const modal = new bootstrap.Modal(document.getElementById('chartModal'));
document.getElementById('chartModalTitle').innerText = 'Fluctuación de Precio: ' + prodName;
modal.show();
// Traer la data desde nuestra nueva API
const res = await fetch(`/admin/api/productos/${prodId}/historial`);
const data = await res.json();
// Extraer zonas únicas y fechas únicas (limpiando la hora para el eje X)
const zonas = [...new Set(data.map(d => d.zona))];
const fechas = [...new Set(data.map(d => d.fecha.split(' ')[0]))].sort();
const colors = ['#0d6efd', '#198754', '#dc3545', '#ffc107', '#0dcaf0'];
const datasets = zonas.map((zona, index) => {
let lastPrice = 0;
// Rellenar huecos: Si no hubo cambio un día, se mantiene el precio del día anterior
const dataPoints = fechas.map(f => {
const hits = data.filter(d => d.zona === zona && d.fecha.startsWith(f));
if (hits.length > 0) {
lastPrice = hits[hits.length - 1].price; // Tomar el último cambio de ese día
}
return lastPrice;
});
return {
label: zona,
data: dataPoints,
borderColor: colors[index % colors.length],
backgroundColor: colors[index % colors.length],
stepped: true, // Hace que se vea como escaleras en vez de curvas raras
borderWidth: 2
};
});
// Dibujar (o redibujar) el gráfico
const ctx = document.getElementById('priceChart').getContext('2d');
if (priceChartInstance) {
priceChartInstance.destroy();
}
priceChartInstance = new Chart(ctx, {
type: 'line',
data: {
labels: fechas,
datasets: datasets
},
options: {
responsive: true,
interaction: {
mode: 'index',
intersect: false
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) { return '$' + value.toLocaleString('es-CL'); }
}
}
}
}
});
}
</script>
<script>
const editModal = document.getElementById('editProductModal');
if (editModal) {
editModal.addEventListener('show.bs.modal', function (event) {
const button = event.relatedTarget;
// Atributos extraídos del botón
const id = button.getAttribute('data-id');
const name = button.getAttribute('data-name');
const price = button.getAttribute('data-price');
const commission = button.getAttribute('data-commission');
const zonaId = button.getAttribute('data-zona');
// Actualizamos el destino del formulario
const form = editModal.querySelector('#editProductForm');
form.action = `/admin/productos/edit/${id}`;
// Llenamos los inputs
editModal.querySelector('#edit_name').value = name;
editModal.querySelector('#edit_price').value = price;
editModal.querySelector('#edit_commission').value = commission;
editModal.querySelector('#edit_zona_id').value = zonaId;
// Forzamos el formato de miles (puntos) inmediatamente
editModal.querySelectorAll('.money-input').forEach(input => {
input.dispatchEvent(new Event('input'));
});
});
}
document.querySelectorAll('.money-input').forEach(function(input) {
input.addEventListener('input', function(e) {
let value = this.value.replace(/\D/g, '');
if (value !== '') {
this.value = parseInt(value, 10).toLocaleString('es-CL');
} else {
this.value = '';
}
});
});
const searchInput = document.getElementById('searchProduct');
const productRows = document.querySelectorAll('.product-row');
if (searchInput) {
searchInput.addEventListener('input', function() {
const term = this.value.toLowerCase();
productRows.forEach(row => {
// Asume que el nombre está en la primera celda
const name = row.cells[0].textContent.toLowerCase();
row.style.display = name.includes(term) ? '' : 'none';
});
});
}
</script>
<script src="{{ url_for('static', filename='js/product-history-chart.js') }}"></script>
<script src="{{ url_for('static', filename='js/admin_productos.js') }}"></script>
{% endblock %}

View File

@@ -1,11 +1,9 @@
{% extends "macros/base.html" %}
{% from 'macros/modals.html' import alert_modal, rendicion_detail_modal, confirm_modal, edit_rendicion_modal %}
{% from "macros/ui.html" import flashed_messages %}
{% block title %}Historial de Rendiciones{% endblock %}
{% block head %}
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="mb-0">Historial de Rendiciones</h2>
@@ -13,7 +11,7 @@
<div class="card shadow-sm mb-4 border-0">
<div class="card-body bg-body-tertiary rounded p-3">
<form method="GET" action="{{ url_for('admin_rendiciones') }}" id="filterForm">
<form method="GET" action="{{ url_for('admin.admin_rendiciones') }}" id="filterForm">
<div class="row g-2 align-items-end">
<div class="col-md-2">
<label class="form-label small text-muted mb-1">Año</label>
@@ -77,13 +75,7 @@
</div>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message|safe }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{{ flashed_messages() }}
<div class="card shadow-sm">
<div class="card-body p-0">
@@ -123,7 +115,7 @@
id='deleteRendicion' ~ r[0],
title='Eliminar Rendición',
message='¿Estás seguro de que deseas eliminar la rendición #' ~ r[0] ~ '? Esta acción no se puede deshacer.',
action_url=url_for('delete_rendicion', id=r[0]),
action_url=url_for('admin.delete_rendicion', id=r[0]),
btn_class='btn-danger',
btn_text='Eliminar'
) }}
@@ -142,171 +134,5 @@
{% endblock %}
{% block scripts %}
<script>
// Función para manejar las etiquetas de jornada (Full/Part Time)
function updateBadge(selectElement, badgeId) {
const option = selectElement.options[selectElement.selectedIndex];
const tipo = option.getAttribute('data-tipo');
const badgeDiv = document.getElementById(badgeId);
if (!badgeDiv) return;
if (!tipo) {
badgeDiv.innerHTML = '';
return;
}
const color = (tipo === 'Full Time') ? 'bg-success' : 'bg-secondary';
badgeDiv.innerHTML = `<span class="badge ${color}">${tipo}</span>`;
}
function toggleCompDiv(id, select) {
const compDiv = document.getElementById(`comp_com_div_${id}`);
compDiv.style.display = select.value ? 'flex' : 'none';
updateBadge(select, `badge_comp_${id}`);
updateComisionToggle(select, `cc_${id}`);
}
function updateComisionToggle(selectElement, toggleId) {
const option = selectElement.options[selectElement.selectedIndex];
const tipoJornada = option.getAttribute('data-tipo');
const toggleSwitch = document.getElementById(toggleId);
if (toggleSwitch && tipoJornada) {
// Explicitly set to true if Full Time, false otherwise
toggleSwitch.checked = (tipoJornada === 'Full Time');
} else if (toggleSwitch && !selectElement.value) {
// If "Sin acompañante" is selected, turn it off
toggleSwitch.checked = false;
}
// Actualizar el badge también
const baseId = toggleId.split('_')[1];
const targetBadge = toggleId.startsWith('wc') ? `badge_worker_${baseId}` : `badge_comp_${baseId}`;
updateBadge(selectElement, targetBadge);
}
// Recalcular total de la línea de producto y el total del sistema
function recalcProductLine(input) {
const qty = parseInt(input.value) || 0;
const price = parseInt(input.getAttribute('data-price')) || 0;
const rid = input.getAttribute('data-rid');
const row = input.closest('tr');
// Actualizar línea individual
const lineTotal = qty * price;
row.querySelector('.item-total-line').innerText = '$' + lineTotal.toLocaleString('es-CL');
// Recalcular total general del sistema en el modal
const modal = document.getElementById(`editRendicion${rid}`);
let newSysTotal = 0;
modal.querySelectorAll('.prod-qty-input').forEach(inp => {
newSysTotal += (parseInt(inp.value) || 0) * (parseInt(inp.getAttribute('data-price')) || 0);
});
document.getElementById(`sys_total_${rid}`).innerText = '$' + newSysTotal.toLocaleString('es-CL');
}
function calcTotalEdit(id) {
const getVal = (inputId) => parseInt(document.getElementById(inputId).value.replace(/\D/g, '')) || 0;
const total = getVal(`edit_debito_${id}`) + getVal(`edit_credito_${id}`) + getVal(`edit_mp_${id}`) + getVal(`edit_efectivo_${id}`);
document.getElementById(`display_nuevo_total_${id}`).innerText = '$' + total.toLocaleString('es-CL');
}
document.addEventListener('DOMContentLoaded', function() {
const editModals = document.querySelectorAll('[id^="editRendicion"]');
const editForms = document.querySelectorAll('form[action*="/admin/rendiciones/edit/"]');
const errorModalEl = document.getElementById('errorPersonaModal');
const errorModal = new bootstrap.Modal(errorModalEl);
const errorBody = document.getElementById('errorPersonaModalBody');
editForms.forEach(form => {
form.addEventListener('submit', function(e) {
const workerId = this.querySelector('select[name="worker_id"]').value;
const companionId = this.querySelector('select[name="companion_id"]').value;
if (companionId && workerId === companionId) {
e.preventDefault();
errorBody.innerHTML = "<strong>Error:</strong> El trabajador titular y el acompañante no pueden ser la misma persona. Por favor, selecciona a alguien más.";
errorModal.show();
}
});
});
editModals.forEach(modal => {
// Inicializar badges al abrir
modal.addEventListener('show.bs.modal', function() {
const rid = this.id.replace('editRendicion', '');
updateBadge(this.querySelector('select[name="worker_id"]'), `badge_worker_${rid}`);
const compSelect = this.querySelector('select[name="companion_id"]');
if (compSelect.value) updateBadge(compSelect, `badge_comp_${rid}`);
});
modal.addEventListener('hidden.bs.modal', function () {
const form = this.querySelector('form');
if (form) {
form.reset();
const rid = this.id.replace('editRendicion', '');
calcTotalEdit(rid);
// Resetear los subtotales visuales de productos
this.querySelectorAll('.prod-qty-input').forEach(inp => recalcProductLine(inp));
}
});
});
});
function validarNombresDiferentes(rendicionId) {
const workerSelect = document.querySelector(`select[name="worker_id"]`);
const companionSelect = document.querySelector(`select[name="companion_id"]`);
if (companionSelect.value && workerSelect.value === companionSelect.value) {
alert("Error: El trabajador titular y el acompañante no pueden ser la misma persona.");
return false;
}
return true;
}
// Vincula esta validación al evento submit del formulario de edición
document.querySelectorAll('form[action*="edit_rendicion"]').forEach(form => {
form.addEventListener('submit', function(e) {
const workerSelect = this.querySelector('select[name="worker_id"]');
const companionSelect = this.querySelector('select[name="companion_id"]');
if (companionSelect.value && workerSelect.value === companionSelect.value) {
e.preventDefault();
alert("Un trabajador no puede ser su propio acompañante. Por favor, corrige la selección.");
}
});
});
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const zonaSelect = document.getElementById('zonaSelect');
const moduloSelect = document.getElementById('moduloSelect');
const moduloOptions = Array.from(moduloSelect.options);
function filterModulos() {
const selectedZona = zonaSelect.value;
moduloOptions.forEach(option => {
if (option.value === "") {
// Siempre mostramos "Todos los Módulos"
option.style.display = '';
} else if (!selectedZona || option.dataset.zona === selectedZona) {
option.style.display = '';
} else {
option.style.display = 'none';
// Si el módulo seleccionado acaba de ocultarse, reseteamos el select
if (option.selected) {
moduloSelect.value = "";
}
}
});
}
zonaSelect.addEventListener('change', filterModulos);
// Ejecutar al cargar la página por si ya viene con una zona filtrada
filterModulos();
});
</script>
<script src="{{ url_for('static', filename='js/admin_rendiciones.js') }}"></script>
{% endblock %}

View File

@@ -1,48 +1,13 @@
{% extends "macros/base.html" %}
{% from "macros/modals.html" import report_filters %}
{% from "macros/ui.html" import back_link %}
{% block title %}Reporte: Centros Comerciales - {{ modulo_name }}{% endblock %}
{% block styles %}
<style>
.table-container {
max-height: 75vh;
overflow-y: auto;
overflow-x: auto;
}
.sticky-col {
position: sticky;
left: 0;
z-index: 2;
background-color: var(--bs-body-bg);
border-right: 2px solid var(--bs-border-color) !important;
}
thead th {
position: sticky;
top: 0;
z-index: 1;
background-color: var(--bs-body-bg);
box-shadow: inset 0 -1px 0 var(--bs-border-color);
}
thead th.sticky-col {
z-index: 3;
}
.numeric-cell {
font-family: 'Courier New', Courier, monospace;
}
</style>
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<a href="{{ url_for('admin_reportes_index') }}" class="btn btn-outline-secondary btn-sm mb-2">
<i class="bi bi-arrow-left"></i> Volver al Menú
</a>
{{ back_link(url_for('admin.admin_reportes_index'), 'Volver al Menú') }}
<h2>Registro Centros Comerciales</h2>
</div>
<div class="text-end">
@@ -51,7 +16,7 @@
</div>
</div>
{{ report_filters(
url_for('report_modulo_periodo', modulo_id=modulo_id),
url_for('admin.report_modulo_periodo', modulo_id=modulo_id),
workers_list,
worker_actual,
dia_actual,

View File

@@ -1,30 +1,13 @@
{% extends "macros/base.html" %}
{% from "macros/modals.html" import report_filters %}
{% from "macros/ui.html" import back_link %}
{% block title %}Reporte: Comisiones - {{ modulo_name }}{% endblock %}
{% block styles %}
<style>
.numeric-cell {
text-align: right;
font-family: 'Courier New', Courier, monospace;
}
.sticky-col {
position: sticky;
left: 0;
z-index: 10;
background-color: #f8f9fa !important;
border-right: 2px solid #dee2e6;
}
</style>
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<a href="{{ url_for('admin_reportes_index') }}" class="btn btn-outline-secondary btn-sm mb-2">
<i class="bi bi-arrow-left"></i> Volver al Menú
</a>
{{ back_link(url_for('admin.admin_reportes_index'), 'Volver al Menú') }}
<h2>Reporte de Comisiones</h2>
</div>
<div class="text-end">
@@ -33,7 +16,7 @@
</div>
</div>
{{ report_filters(
url_for('report_modulo_periodo', modulo_id=modulo_id),
url_for('admin.report_modulo_periodo', modulo_id=modulo_id),
workers_list,
worker_actual,
dia_actual,

View File

@@ -1,44 +1,13 @@
{% extends "macros/base.html" %}
{% from "macros/modals.html" import report_filters %}
{% from "macros/ui.html" import back_link %}
{% block title %}Reporte: Horarios - {{ modulo_name }}{% endblock %}
{% block styles %}
<style>
.table-container {
max-height: 75vh;
overflow-y: auto;
overflow-x: auto;
}
.sticky-col {
position: sticky;
left: 0;
z-index: 2;
background-color: var(--bs-body-bg);
border-right: 2px solid var(--bs-border-color) !important;
}
thead th {
position: sticky;
top: 0;
z-index: 1;
background-color: var(--bs-body-bg);
box-shadow: inset 0 -1px 0 var(--bs-border-color);
}
thead th.sticky-col {
z-index: 3;
}
</style>
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<a href="{{ url_for('admin_reportes_index') }}" class="btn btn-outline-secondary btn-sm mb-2">
<i class="bi bi-arrow-left"></i> Volver al Menú
</a>
{{ back_link(url_for('admin.admin_reportes_index'), 'Volver al Menú') }}
<h2>Control de Horarios</h2>
</div>
<div class="text-end">
@@ -47,7 +16,7 @@
</div>
</div>
{{ report_filters(
url_for('report_modulo_periodo', modulo_id=modulo_id),
url_for('admin.report_modulo_periodo', modulo_id=modulo_id),
workers_list,
worker_actual,
dia_actual,

View File

@@ -1,48 +1,13 @@
{% extends "macros/base.html" %}
{% from "macros/modals.html" import report_filters %}
{% from "macros/ui.html" import back_link %}
{% block title %}Reporte: Cálculo de IVA - {{ modulo_name }}{% endblock %}
{% block styles %}
<style>
.table-container {
max-height: 75vh;
overflow-y: auto;
overflow-x: auto;
}
.sticky-col {
position: sticky;
left: 0;
z-index: 2;
background-color: var(--bs-body-bg);
border-right: 2px solid var(--bs-border-color) !important;
}
thead th {
position: sticky;
top: 0;
z-index: 1;
background-color: var(--bs-body-bg);
box-shadow: inset 0 -1px 0 var(--bs-border-color);
}
thead th.sticky-col {
z-index: 3;
}
.numeric-cell {
font-family: 'Courier New', Courier, monospace;
}
</style>
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<a href="{{ url_for('admin_reportes_index') }}" class="btn btn-outline-secondary btn-sm mb-2">
<i class="bi bi-arrow-left"></i> Volver al Menú
</a>
{{ back_link(url_for('admin.admin_reportes_index'), 'Volver al Menú') }}
<h2>Cálculo de IVA</h2>
</div>
<div class="text-end">
@@ -51,7 +16,7 @@
</div>
</div>
{{ report_filters(
url_for('report_modulo_periodo', modulo_id=modulo_id),
url_for('admin.report_modulo_periodo', modulo_id=modulo_id),
workers_list,
worker_actual,
dia_actual,

View File

@@ -1,35 +1,13 @@
{% extends "macros/base.html" %}
{% from "macros/modals.html" import report_filters %}
{% from "macros/ui.html" import back_link %}
{% block title %}Reporte: Finanzas - {{ modulo_name }}{% endblock %}
{% block styles %}
<style>
.numeric-cell {
text-align: right;
font-family: 'Courier New', Courier, monospace;
font-weight: 500;
}
.total-column {
font-weight: bold;
background-color: #e9ecef !important;
}
.sticky-col {
position: sticky;
left: 0;
z-index: 10;
background-color: #f8f9fa !important;
border-right: 2px solid #dee2e6;
}
</style>
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<a href="{{ url_for('admin_reportes_index') }}" class="btn btn-outline-secondary btn-sm mb-2">
<i class="bi bi-arrow-left"></i> Volver al Menú
</a>
{{ back_link(url_for('admin.admin_reportes_index'), 'Volver al Menú') }}
<h2>Resumen Financiero y Medios de Pago</h2>
</div>
<div class="text-end">
@@ -74,7 +52,7 @@
</div>
{{ report_filters(
url_for('report_modulo_periodo', modulo_id=modulo_id),
url_for('admin.report_modulo_periodo', modulo_id=modulo_id),
workers_list,
worker_actual,
dia_actual,

View File

@@ -17,7 +17,7 @@
<h4 class="text-info border-bottom border-secondary pb-2 mb-4">
<i class="bi bi-geo-alt-fill me-2"></i>Zona: {{ zona_name }}
</h4>
<div class="row g-4">
{% for mod in lista_modulos %}
<div class="col-md-4 col-sm-6">
@@ -45,23 +45,4 @@
{{ reportes_menu_modal(mod[0], mod[1]) }}
{% endfor %}
{% endfor %}
<style>
.hover-shadow:hover {
transform: translateY(-5px);
box-shadow: 0 .5rem 1rem rgba(0,0,0,.3)!important;
border-color: #0dcaf0 !important;
}
.hover-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
background-color: #1e2125;
}
.hover-card:hover {
transform: translateY(-3px);
box-shadow: 0 .5rem 1rem rgba(0,0,0,.25)!important;
}
.transition-all {
transition: all .3s ease-in-out;
}
</style>
{% endblock %}

View File

@@ -1,18 +1,13 @@
{% extends "macros/base.html" %}
{% import "macros/modals.html" as modals %}
{% from "macros/ui.html" import flashed_messages %}
{% block title %}Estructura Operativa{% endblock %}
{% block content %}
<h2 class="mb-4">Estructura Operativa</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message|safe }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{{ flashed_messages() }}
<div class="row">
<div class="col-md-6 mb-4">
@@ -21,7 +16,7 @@
<h5 class="mb-0">Gestión de Zonas</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('manage_structure') }}" class="mb-4">
<form method="POST" action="{{ url_for('admin.manage_structure') }}" class="mb-4">
<input type="hidden" name="action" value="add_zona">
<div class="input-group">
<input type="text" class="form-control" name="zona_name" placeholder="Nombre de la Zona (ej: Norte)" required>
@@ -53,7 +48,7 @@
'deleteZona' ~ zona[0],
'Eliminar Zona',
'¿Estás seguro de eliminar la zona "' ~ zona[1] ~ '"? Esto podría afectar a los módulos asociados.',
url_for('delete_structure', type='zona', id=zona[0]),
url_for('admin.delete_structure', type='zona', id=zona[0]),
'btn-danger',
'Eliminar'
) }}
@@ -76,7 +71,7 @@
<h5 class="mb-0">Gestión de Módulos</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('manage_structure') }}" class="mb-4">
<form method="POST" action="{{ url_for('admin.manage_structure') }}" class="mb-4">
<input type="hidden" name="action" value="add_modulo">
<div class="row g-2">
<div class="col-md-5">
@@ -122,7 +117,7 @@
'deleteModulo' ~ modulo[0],
'Eliminar Módulo',
'¿Deseas eliminar el módulo "' ~ modulo[1] ~ '"?',
url_for('delete_structure', type='modulo', id=modulo[0]),
url_for('admin.delete_structure', type='modulo', id=modulo[0]),
'btn-danger',
'Eliminar'
) }}

View File

@@ -1,29 +1,20 @@
{% extends "macros/base.html" %}
{% from 'macros/modals.html' import confirm_modal, edit_worker_modal %}
{% from "macros/ui.html" import flashed_messages %}
{% block title %}Gestión de Trabajadores{% endblock %}
{% block head %}
<!-- HEAD -->
{% endblock %}
{% block content %}
<h2 class="mb-4">Gestión de Trabajadores</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message|safe }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{{ flashed_messages() }}
{{ edit_worker_modal(modulos) }}
<div class="card mb-4">
<div class="card-header bg-primary text-white">Agregar Nuevo Trabajador</div>
<div class="card-body">
<form method="POST" action="{{ url_for('manage_workers') }}">
<form method="POST" action="{{ url_for('admin.manage_workers') }}">
<div class="row g-3">
<div class="col-md-2">
<label class="form-label">RUT</label>
@@ -134,7 +125,7 @@
id='delWorker' ~ worker[0],
title='Eliminar Trabajador',
message='¿Eliminar a ' ~ worker[2] ~ '?',
action_url=url_for('delete_worker', id=worker[0]),
action_url=url_for('admin.delete_worker', id=worker[0]),
btn_class='btn-danger'
) }}
</td>
@@ -159,116 +150,5 @@
) }}
{% endblock %}
{% block scripts %}
<script>
const editWorkerModal = document.getElementById('editWorkerModal');
const confirmResetModal = document.getElementById('confirmResetPass');
if (editWorkerModal) {
editWorkerModal.addEventListener('show.bs.modal', function (event) {
const button = event.relatedTarget;
// Si el modal se abre desde el botón "Editar" de la tabla
if (button && button.hasAttribute('data-id')) {
const id = button.getAttribute('data-id');
const name = button.getAttribute('data-name');
const editForm = editWorkerModal.querySelector('#editWorkerForm');
const resetForm = confirmResetModal.querySelector('form');
editForm.action = "/admin/workers/edit/" + id;
resetForm.action = "/admin/workers/reset_password/" + id;
confirmResetModal.querySelector('.modal-body').textContent = `¿Estás seguro de generar una nueva contraseña para ${name}? La anterior dejará de funcionar.`;
editWorkerModal.querySelector('#edit_worker_rut').value = button.getAttribute('data-rut');
editWorkerModal.querySelector('#edit_worker_name').value = name;
editWorkerModal.querySelector('#edit_worker_phone').value = button.getAttribute('data-phone');
editWorkerModal.querySelector('#edit_worker_modulo').value = button.getAttribute('data-modulo');
editWorkerModal.querySelector('#edit_worker_tipo').value = button.getAttribute('data-tipo');
}
});
}
// Lógica para reabrir el modal de edición al cancelar el de confirmación
if (confirmResetModal) {
// Buscamos el botón de cancelar dentro del modal de confirmación
const btnCancelar = confirmResetModal.querySelector('.btn-secondary');
const btnCerrarX = confirmResetModal.querySelector('.btn-close');
const reabrirEdicion = () => {
const modalEdicion = new bootstrap.Modal(editWorkerModal);
modalEdicion.show();
};
btnCancelar.addEventListener('click', reabrirEdicion);
btnCerrarX.addEventListener('click', reabrirEdicion);
}
document.getElementById('rutInput').addEventListener('input', function(e) {
let value = this.value.replace(/[^0-9kK]/g, '').toUpperCase();
// Bloquear el largo interno a 9 caracteres (8 cuerpo + 1 dv)
if (value.length > 9) {
value = value.slice(0, 9);
}
if (value.length > 1) {
let body = value.slice(0, -1);
let dv = value.slice(-1);
body = body.replace(/\B(?=(\d{3})+(?!\d))/g, ".");
this.value = `${body}-${dv}`;
} else {
this.value = value;
}
});
function formatPhone(input) {
let value = input.value.replace(/\D/g, '');
if (value.startsWith('56')) value = value.substring(2);
value = value.substring(0, 9);
if (value.length > 5) input.value = value.replace(/(\d{1})(\d{4})(\d+)/, '$1 $2 $3');
else if (value.length > 1) input.value = value.replace(/(\d{1})(\d+)/, '$1 $2');
else input.value = value;
}
document.querySelectorAll('.phone-input, #phoneInput').forEach(inp => {
inp.addEventListener('input', () => formatPhone(inp));
});
// --- LÓGICA DE FILTRADO DE TRABAJADORES ---
const searchInputWorker = document.getElementById('searchWorker');
const moduleSelectFilter = document.getElementById('filterModule');
const typeSelectFilter = document.getElementById('filterType');
const workerRows = document.querySelectorAll('.worker-row');
function filterWorkers() {
const searchTerm = searchInputWorker.value.toLowerCase();
const selectedModule = moduleSelectFilter.value;
const selectedType = typeSelectFilter.value;
workerRows.forEach(row => {
// Asumiendo que celda 0 es RUT y celda 1 es Nombre
const rut = row.cells[0].textContent.toLowerCase();
const name = row.cells[1].textContent.toLowerCase();
const rowModule = row.getAttribute('data-modulo');
const rowType = row.getAttribute('data-tipo');
const matchesSearch = rut.includes(searchTerm) || name.includes(searchTerm);
const matchesModule = selectedModule === 'all' || rowModule === selectedModule;
const matchesType = selectedType === 'all' || rowType === selectedType;
if (matchesSearch && matchesModule && matchesType) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
}
searchInputWorker.addEventListener('input', filterWorkers);
moduleSelectFilter.addEventListener('change', filterWorkers);
typeSelectFilter.addEventListener('change', filterWorkers);
</script>
<script src="{{ url_for('static', filename='js/admin_workers.js') }}"></script>
{% endblock %}

View File

@@ -1,4 +1,5 @@
{% extends "macros/base.html" %}
{% from "macros/ui.html" import flashed_messages %}
{% block title %}Inicio de sesión{% endblock %}
@@ -10,14 +11,8 @@
<h4 class="mb-0">Iniciar Sesión</h4>
</div>
<div class="card-body p-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message|safe }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{{ flashed_messages() }}
<form method="POST">
<div class="mb-3">
<label class="form-label">RUT</label>
@@ -43,22 +38,5 @@
{% endblock %}
{% block scripts %}
<script>
document.getElementById('rutInput').addEventListener('input', function(e) {
let value = this.value.replace(/[^0-9kK]/g, '').toUpperCase();
if (value.length > 9) {
value = value.slice(0, 9);
}
if (value.length > 1) {
let body = value.slice(0, -1);
let dv = value.slice(-1);
body = body.replace(/\B(?=(\d{3})+(?!\d))/g, ".");
this.value = `${body}-${dv}`;
} else {
this.value = value;
}
});
</script>
{% endblock %}
<script src="{{ url_for('static', filename='js/login.js') }}"></script>
{% endblock %}

View File

@@ -6,6 +6,8 @@
<title>Sistema de Rendiciones - {% block title %}{% endblock %}</title>
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.png') }}" type="image/x-icon">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/report-tables.css') }}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
</head>
@@ -21,7 +23,9 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='cookieStuff.js') }}"></script>
<script src="{{ url_for('static', filename='themeStuff.js') }}"></script>
<script src="{{ url_for('static', filename='js/format-helpers.js') }}"></script>
<script src="{{ url_for('static', filename='js/navbar.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>
</html>

View File

@@ -275,7 +275,7 @@
</div>
<div>
<span class="text-muted d-block mb-1">Observaciones:</span>
<p class="mb-0 bg-dark p-2 rounded border border-secondary text-wrap text-break" style="font-size: 0.9em;">
<p class="mb-0 bg-body-tertiary p-2 rounded border border-secondary text-wrap text-break" style="font-size: 0.9em;">
{{ rendicion[9] if rendicion[9] else "Sin observaciones." }}
</p>
</div>
@@ -301,7 +301,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-start">
<form method="POST" action="{{ url_for('edit_rendicion', id=rendicion[0]) }}" id="editRendicionForm_{{ rendicion[0] }}">
<form method="POST" action="{{ url_for('admin.edit_rendicion', id=rendicion[0]) }}" id="editRendicionForm_{{ rendicion[0] }}">
<div class="row">
<div class="col-md-8 mb-3">
<div class="card shadow-sm h-100">
@@ -400,32 +400,32 @@
<label class="small text-muted mb-0">Débito</label>
<input type="text" class="form-control form-control-sm text-end money-input mb-1" id="edit_debito_{{ rendicion[0] }}" name="venta_debito" value="{{ '{:,.0f}'.format(rendicion[4] or 0).replace(',', '.') }}" oninput="calcTotalEdit({{ rendicion[0] }})">
<div class="input-group input-group-sm">
<span class="input-group-text bg-dark border-secondary text-muted" style="font-size: 0.7em;">Boletas</span>
<input type="number" class="form-control text-center bg-dark text-white border-secondary" name="boletas_debito" value="{{ rendicion[16] or 0 }}">
<span class="input-group-text bg-body-tertiary text-muted" style="font-size: 0.7em;">Boletas</span>
<input type="number" class="form-control text-center bg-body text-body border-secondary" name="boletas_debito" value="{{ rendicion[16] or 0 }}">
</div>
</div>
<div class="col-6">
<label class="small text-muted mb-0">Crédito</label>
<input type="text" class="form-control form-control-sm text-end money-input mb-1" id="edit_credito_{{ rendicion[0] }}" name="venta_credito" value="{{ '{:,.0f}'.format(rendicion[5] or 0).replace(',', '.') }}" oninput="calcTotalEdit({{ rendicion[0] }})">
<div class="input-group input-group-sm">
<span class="input-group-text bg-dark border-secondary text-muted" style="font-size: 0.7em;">Boletas</span>
<input type="number" class="form-control text-center bg-dark text-white border-secondary" name="boletas_credito" value="{{ rendicion[17] or 0 }}">
<span class="input-group-text bg-body-tertiary text-muted" style="font-size: 0.7em;">Boletas</span>
<input type="number" class="form-control text-center bg-body text-body border-secondary" name="boletas_credito" value="{{ rendicion[17] or 0 }}">
</div>
</div>
<div class="col-6">
<label class="small text-muted mb-0">Mercado Pago</label>
<input type="text" class="form-control form-control-sm text-end money-input mb-1" id="edit_mp_{{ rendicion[0] }}" name="venta_mp" value="{{ '{:,.0f}'.format(rendicion[6] or 0).replace(',', '.') }}" oninput="calcTotalEdit({{ rendicion[0] }})">
<div class="input-group input-group-sm">
<span class="input-group-text bg-dark border-secondary text-muted" style="font-size: 0.7em;">Boletas</span>
<input type="number" class="form-control text-center bg-dark text-white border-secondary" name="boletas_mp" value="{{ rendicion[18] or 0 }}">
<span class="input-group-text bg-body-tertiary text-muted" style="font-size: 0.7em;">Boletas</span>
<input type="number" class="form-control text-center bg-body text-body border-secondary" name="boletas_mp" value="{{ rendicion[18] or 0 }}">
</div>
</div>
<div class="col-6">
<label class="small text-muted mb-0">Efectivo</label>
<input type="text" class="form-control form-control-sm text-end money-input mb-1" id="edit_efectivo_{{ rendicion[0] }}" name="venta_efectivo" value="{{ '{:,.0f}'.format(rendicion[7] or 0).replace(',', '.') }}" oninput="calcTotalEdit({{ rendicion[0] }})">
<div class="input-group input-group-sm">
<span class="input-group-text bg-dark border-secondary text-muted" style="font-size: 0.7em;">Boletas</span>
<input type="number" class="form-control text-center bg-dark text-white border-secondary" name="boletas_efectivo" value="{{ rendicion[19] or 0 }}">
<span class="input-group-text bg-body-tertiary text-muted" style="font-size: 0.7em;">Boletas</span>
<input type="number" class="form-control text-center bg-body text-body border-secondary" name="boletas_efectivo" value="{{ rendicion[19] or 0 }}">
</div>
</div>
</div>
@@ -443,13 +443,13 @@
<div class="mb-2">
<label class="small text-danger fw-bold">Monto Gastos</label>
<div class="input-group input-group-sm">
<span class="input-group-text bg-dark border-danger text-danger">-$</span>
<span class="input-group-text bg-danger-subtle border-danger text-danger">-$</span>
<input type="text" class="form-control money-input border-danger text-end" name="gastos" value="{{ '{:,.0f}'.format(rendicion[8] or 0).replace(',', '.') }}" required>
</div>
</div>
<div class="mb-0">
<label class="small text-muted">Observaciones</label>
<textarea class="form-control form-control-sm bg-dark text-white" name="observaciones" rows="2">{{ rendicion[9] }}</textarea>
<textarea class="form-control form-control-sm bg-body text-body" name="observaciones" rows="2">{{ rendicion[9] }}</textarea>
</div>
</div>
</div>
@@ -486,7 +486,7 @@
<h5 class="card-title mb-0">Detalle de Ventas</h5>
</div>
<p class="text-muted small flex-grow-1">Análisis detallado de ventas diarias, productos vendidos y consolidado mensual.</p>
<a href="{{ url_for('report_modulo_periodo', modulo_id=modulo_id) }}" class="btn btn-primary w-100 mt-3">Generar Reporte</a>
<a href="{{ url_for('admin.report_modulo_periodo', modulo_id=modulo_id) }}" class="btn btn-primary w-100 mt-3">Generar Reporte</a>
</div>
</div>
</div>
@@ -501,7 +501,7 @@
<h5 class="card-title mb-0">Comisiones</h5>
</div>
<p class="text-muted small flex-grow-1">Cálculo de comisiones generadas por los trabajadores en este módulo.</p>
<a href="{{ url_for('report_modulo_comisiones', modulo_id=modulo_id) }}" class="btn btn-success w-100 mt-3">Generar Reporte</a>
<a href="{{ url_for('admin.report_modulo_comisiones', modulo_id=modulo_id) }}" class="btn btn-success w-100 mt-3">Generar Reporte</a>
</div>
</div>
</div>
@@ -516,7 +516,7 @@
<h5 class="card-title mb-0">Control de Horarios</h5>
</div>
<p class="text-muted small flex-grow-1">Registro de horas de entrada y salida de los trabajadores y acompañantes.</p>
<a href="{{ url_for('report_modulo_horarios', modulo_id=modulo_id) }}" class="btn btn-warning w-100 mt-3">Generar Reporte</a>
<a href="{{ url_for('admin.report_modulo_horarios', modulo_id=modulo_id) }}" class="btn btn-warning w-100 mt-3">Generar Reporte</a>
</div>
</div>
</div>
@@ -531,7 +531,7 @@
<h5 class="card-title mb-0">Centros Comerciales</h5>
</div>
<p class="text-muted small flex-grow-1">Reporte de datos exigidos por la administración del centro comercial.</p>
<a href="{{ url_for('report_modulo_centros_comerciales', modulo_id=modulo_id) }}" class="btn btn-info w-100 mt-3">Generar Reporte</a>
<a href="{{ url_for('admin.report_modulo_centros_comerciales', modulo_id=modulo_id) }}" class="btn btn-info w-100 mt-3">Generar Reporte</a>
</div>
</div>
</div>
@@ -546,7 +546,7 @@
<h5 class="card-title mb-0">Cálculo de IVA</h5>
</div>
<p class="text-muted small flex-grow-1">Proyección de impuestos basados en las ventas declaradas.</p>
<a href="{{ url_for('report_modulo_calculo_iva', modulo_id=modulo_id) }}" class="btn btn-secondary w-100 mt-3">Generar Reporte</a>
<a href="{{ url_for('admin.report_modulo_calculo_iva', modulo_id=modulo_id) }}" class="btn btn-secondary w-100 mt-3">Generar Reporte</a>
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
<nav class="navbar navbar-expand-lg bg-body-tertiary shadow-sm border-bottom mb-4">
<div class="container">
<a class="navbar-brand d-flex flex-column align-items-start text-primary-emphasis" href="{{ url_for('index') }}" style="gap: 0;">
<a class="navbar-brand d-flex flex-column align-items-start text-primary-emphasis" href="{{ url_for('auth.index') }}" style="gap: 0;">
<div class="d-flex align-items-center">
<i id="brandIcon" class="bi bi-receipt-cutoff fs-3 text-info me-2"></i>
@@ -21,33 +21,33 @@
<ul class="navbar-nav me-auto">
{% if session.get('is_admin') %}
<li class="nav-item">
<a class="nav-link d-flex align-items-center {{ 'active fw-bold' if 'rendiciones' in request.path }}" href="{{ url_for('admin_rendiciones') }}">
<a class="nav-link d-flex align-items-center {{ 'active fw-bold' if 'rendiciones' in request.path }}" href="{{ url_for('admin.admin_rendiciones') }}">
<i class="bi bi-journal-text me-1"></i> Rendiciones
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center {{ 'active fw-bold' if request.endpoint == 'manage_workers' }}" href="{{ url_for('manage_workers') }}">
<a class="nav-link d-flex align-items-center {{ 'active fw-bold' if request.endpoint == 'admin.manage_workers' }}" href="{{ url_for('admin.manage_workers') }}">
<i class="bi bi-people me-1"></i> Trabajadores
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center {{ 'active fw-bold' if request.endpoint == 'manage_structure' }}" href="{{ url_for('manage_structure') }}">
<a class="nav-link d-flex align-items-center {{ 'active fw-bold' if request.endpoint == 'admin.manage_structure' }}" href="{{ url_for('admin.manage_structure') }}">
<i class="bi bi-diagram-3 me-1"></i> Estructura
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center {{ 'active fw-bold' if request.endpoint == 'manage_products' }}" href="{{ url_for('manage_products') }}">
<a class="nav-link d-flex align-items-center {{ 'active fw-bold' if request.endpoint == 'admin.manage_products' }}" href="{{ url_for('admin.manage_products') }}">
<i class="bi bi-box-seam me-1"></i> Productos
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'reporte' in request.endpoint %}active fw-bold text-white{% endif %}" href="{{ url_for('admin_reportes_index') }}">
<a class="nav-link {% if request.endpoint and 'reporte' in request.endpoint %}active fw-bold text-white{% endif %}" href="{{ url_for('admin.admin_reportes_index') }}">
<i class="bi bi-graph-up me-1"></i> Reportes
</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link {{ 'active fw-bold' if request.endpoint == 'worker_dashboard' }}" href="{{ url_for('worker_dashboard') }}">
<a class="nav-link {{ 'active fw-bold' if request.endpoint == 'worker.worker_dashboard' }}" href="{{ url_for('worker.worker_dashboard') }}">
<i class="bi bi-speedometer2 me-1"></i> Mis Rendiciones
</a>
</li>
@@ -63,7 +63,7 @@
<i class="bi bi-person-circle me-1 text-info"></i> {{ session.get('rut') }}
</span>
<a href="{{ url_for('logout') }}" class="btn btn-outline-danger btn-sm rounded-pill px-3">
<a href="{{ url_for('auth.logout') }}" class="btn btn-outline-danger btn-sm rounded-pill px-3">
Salir
</a>
</div>
@@ -72,52 +72,3 @@
</div>
</nav>
<style>
@keyframes baguetteRoll {
0% { transform: rotate(0deg) scale(1.2); }
50% { transform: rotate(180deg) scale(1.5); }
100% { transform: rotate(360deg) scale(1); }
}
.baguette-spin {
display: inline-block;
animation: baguetteRoll 1s ease-in-out;
font-style: normal;
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function() {
const brandIcon = document.getElementById("brandIcon");
if (brandIcon) {
let clickCount = 0;
let clickResetTimer;
brandIcon.addEventListener("click", function(e) {
e.preventDefault();
e.stopPropagation();
clickCount++;
clearTimeout(clickResetTimer);
clickResetTimer = setTimeout(() => {
clickCount = 0;
}, 800);
if (clickCount >= 5) {
clickCount = 0;
clearTimeout(clickResetTimer);
const originalClass = this.className;
this.className = "fs-3 me-2 baguette-spin";
this.innerHTML = "&#129366;";
setTimeout(() => {
this.className = originalClass;
this.innerHTML = "";
}, 1000);
}
});
}
});
</script>

22
templates/macros/ui.html Normal file
View File

@@ -0,0 +1,22 @@
{% macro flashed_messages() %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message|safe }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% endmacro %}
{% macro back_link(url, text='Volver', icon='bi-arrow-left') %}
<a href="{{ url }}" class="btn btn-outline-secondary btn-sm mb-2">
<i class="bi {{ icon }}"></i> {{ text }}
</a>
{% endmacro %}
{% macro page_header(title, icon='', actions='') %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>{% if icon %}<i class="bi {{ icon }} me-2"></i>{% endif %}{{ title }}</h2>
{% if actions %}{{ actions|safe }}{% endif %}
</div>
{% endmacro %}

View File

@@ -1,19 +1,14 @@
{% extends "macros/base.html" %}
{% from 'macros/modals.html' import confirm_modal, alert_modal %}
{% from "macros/ui.html" import flashed_messages, back_link %}
{% block title %}Rendición de Caja{% endblock %}
{% block head %}
<!-- HEAD -->
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<a href="{{ url_for('worker_dashboard') }}" class="btn btn-outline-secondary btn-sm mb-2">
<i class="bi bi-arrow-left"></i> Volver al Historial
</a>
{{ back_link(url_for('worker.worker_dashboard'), 'Volver al Historial') }}
<h2>Nueva Rendición de Caja</h2>
</div>
<div class="text-end text-muted">
@@ -22,13 +17,7 @@
</div>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message|safe }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{{ flashed_messages() }}
<form method="POST">
<div class="card mb-4 shadow-sm">
@@ -241,228 +230,5 @@
{% endblock %}
{% block scripts %}
<script>
document.getElementById('companion_select').addEventListener('change', function() {
const timeDiv = document.getElementById('companion_times_div');
const compIn = document.getElementById('comp_in');
const compOut = document.getElementById('comp_out');
if (this.value) {
timeDiv.style.display = 'block';
compIn.required = true;
compOut.required = true;
} else {
timeDiv.style.display = 'none';
compIn.required = false;
compOut.required = false;
compIn.value = '';
compOut.value = '';
}
});
</script>
<script>
const inputsCantidad = document.querySelectorAll('input[name^="qty_"]');
const displayTotalProductos = document.getElementById('total_productos_calc');
function calcularVentaProductos() {
let granTotal = 0;
const filas = document.querySelectorAll('tbody tr');
filas.forEach(fila => {
const inputQty = fila.querySelector('input[name^="qty_"]');
if (inputQty) {
if (parseInt(inputQty.value) < 0) {
inputQty.value = 0;
}
const cantidad = parseInt(inputQty.value) || 0;
const precioTexto = fila.cells[1].innerText.replace(/\D/g, '');
const precio = parseInt(precioTexto) || 0;
granTotal += (cantidad * precio);
}
});
displayTotalProductos.value = granTotal.toLocaleString('es-CL');
checkWarnings();
}
inputsCantidad.forEach(input => {
input.addEventListener('keydown', function(e) {
if (['Backspace', 'Tab', 'ArrowLeft', 'ArrowRight', 'Delete', 'Enter'].includes(e.key) || e.ctrlKey || e.metaKey) return;
if (e.key < '0' || e.key > '9') {
e.preventDefault();
}
});
input.addEventListener('input', function() {
this.value = this.value.replace(/\D/g, '');
calcularVentaProductos();
});
});
function checkWarnings() {
const getVal = (id) => {
const el = document.getElementById(id);
if (!el || !el.value.trim() || el.value === '0') return 0;
return parseInt(el.value.replace(/\D/g, '')) || 0;
};
const totalProductos = getVal('total_productos_calc');
const totalDeclarado = getVal('total_general');
const gastos = getVal('gastos');
const efectivo = getVal('venta_efectivo');
let warnings = [];
// Comprobar diferencias en totales (solo si se ha ingresado algo para no mostrar el error de entrada)
if ((totalProductos > 0 || totalDeclarado > 0) && totalProductos !== totalDeclarado) {
warnings.push("El <strong>Total Venta por Productos</strong> no coincide con el <strong>Total Ventas Declaradas</strong>.");
}
// Comprobar si los gastos superan el efectivo
if (gastos > efectivo) {
warnings.push("El <strong>Monto de Gastos</strong> es mayor que el <strong>Efectivo</strong> declarado.");
}
const warningContainer = document.getElementById('discrepancy_warning');
const warningText = document.getElementById('discrepancy_text');
if (warnings.length > 0) {
warningText.innerHTML = warnings.join("<br>");
warningContainer.style.display = 'block';
} else {
warningContainer.style.display = 'none';
}
}
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const submitModal = document.getElementById('confirmSubmitModal');
const mainForm = document.querySelector('form');
const alertModalEl = document.getElementById('globalAlertModal');
const alertModal = new bootstrap.Modal(alertModalEl);
const confirmBtn = submitModal.querySelector('button[type="submit"]');
function mostrarError(mensaje) {
document.getElementById('globalAlertModalBody').textContent = mensaje;
alertModal.show();
}
function validarFormulario() {
const requiredInputs = mainForm.querySelectorAll('[required]');
let valid = true;
requiredInputs.forEach(input => {
const isMoney = input.classList.contains('money-input');
if (!input.value.trim() || (isMoney && input.value === '')) {
input.classList.add('is-invalid');
valid = false;
} else {
input.classList.remove('is-invalid');
}
});
return valid;
}
confirmBtn.addEventListener('click', function(e) {
e.preventDefault();
if (validarFormulario()) {
mainForm.submit();
} else {
bootstrap.Modal.getInstance(submitModal).hide();
mostrarError("Por favor, rellena los campos obligatorios (Fecha y Hora) antes de enviar.");
}
});
document.querySelectorAll('.money-input').forEach(input => {
if (!input.value.trim()) input.value = '0';
});
});
</script>
<script>
const inputsVenta = document.querySelectorAll('.sale-input');
const displayDigital = document.getElementById('total_digital');
const displayGeneral = document.getElementById('total_general');
function calcularTotales() {
const getVal = (id) => {
const el = document.getElementById(id);
if (!el || !el.value.trim() || el.value === '0') return 0;
return parseInt(el.value.replace(/\D/g, '')) || 0;
};
const debito = getVal('venta_debito');
const credito = getVal('venta_credito');
const mp = getVal('venta_mp');
const efectivo = getVal('venta_efectivo');
const totalDigital = debito + credito + mp;
const totalGeneral = totalDigital + efectivo; // Ya no restamos los gastos aquí
document.getElementById('total_digital').value = totalDigital.toLocaleString('es-CL');
document.getElementById('total_general').value = totalGeneral.toLocaleString('es-CL');
checkWarnings();
}
inputsVenta.forEach(input => {
input.addEventListener('input', calcularTotales);
});
document.querySelector('form').addEventListener('submit', function(e) {
const requiredInputs = this.querySelectorAll('[required]');
let valid = true;
requiredInputs.forEach(input => {
const isMoney = input.classList.contains('money-input');
if (!input.value.trim() || (isMoney && input.value === '')) {
input.classList.add('is-invalid');
valid = false;
} else {
input.classList.remove('is-invalid');
}
});
if (!valid) {
e.preventDefault();
const alertModalEl = document.getElementById('globalAlertModal');
if (alertModalEl) {
const alertModal = bootstrap.Modal.getOrCreateInstance(alertModalEl);
document.getElementById('globalAlertModalBody').textContent = "Por favor, rellena todos los campos obligatorios antes de enviar.";
alertModal.show();
} else {
alert("Por favor, rellena todos los campos obligatorios.");
}
}
});
document.querySelectorAll('.money-input').forEach(function(input) {
input.addEventListener('keydown', function(e) {
if (['Backspace', 'Tab', 'ArrowLeft', 'ArrowRight', 'Delete', 'Enter'].includes(e.key) || e.ctrlKey || e.metaKey) return;
if (e.key < '0' || e.key > '9') {
e.preventDefault();
}
});
input.addEventListener('focus', function() {
if (this.value === '0') this.value = '';
});
input.addEventListener('blur', function() {
if (this.value.trim() === '' || this.value.trim() === '0') {
this.value = '0';
}
calcularTotales();
});
input.addEventListener('input', function() {
let value = this.value.replace(/\D/g, '');
if (value !== '') {
this.value = parseInt(value, 10).toLocaleString('es-CL');
}
calcularTotales();
});
});
</script>
<script src="{{ url_for('static', filename='js/worker_dashboard.js') }}"></script>
{% endblock %}

View File

@@ -1,23 +1,18 @@
{% extends "macros/base.html" %}
{% from 'macros/modals.html' import rendicion_detail_modal %}
{% from "macros/ui.html" import flashed_messages %}
{% block title %}Mis Rendiciones{% endblock %}
{% block content %}
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-4 gap-3">
<h2 class="mb-0">Mis Rendiciones</h2>
<a href="{{ url_for('new_rendicion') }}" class="btn btn-success shadow-sm align-self-start align-self-md-auto">
<a href="{{ url_for('worker.new_rendicion') }}" class="btn btn-success shadow-sm align-self-start align-self-md-auto">
<i class="bi bi-plus-circle me-2"></i>Nueva Rendición
</a>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message|safe }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{{ flashed_messages() }}
<div class="card shadow-sm border-0">
<div class="card-body p-0">