modified: database.py

modified:   routes_admin.py
	modified:   routes_worker.py
	modified:   templates/admin_productos.html
	modified:   templates/admin_workers.html
	modified:   templates/macros/modals.html
	modified:   templates/worker_dashboard.html
This commit is contained in:
2026-05-28 00:59:37 -04:00
parent a8256860a2
commit 9c4753cd1f
7 changed files with 534 additions and 135 deletions

View File

@@ -71,10 +71,20 @@ def populateDefaults():
] ]
c.execute("SELECT id FROM zonas") c.execute("SELECT id FROM zonas")
zonas_ids = [row[0] for row in c.fetchall()] zonas_ids = [row[0] for row in c.fetchall()]
for zona_id in zonas_ids:
# Fecha de activación artificial en el pasado para que las ventas históricas funcionen
fecha_base_precios = (datetime.date.today() - datetime.timedelta(days=365*3)).strftime('%Y-%m-%d 00:00:00')
for name, price, commission in productos_base: for name, price, commission in productos_base:
c.execute("INSERT INTO productos (zona_id, name, price, commission) VALUES (?, ?, ?, ?)", c.execute("INSERT OR IGNORE INTO productos (name) VALUES (?)", (name,))
(zona_id, name, price, commission)) c.execute("SELECT id FROM productos WHERE name = ?", (name,))
p_id = c.fetchone()[0]
for zona_id in zonas_ids:
c.execute('''INSERT INTO precios_historicos
(producto_id, zona_id, price, commission, fecha_activacion)
VALUES (?, ?, ?, ?, ?)''',
(p_id, zona_id, price, commission, fecha_base_precios))
conn.commit() conn.commit()
c.execute("SELECT COUNT(*) FROM workers WHERE is_admin = 0") c.execute("SELECT COUNT(*) FROM workers WHERE is_admin = 0")
@@ -165,7 +175,19 @@ def populateDefaults():
rend_id = c.lastrowid rend_id = c.lastrowid
c.execute("SELECT id, price, commission FROM productos WHERE zona_id = (SELECT zona_id FROM modulos WHERE id = ?)", (m_id,)) # Buscar el precio activo en el momento exacto de la venta histórica
c.execute('''
SELECT p.id, ph.price, ph.commission
FROM productos p
JOIN precios_historicos ph ON p.id = ph.producto_id
WHERE ph.zona_id = (SELECT zona_id FROM modulos WHERE id = ?)
AND ph.fecha_activacion = (
SELECT MAX(fecha_activacion)
FROM precios_historicos
WHERE producto_id = p.id AND zona_id = ph.zona_id
AND fecha_activacion <= ?
)
''', (m_id, fecha_str + ' 23:59:59'))
prods_zona = c.fetchall() prods_zona = c.fetchall()
if prods_zona: if prods_zona:
@@ -194,12 +216,20 @@ def init_db():
zona_id INTEGER NOT NULL, zona_id INTEGER NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
FOREIGN KEY (zona_id) REFERENCES zonas(id))''') FOREIGN KEY (zona_id) REFERENCES zonas(id))''')
# 1. Catálogo Maestro (Solo el nombre)
c.execute('''CREATE TABLE IF NOT EXISTS productos c.execute('''CREATE TABLE IF NOT EXISTS productos
(id INTEGER PRIMARY KEY AUTOINCREMENT, (id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL)''')
# 2. Historial de Precios por Zona
c.execute('''CREATE TABLE IF NOT EXISTS precios_historicos
(id INTEGER PRIMARY KEY AUTOINCREMENT,
producto_id INTEGER NOT NULL,
zona_id INTEGER NOT NULL, zona_id INTEGER NOT NULL,
name TEXT NOT NULL, price INTEGER NOT NULL,
price REAL NOT NULL, commission INTEGER NOT NULL,
commission REAL NOT NULL, fecha_activacion DATETIME NOT NULL,
FOREIGN KEY (producto_id) REFERENCES productos(id),
FOREIGN KEY (zona_id) REFERENCES zonas(id))''') FOREIGN KEY (zona_id) REFERENCES zonas(id))''')
c.execute('''CREATE TABLE IF NOT EXISTS workers c.execute('''CREATE TABLE IF NOT EXISTS workers
(id INTEGER PRIMARY KEY AUTOINCREMENT, (id INTEGER PRIMARY KEY AUTOINCREMENT,

View File

@@ -1,5 +1,5 @@
import sqlite3 import sqlite3
from flask import app, render_template, request, redirect, url_for, flash, session from flask import app, render_template, request, redirect, url_for, flash, session, jsonify
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from datetime import date, datetime from datetime import date, datetime
from database import get_db_connection from database import get_db_connection
@@ -211,88 +211,166 @@ def register_admin_routes(app):
if request.method == 'POST': if request.method == 'POST':
name = request.form.get('name').strip() name = request.form.get('name').strip()
zona_id = request.form.get('zona_id')
raw_price = request.form.get('price').replace('.', '')
raw_commission = request.form.get('commission').replace('.', '')
if not zona_id:
flash("Debes seleccionar una Zona.", "danger")
else:
try: try:
price = int(raw_price) # 1. Crear producto maestro
commission = int(raw_commission) c.execute("INSERT INTO productos (name) VALUES (?)", (name,))
prod_id = c.lastrowid
# 2. Inicializar precios en 0 para todas las zonas activas
c.execute("SELECT id FROM zonas")
zonas = c.fetchall()
for zona in zonas:
c.execute('''INSERT INTO precios_historicos
(producto_id, zona_id, price, commission, is_active)
VALUES (?, ?, 0, 0, 1)''', (prod_id, zona[0]))
c.execute("INSERT INTO productos (zona_id, name, price, commission) VALUES (?, ?, ?, ?)",
(zona_id, name, price, commission))
conn.commit() conn.commit()
flash("Producto guardado exitosamente.", "success") flash("Producto maestro creado. No olvides configurar sus precios.", "success")
except ValueError: except sqlite3.IntegrityError:
flash("El precio y la comisión deben ser números enteros válidos.", "danger") flash("Ese producto ya existe en el catálogo.", "danger")
return redirect(url_for('manage_products')) return redirect(url_for('manage_products'))
c.execute("SELECT id, name FROM zonas ORDER BY name") c.execute("SELECT id, name FROM zonas ORDER BY name")
zonas = c.fetchall() zonas = c.fetchall()
c.execute('''SELECT p.id, p.name, p.price, p.commission, z.name, p.zona_id # Obtener productos y el precio VIGENTE por zona usando una subquery
c.execute('''
SELECT p.id, p.name,
z.id as zona_id, z.name as zona_name,
ph.price, ph.commission
FROM productos p FROM productos p
JOIN zonas z ON p.zona_id = z.id CROSS JOIN zonas z
ORDER BY z.name, p.name''') LEFT JOIN precios_historicos ph
productos = c.fetchall() ON p.id = ph.producto_id AND z.id = ph.zona_id
AND ph.fecha_activacion = (
SELECT MAX(fecha_activacion)
FROM precios_historicos
WHERE producto_id = p.id AND zona_id = z.id
AND fecha_activacion <= datetime('now', 'localtime')
)
ORDER BY p.name, z.name
''')
# Agrupar datos para la vista: {prod_id: {'name': 'Lentes', 'precios': {zona_id: {'price': X, 'comm': Y}}}}
raw_data = c.fetchall()
productos_dict = {}
for row in raw_data:
p_id, p_name, z_id, z_name, price, comm = row
if p_id not in productos_dict:
# Añadimos la lista 'futuros'
productos_dict[p_id] = {'id': p_id, 'name': p_name, 'precios': {}, 'futuros': []}
productos_dict[p_id]['precios'][z_id] = {
'zona_name': z_name,
'price': price or 0,
'commission': comm or 0
}
# BÚSQUEDA DE PRECIOS PROGRAMADOS (Futuro)
c.execute('''
SELECT ph.id, ph.producto_id, z.name, ph.price, ph.commission, ph.fecha_activacion
FROM precios_historicos ph
JOIN zonas z ON ph.zona_id = z.id
WHERE ph.fecha_activacion > datetime('now', 'localtime')
ORDER BY ph.fecha_activacion ASC
''')
future_data = c.fetchall()
for row in future_data:
ph_id, p_id, z_name, price, comm, fecha = row
if p_id in productos_dict:
productos_dict[p_id]['futuros'].append({
'id': ph_id,
'zona_name': z_name,
'price': price,
'commission': comm,
'fecha': fecha
})
conn.close() conn.close()
return render_template('admin_productos.html', zonas=zonas, productos=productos) return render_template('admin_productos.html', zonas=zonas, productos=productos_dict.values())
@app.route('/admin/productos/edit/<int:id>', methods=['GET', 'POST']) from datetime import datetime
@admin_required
def edit_product(id):
conn = get_db_connection()
c = conn.cursor()
if request.method == 'POST':
name = request.form.get('name').strip()
zona_id = request.form.get('zona_id')
raw_price = request.form.get('price').replace('.', '')
raw_commission = request.form.get('commission').replace('.', '')
try:
price = int(raw_price)
commission = int(raw_commission)
c.execute("UPDATE productos SET zona_id=?, name=?, price=?, commission=? WHERE id=?",
(zona_id, name, price, commission, id))
conn.commit()
flash("Producto actualizado exitosamente.", "success")
conn.close()
return redirect(url_for('manage_products'))
except ValueError:
flash("El precio y la comisión deben ser números enteros válidos.", "danger")
c.execute("SELECT id, name FROM zonas ORDER BY name")
zonas = c.fetchall()
c.execute("SELECT id, zona_id, name, price, commission FROM productos WHERE id=?", (id,))
producto = c.fetchone()
conn.close()
if not producto:
return redirect(url_for('manage_products'))
return render_template('edit_producto.html', zonas=zonas, producto=producto)
@app.route('/admin/productos/delete/<int:id>', methods=['POST']) @app.route('/admin/productos/delete/<int:id>', methods=['POST'])
@admin_required @admin_required
def delete_product(id): def delete_product(id):
conn = get_db_connection() conn = get_db_connection()
c = conn.cursor() c = conn.cursor()
try:
# Borrar historial de precios primero
c.execute("DELETE FROM precios_historicos WHERE producto_id=?", (id,))
# Borrar producto maestro
c.execute("DELETE FROM productos WHERE id=?", (id,)) c.execute("DELETE FROM productos WHERE id=?", (id,))
conn.commit() conn.commit()
flash("Producto maestro y su historial eliminados.", "info")
except sqlite3.IntegrityError:
# Si hay ventas asociadas en rendicion_items, SQLite bloqueará el borrado
flash("No puedes eliminar este producto porque ya tiene ventas registradas. Cámbiale el precio a 0 en su lugar.", "danger")
finally:
conn.close() conn.close()
flash("Producto eliminado.", "info")
return redirect(url_for('manage_products')) return redirect(url_for('manage_products'))
@app.route('/admin/productos/precios/<int:id>', methods=['POST'])
@admin_required
def update_product_prices(id):
conn = get_db_connection()
c = conn.cursor()
c.execute("SELECT id FROM zonas")
zonas = c.fetchall()
# Obtener fecha programada o usar la actual
fecha_input = request.form.get('fecha_activacion')
if fecha_input:
fecha_activacion = fecha_input.replace('T', ' ') + ':00'
else:
fecha_activacion = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
for zona in zonas:
z_id = str(zona[0])
new_price = int(request.form.get(f'price_{z_id}', '0').replace('.', ''))
new_comm = int(request.form.get(f'comm_{z_id}', '0').replace('.', ''))
c.execute('''INSERT INTO precios_historicos
(producto_id, zona_id, price, commission, fecha_activacion)
VALUES (?, ?, ?, ?, ?)''', (id, z_id, new_price, new_comm, fecha_activacion))
conn.commit()
conn.close()
flash(f"Precios actualizados. Entrarán en vigencia el {fecha_activacion}.", "success")
return redirect(url_for('manage_products'))
@app.route('/admin/productos/precios/cancelar/<int:id>', methods=['POST'])
@admin_required
def cancel_scheduled_price(id):
conn = get_db_connection()
c = conn.cursor()
c.execute("DELETE FROM precios_historicos WHERE id = ?", (id,))
conn.commit()
conn.close()
flash("Cambio de precio programado cancelado.", "info")
return redirect(url_for('manage_products'))
@app.route('/admin/api/productos/<int:id>/historial')
@admin_required
def api_product_history(id):
conn = get_db_connection()
c = conn.cursor()
c.execute('''
SELECT z.name, ph.price, ph.fecha_activacion
FROM precios_historicos ph
JOIN zonas z ON ph.zona_id = z.id
WHERE ph.producto_id = ?
ORDER BY ph.fecha_activacion ASC
''', (id,)) # <-- Añade la coma para que sea una tupla
rows = c.fetchall()
conn.close()
# Convertimos la data a una lista de diccionarios que JS entienda felizmente
history = [{'zona': r[0], 'price': r[1], 'fecha': r[2]} for r in rows]
return jsonify(history)
@app.route('/admin/rendiciones') @app.route('/admin/rendiciones')
@admin_required @admin_required
def admin_rendiciones(): def admin_rendiciones():

View File

@@ -133,7 +133,14 @@ def register_worker_routes(app):
prod_id = int(key.split('_')[1]) prod_id = int(key.split('_')[1])
cantidad = int(value) cantidad = int(value)
c.execute("SELECT price, commission FROM productos WHERE id = ?", (prod_id,)) # Buscar el precio vigente al momento de la venta
c.execute('''
SELECT price, commission
FROM precios_historicos
WHERE producto_id = ? AND zona_id = ?
AND fecha_activacion <= datetime('now', 'localtime')
ORDER BY fecha_activacion DESC LIMIT 1
''', (prod_id, zona_id))
prod_data = c.fetchone() prod_data = c.fetchone()
if prod_data: if prod_data:
@@ -153,7 +160,20 @@ def register_worker_routes(app):
''', (session['user_id'], modulo_id)) ''', (session['user_id'], modulo_id))
otros_trabajadores = c.fetchall() otros_trabajadores = c.fetchall()
c.execute("SELECT id, name, price, commission FROM productos WHERE zona_id = ? ORDER BY name", (zona_id,)) # Buscar solo el precio vigente actual para esta zona
c.execute('''
SELECT p.id, p.name, ph.price, ph.commission
FROM productos p
JOIN precios_historicos ph ON p.id = ph.producto_id
WHERE ph.zona_id = ?
AND ph.fecha_activacion = (
SELECT MAX(fecha_activacion)
FROM precios_historicos
WHERE producto_id = p.id AND zona_id = ?
AND fecha_activacion <= datetime('now', 'localtime')
)
ORDER BY p.name
''', (zona_id, zona_id)) # Nota: zona_id se pasa dos veces
productos = c.fetchall() productos = c.fetchall()
conn.close() conn.close()

View File

@@ -21,103 +21,254 @@
{{ edit_product_modal(zonas) }} {{ edit_product_modal(zonas) }}
<div class="card mb-4 shadow-sm"> <div class="card mb-4 shadow-sm">
<div class="card-header bg-primary text-white">Agregar Nuevo Producto</div> <div class="card-header bg-primary text-white">Agregar Producto Maestro</div>
<div class="card-body"> <div class="card-body">
<form method="POST" action="{{ url_for('manage_products') }}"> <form method="POST" action="{{ url_for('manage_products') }}">
<div class="row g-3"> <div class="row g-3">
<div class="col-md-3"> <div class="col-md-10">
<label class="form-label">Zona</label> <input type="text" class="form-control" name="name" placeholder="Nombre del Producto" required>
<select class="form-select" name="zona_id" required>
<option value="" selected disabled>Seleccionar Zona...</option>
{% for zona in zonas %}
<option value="{{ zona[0] }}">{{ zona[1] }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label class="form-label">Nombre del Producto</label>
<input type="text" class="form-control" name="name" required>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<label class="form-label">Precio de Venta</label> <button type="submit" class="btn btn-primary w-100">Crear Producto</button>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="text" class="form-control money-input" name="price" placeholder="1.500" required>
</div>
</div>
<div class="col-md-2">
<label class="form-label">Comisión</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="text" class="form-control money-input" name="commission" placeholder="500" required>
</div>
</div>
<div class="col-md-1 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">+</button>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<div class="input-group mb-3 shadow-sm">
<span class="input-group-text bg-body-tertiary"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" id="searchProduct" placeholder="Buscar producto maestro...">
</div>
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body p-0"> <div class="card-body p-0">
<table class="table table-striped table-hover mb-0"> <table class="table table-striped table-hover mb-0">
<thead class="table-dark"> <thead class="table-dark">
<tr> <tr>
<th>Zona</th> <th>Producto Maestro</th>
<th>Producto</th>
<th>Precio</th>
<th>Comisión</th>
<th class="text-end">Acciones</th> <th class="text-end">Acciones</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for prod in productos %} {% for prod in productos %}
<tr> <tr class="product-row">
<td class="align-middle"><span class="badge bg-info text-dark">{{ prod[4] }}</span></td> <td class="align-middle fw-bold">{{ prod.name }}</td>
<td class="align-middle">{{ prod[1] }}</td>
<td class="align-middle">${{ "{:,.0f}".format(prod[2]).replace(',', '.') }}</td>
<td class="align-middle">${{ "{:,.0f}".format(prod[3]).replace(',', '.') }}</td>
<td class="text-end"> <td class="text-end">
<button type="button" <button type="button" class="btn btn-info btn-sm text-white" onclick="showHistory({{ prod.id }}, '{{ prod.name }}')">
class="btn btn-primary btn-sm btn-edit-sm" <i class="bi bi-graph-up"></i> Historial
data-bs-toggle="modal"
data-bs-target="#editProductModal"
data-id="{{ prod[0] }}"
data-name="{{ prod[1] }}"
data-price="{{ prod[2]|int }}"
data-commission="{{ prod[3]|int }}"
data-zona="{{ prod[5] }}">
<i class="bi bi-pencil"></i>
</button> </button>
<button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#pricesModal{{ prod.id }}">
<button type="button" class="btn btn-danger btn-sm btn-del-sm" data-bs-toggle="modal" data-bs-target="#deleteProd{{ prod[0] }}"> <i class="bi bi-currency-dollar"></i> Precios
</button>
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#deleteProd{{ prod.id }}">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</button> </button>
{{ confirm_modal( {{ confirm_modal(
id='deleteProd' ~ prod[0], id='deleteProd' ~ prod.id,
title='Eliminar Producto', title='Eliminar Producto',
message='¿Estás seguro de que deseas eliminar "' ~ prod[1] ~ '"?', message='¿Eliminar "' ~ prod.name ~ '"? Esto fallará si el producto ya tiene ventas registradas.',
action_url=url_for('delete_product', id=prod[0]), action_url=url_for('delete_product', id=prod.id),
btn_class='btn-danger', btn_class='btn-danger',
btn_text='Eliminar permanentemente' btn_text='Eliminar'
) }} ) }}
</td> </td>
</tr> </tr>
{% else %}
<tr>
<td colspan="5" class="text-center py-3 text-muted">No hay productos registrados.</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
{% for prod in productos %}
<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) }}">
<div class="modal-header">
<h5 class="modal-title">Precios: {{ prod.name }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row fw-bold mb-2">
<div class="col-4">Zona</div>
<div class="col-4">Precio</div>
<div class="col-4">Comisión</div>
</div>
{% for z_id, datos in prod.precios.items() %}
<div class="row mb-2">
<div class="col-4 d-flex align-items-center">
<span class="badge bg-info text-dark w-100">{{ datos.zona_name }}</span>
</div>
<div class="col-4">
<div class="input-group input-group-sm">
<span class="input-group-text">$</span>
<input type="text" class="form-control money-input" name="price_{{ z_id }}" value="{{ datos.price }}">
</div>
</div>
<div class="col-4">
<div class="input-group input-group-sm">
<span class="input-group-text">$</span>
<input type="text" class="form-control money-input" name="comm_{{ z_id }}" value="{{ datos.commission }}">
</div>
</div>
</div>
{% endfor %}
</div>
<div class="modal-body bg-body-tertiary border-top pb-3">
<label class="form-label text-warning fw-bold mb-1">
<i class="bi bi-clock-history"></i> Programar Cambio de Precio (Opcional)
</label>
<input type="datetime-local" class="form-control form-control-sm mb-1" name="fecha_activacion">
<small class="text-muted" style="font-size: 0.85em;">Si lo dejas en blanco, el cambio se aplicará inmediatamente.</small>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
<button type="submit" class="btn btn-primary">Guardar Precios</button>
</div>
</form>
{% if prod.futuros %}
<div class="modal-body border-top">
<h6 class="text-info fw-bold mb-3">
<i class="bi bi-calendar-event"></i> Cambios Programados a Futuro
</h6>
<div class="table-responsive">
<table class="table table-sm table-bordered text-center mb-0" style="font-size: 0.85rem;">
<thead class="table-dark">
<tr>
<th>Fecha Programada</th>
<th>Zona</th>
<th>Precio</th>
<th>Comisión</th>
<th>Cancelar</th>
</tr>
</thead>
<tbody>
{% for futuro in prod.futuros %}
<tr>
<td class="align-middle text-nowrap text-warning">{{ futuro.fecha }}</td>
<td class="align-middle fw-bold">{{ futuro.zona_name }}</td>
<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">
<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>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="modal fade" id="chartModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="chartModalTitle">Historial de Precios</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<canvas id="priceChart" width="400" height="200"></canvas>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block scripts %} {% 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> <script>
const editModal = document.getElementById('editProductModal'); const editModal = document.getElementById('editProductModal');
if (editModal) { if (editModal) {
@@ -158,5 +309,19 @@
} }
}); });
}); });
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>
{% endblock %} {% endblock %}

View File

@@ -59,6 +59,34 @@
</div> </div>
</div> </div>
<div class="card mb-4 shadow-sm border-0">
<div class="card-body bg-body-tertiary rounded">
<div class="row g-3">
<div class="col-md-5">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" id="searchWorker" placeholder="Buscar por Nombre o RUT...">
</div>
</div>
<div class="col-md-4">
<select class="form-select" id="filterModule">
<option value="all">Todos los Módulos</option>
{% for mod in modulos %}
<option value="{{ mod[0] }}">{{ mod[2] }} - {{ mod[1] }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<select class="form-select" id="filterType">
<option value="all">Todas las Jornadas</option>
<option value="Full Time">Full Time</option>
<option value="Part Time">Part Time</option>
</select>
</div>
</div>
</div>
</div>
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body p-0"> <div class="card-body p-0">
<table class="table table-striped table-hover mb-0"> <table class="table table-striped table-hover mb-0">
@@ -74,7 +102,7 @@
</thead> </thead>
<tbody> <tbody>
{% for worker in workers %} {% for worker in workers %}
<tr> <tr class="worker-row" data-modulo="{{ worker[5] }}" data-tipo="{{ worker[6] }}">
<td class="align-middle">{{ worker[1] }}</td> <td class="align-middle">{{ worker[1] }}</td>
<td class="align-middle">{{ worker[2] }}</td> <td class="align-middle">{{ worker[2] }}</td>
<td class="align-middle">{{ worker[3] }}</td> <td class="align-middle">{{ worker[3] }}</td>
@@ -207,5 +235,40 @@
document.querySelectorAll('.phone-input, #phoneInput').forEach(inp => { document.querySelectorAll('.phone-input, #phoneInput').forEach(inp => {
inp.addEventListener('input', () => formatPhone(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>
{% endblock %} {% endblock %}

View File

@@ -215,7 +215,7 @@
<dd class="col-sm-7">{{ rendicion[2] }} <dd class="col-sm-7">{{ rendicion[2] }}
{% if session.get('is_admin') %} {% if session.get('is_admin') %}
<span class="badge {% if rendicion[14] %}bg-success{% else %}bg-secondary{% endif %} ms-1" style="font-size: 0.65em;"> <span class="badge {% if rendicion[14] %}bg-success{% else %}bg-secondary{% endif %} ms-1" style="font-size: 0.65em;">
{% if rendicion[14] %}$ Sí{% else %}$ No{% endif %} {% if rendicion[14] %}$ Si Recibe Comision{% else %}$ No Recibe Comision{% endif %}
</span> </span>
{% endif %} {% endif %}
</dd> </dd>
@@ -226,7 +226,7 @@
{{ rendicion[10] }} {{ rendicion[10] }}
{% if session.get('is_admin') %} {% if session.get('is_admin') %}
<span class="badge {% if rendicion[15] %}bg-success{% else %}bg-secondary{% endif %} ms-1" style="font-size: 0.65em;"> <span class="badge {% if rendicion[15] %}bg-success{% else %}bg-secondary{% endif %} ms-1" style="font-size: 0.65em;">
{% if rendicion[15] %}$ Sí{% else %}$ No{% endif %} {% if rendicion[15] %}$ Si Recibe Comision{% else %}$ No Recibe Comision{% endif %}
</span> </span>
{% endif %} {% endif %}
{% else %} {% else %}

View File

@@ -214,6 +214,10 @@
</div> </div>
</div> </div>
<div id="discrepancy_warning" class="alert alert-warning mb-4" style="display: none;">
<i class="bi bi-exclamation-triangle-fill me-2"></i> <span id="discrepancy_text"></span>
</div>
<button type="button" class="btn btn-primary w-100 py-3 mb-5" data-bs-toggle="modal" data-bs-target="#confirmSubmitModal"> <button type="button" class="btn btn-primary w-100 py-3 mb-5" data-bs-toggle="modal" data-bs-target="#confirmSubmitModal">
<i class="bi bi-send-check me-2"></i> Enviar Rendición Diaria <i class="bi bi-send-check me-2"></i> Enviar Rendición Diaria
</button> </button>
@@ -277,6 +281,8 @@
} }
}); });
displayTotalProductos.value = granTotal.toLocaleString('es-CL'); displayTotalProductos.value = granTotal.toLocaleString('es-CL');
checkWarnings();
} }
inputsCantidad.forEach(input => { inputsCantidad.forEach(input => {
@@ -292,6 +298,41 @@
calcularVentaProductos(); 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>
<script> <script>
@@ -361,6 +402,8 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('total_digital').value = totalDigital.toLocaleString('es-CL'); document.getElementById('total_digital').value = totalDigital.toLocaleString('es-CL');
document.getElementById('total_general').value = totalGeneral.toLocaleString('es-CL'); document.getElementById('total_general').value = totalGeneral.toLocaleString('es-CL');
checkWarnings();
} }
inputsVenta.forEach(input => { inputsVenta.forEach(input => {