new file: app.py

new file:   rendiciones.db
	new file:   templates/admin_productos.html
	new file:   templates/admin_rendicion_detail.html
	new file:   templates/admin_rendiciones.html
	new file:   templates/admin_structure.html
	new file:   templates/admin_workers.html
	new file:   templates/base.html
	new file:   templates/edit_producto.html
	new file:   templates/edit_worker.html
	new file:   templates/login.html
	new file:   templates/navbar.html
	new file:   templates/worker_dashboard.html
This commit is contained in:
2026-03-18 21:15:26 -03:00
parent 0a5ed0733c
commit 9692f7dd32
13 changed files with 1511 additions and 0 deletions

619
app.py Normal file
View File

@@ -0,0 +1,619 @@
from flask import Flask, render_template, request, redirect, url_for, flash, session
from werkzeug.security import generate_password_hash, check_password_hash
import sqlite3
import re
import random
import string
from functools import wraps
from datetime import date
app = Flask(__name__)
app.secret_key = "super_secret_dev_key"
DB_NAME = "rendiciones.db"
# --- Database & Helpers ---
def init_db():
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
# 1. Zonas
c.execute('''CREATE TABLE IF NOT EXISTS zonas
(id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL)''')
# 2. Modulos (Belong to Zonas)
c.execute('''CREATE TABLE IF NOT EXISTS modulos
(id INTEGER PRIMARY KEY AUTOINCREMENT,
zona_id INTEGER NOT NULL,
name TEXT NOT NULL,
FOREIGN KEY (zona_id) REFERENCES zonas(id))''')
# 3. Productos (Belong to Zonas to enforce unique pricing/commissions per zone)
c.execute('''CREATE TABLE IF NOT EXISTS productos
(id INTEGER PRIMARY KEY AUTOINCREMENT,
zona_id INTEGER NOT NULL,
name TEXT NOT NULL,
price REAL NOT NULL,
commission REAL NOT NULL,
FOREIGN KEY (zona_id) REFERENCES zonas(id))''')
# 4. Workers (Now tied to a Modulo)
# Added modulo_id. It can be NULL for the system admin.
c.execute('''CREATE TABLE IF NOT EXISTS workers
(id INTEGER PRIMARY KEY AUTOINCREMENT,
rut TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
phone TEXT NOT NULL,
password_hash TEXT NOT NULL,
is_admin BOOLEAN DEFAULT 0,
modulo_id INTEGER,
FOREIGN KEY (modulo_id) REFERENCES modulos(id))''')
# 5. Rendiciones (The main form headers)
c.execute('''CREATE TABLE IF NOT EXISTS rendiciones
(id INTEGER PRIMARY KEY AUTOINCREMENT,
worker_id INTEGER NOT NULL,
modulo_id INTEGER NOT NULL,
fecha DATE NOT NULL,
turno TEXT NOT NULL,
venta_tarjeta INTEGER DEFAULT 0,
venta_mp INTEGER DEFAULT 0,
venta_efectivo INTEGER DEFAULT 0,
gastos INTEGER DEFAULT 0,
observaciones TEXT,
FOREIGN KEY (worker_id) REFERENCES workers(id),
FOREIGN KEY (modulo_id) REFERENCES modulos(id))''')
# 6. Rendicion Items (The individual product quantities sold)
c.execute('''CREATE TABLE IF NOT EXISTS rendicion_items
(id INTEGER PRIMARY KEY AUTOINCREMENT,
rendicion_id INTEGER NOT NULL,
producto_id INTEGER NOT NULL,
cantidad INTEGER NOT NULL,
precio_historico INTEGER NOT NULL,
comision_historica INTEGER NOT NULL,
FOREIGN KEY (rendicion_id) REFERENCES rendiciones(id),
FOREIGN KEY (producto_id) REFERENCES productos(id))''')
# Ensure default admin exists
c.execute("SELECT id FROM workers WHERE is_admin = 1")
if not c.fetchone():
admin_pass = generate_password_hash("admin123")
c.execute("INSERT INTO workers (rut, name, phone, password_hash, is_admin) VALUES (?, ?, ?, ?, ?)",
("1-9", "System Admin", "+56 9 0000 0000", admin_pass, 1))
conn.commit()
conn.close()
def generate_random_password(length=6):
"""Generates a simple alphanumeric password."""
return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
def validate_rut(rut):
rut_clean = re.sub(r'[^0-9kK]', '', rut).upper()
if len(rut_clean) < 2: return False
body, dv = rut_clean[:-1], rut_clean[-1]
try:
body_reversed = reversed(body)
total = sum(int(digit) * factor for digit, factor in zip(body_reversed, [2, 3, 4, 5, 6, 7, 2, 3, 4, 5, 6, 7]))
calc_dv = 11 - (total % 11)
if calc_dv == 11: calc_dv = '0'
elif calc_dv == 10: calc_dv = 'K'
else: calc_dv = str(calc_dv)
return calc_dv == dv
except ValueError:
return False
def format_rut(rut):
rut_clean = re.sub(r'[^0-9kK]', '', rut).upper()
body, dv = rut_clean[:-1], rut_clean[-1]
body_fmt = f"{int(body):,}".replace(',', '.')
return f"{body_fmt}-{dv}"
def validate_phone(phone):
phone_clean = re.sub(r'\D', '', phone)
if phone_clean.startswith('56'): phone_clean = phone_clean[2:]
return len(phone_clean) == 9
def format_phone(phone):
phone_clean = re.sub(r'\D', '', phone)
if phone_clean.startswith('56'): phone_clean = phone_clean[2:]
return f"+56 {phone_clean[-9]} {phone_clean[-8:-4]} {phone_clean[-4:]}"
# --- Decorators ---
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated_function
def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session or not session.get('is_admin'):
flash("Acceso denegado. Se requieren permisos de administrador.", "danger")
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated_function
# --- Routes ---
@app.route('/', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
raw_rut = request.form['rut']
password = request.form['password']
rut = format_rut(raw_rut) if validate_rut(raw_rut) else raw_rut
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT id, password_hash, is_admin FROM workers WHERE rut = ?", (rut,))
user = c.fetchone()
conn.close()
if user and check_password_hash(user[1], password):
session['user_id'] = user[0]
session['is_admin'] = user[2]
session['rut'] = rut # Stores RUT for the navbar
if user[2]:
# This line changed: Redirects to the rendiciones list instead of workers
return redirect(url_for('admin_rendiciones'))
else:
return redirect(url_for('worker_dashboard'))
else:
flash("RUT o contraseña incorrectos.", "danger")
return render_template('login.html')
@app.route('/logout')
def logout():
session.clear()
return redirect(url_for('login'))
@app.route('/dashboard', methods=['GET', 'POST'])
@login_required
def worker_dashboard():
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
# 1. Identify the worker and their assigned location
c.execute('''SELECT w.modulo_id, m.name, z.id, z.name
FROM workers w
JOIN modulos m ON w.modulo_id = m.id
JOIN zonas z ON m.zona_id = z.id
WHERE w.id = ?''', (session['user_id'],))
worker_info = c.fetchone()
if not worker_info:
conn.close()
return "Error: No tienes un módulo asignado. Contacta al administrador."
modulo_id, modulo_name, zona_id, zona_name = worker_info
# 2. Handle form submission
if request.method == 'POST':
fecha = request.form.get('fecha')
turno = request.form.get('turno')
# Clean the money inputs (strip dots)
def clean_money(val):
return int(val.replace('.', '')) if val else 0
tarjeta = clean_money(request.form.get('venta_tarjeta'))
mp = clean_money(request.form.get('venta_mp'))
efectivo = clean_money(request.form.get('venta_efectivo'))
gastos = clean_money(request.form.get('gastos'))
obs = request.form.get('observaciones', '').strip()
# Insert Header
c.execute('''INSERT INTO rendiciones
(worker_id, modulo_id, fecha, turno, venta_tarjeta, venta_mp, venta_efectivo, gastos, observaciones)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)''',
(session['user_id'], modulo_id, fecha, turno, tarjeta, mp, efectivo, gastos, obs))
rendicion_id = c.lastrowid
# Insert Products (Only those with a quantity > 0)
for key, value in request.form.items():
if key.startswith('qty_') and value and int(value) > 0:
prod_id = int(key.split('_')[1])
cantidad = int(value)
# Fetch current price/commission to snapshot it
c.execute("SELECT price, commission FROM productos WHERE id = ?", (prod_id,))
prod_data = c.fetchone()
if prod_data:
c.execute('''INSERT INTO rendicion_items
(rendicion_id, producto_id, cantidad, precio_historico, comision_historica)
VALUES (?, ?, ?, ?, ?)''',
(rendicion_id, prod_id, cantidad, prod_data[0], prod_data[1]))
conn.commit()
flash("Rendición enviada exitosamente.", "success")
return redirect(url_for('worker_dashboard'))
# 3. Load Products for the GET request
c.execute("SELECT id, name, price, commission FROM productos WHERE zona_id = ? ORDER BY name", (zona_id,))
productos = c.fetchall()
conn.close()
# Determine if this module uses commissions (Peppermint) or not (Candy)
has_commission = any(prod[3] > 0 for prod in productos)
return render_template('worker_dashboard.html',
modulo_name=modulo_name,
zona_name=zona_name,
productos=productos,
has_commission=has_commission,
today=date.today().strftime('%Y-%m-%d'))
@app.route('/admin/workers', methods=['GET', 'POST'])
@admin_required
def manage_workers():
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
form_data = {}
if request.method == 'POST':
raw_rut = request.form['rut']
raw_phone = request.form['phone']
name = request.form['name'].strip()
modulo_id = request.form.get('modulo_id')
form_data = request.form
if not validate_rut(raw_rut):
flash("El RUT ingresado no es válido.", "danger")
elif not validate_phone(raw_phone):
flash("El teléfono debe tener 9 dígitos válidos.", "danger")
elif not modulo_id:
flash("Debes asignar un módulo al trabajador.", "danger")
else:
rut = format_rut(raw_rut)
phone = format_phone(raw_phone)
password = generate_random_password()
p_hash = generate_password_hash(password)
try:
# Now inserting modulo_id
c.execute("INSERT INTO workers (rut, name, phone, password_hash, is_admin, modulo_id) VALUES (?, ?, ?, ?, 0, ?)",
(rut, name, phone, p_hash, modulo_id))
conn.commit()
flash(f"Trabajador guardado. Contraseña temporal: <strong>{password}</strong>", "success")
return redirect(url_for('manage_workers'))
except sqlite3.IntegrityError:
flash("El RUT ya existe en el sistema.", "danger")
# Fetch workers and JOIN their module name
c.execute('''SELECT w.id, w.rut, w.name, w.phone, m.name
FROM workers w
LEFT JOIN modulos m ON w.modulo_id = m.id
WHERE w.is_admin = 0''')
workers = c.fetchall()
# Fetch modules and their zones for the dropdown
c.execute('''SELECT m.id, m.name, z.name
FROM modulos m
JOIN zonas z ON m.zona_id = z.id
ORDER BY z.name, m.name''')
modulos = c.fetchall()
conn.close()
return render_template('admin_workers.html', workers=workers, form=form_data, modulos=modulos)
@app.route('/admin/workers/edit/<int:id>', methods=['GET', 'POST'])
@admin_required
def edit_worker(id):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
if request.method == 'POST':
raw_phone = request.form['phone']
name = request.form['name'].strip()
modulo_id = request.form.get('modulo_id')
if not validate_phone(raw_phone):
flash("El teléfono debe tener 9 dígitos válidos.", "danger")
return redirect(url_for('edit_worker', id=id))
elif not modulo_id:
flash("Debes seleccionar un módulo.", "danger")
return redirect(url_for('edit_worker', id=id))
c.execute("UPDATE workers SET name=?, phone=?, modulo_id=? WHERE id=?",
(name, format_phone(raw_phone), modulo_id, id))
conn.commit()
flash("Trabajador actualizado exitosamente.", "success")
conn.close()
return redirect(url_for('manage_workers'))
# Added modulo_id to SELECT
c.execute("SELECT id, rut, name, phone, modulo_id FROM workers WHERE id=?", (id,))
worker = c.fetchone()
c.execute('''SELECT m.id, m.name, z.name
FROM modulos m
JOIN zonas z ON m.zona_id = z.id
ORDER BY z.name, m.name''')
modulos = c.fetchall()
conn.close()
if not worker: return redirect(url_for('manage_workers'))
return render_template('edit_worker.html', worker=worker, modulos=modulos)
@app.route('/admin/workers/delete/<int:id>', methods=['POST'])
@admin_required
def delete_worker(id):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("DELETE FROM workers WHERE id=?", (id,))
conn.commit()
conn.close()
flash("Trabajador eliminado.", "info")
return redirect(url_for('manage_workers'))
@app.route('/admin/workers/reset_password/<int:id>', methods=['POST'])
@admin_required
def admin_reset_password(id):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT name FROM workers WHERE id=?", (id,))
worker = c.fetchone()
if not worker:
conn.close()
return redirect(url_for('manage_workers'))
new_password = generate_random_password()
p_hash = generate_password_hash(new_password)
c.execute("UPDATE workers SET password_hash=? WHERE id=?", (p_hash, id))
conn.commit()
conn.close()
flash(f"Contraseña de {worker[0]} restablecida. La nueva contraseña es: <strong>{new_password}</strong>", "warning")
return redirect(url_for('edit_worker', id=id))
@app.route('/admin/estructura', methods=['GET', 'POST'])
@admin_required
def manage_structure():
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
if request.method == 'POST':
action = request.form.get('action')
if action == 'add_zona':
name = request.form.get('zona_name').strip()
try:
c.execute("INSERT INTO zonas (name) VALUES (?)", (name,))
conn.commit()
flash("Zona guardada exitosamente.", "success")
except sqlite3.IntegrityError:
flash("Ese nombre de Zona ya existe.", "danger")
elif action == 'add_modulo':
name = request.form.get('modulo_name').strip()
zona_id = request.form.get('zona_id')
if not zona_id:
flash("Debes seleccionar una Zona válida.", "danger")
else:
c.execute("INSERT INTO modulos (zona_id, name) VALUES (?, ?)", (zona_id, name))
conn.commit()
flash("Módulo guardado exitosamente.", "success")
return redirect(url_for('manage_structure'))
# Fetch Zonas
c.execute("SELECT id, name FROM zonas ORDER BY name")
zonas = c.fetchall()
# Fetch Modulos with their parent Zona name
c.execute('''SELECT m.id, m.name, z.name
FROM modulos m
JOIN zonas z ON m.zona_id = z.id
ORDER BY z.name, m.name''')
modulos = c.fetchall()
conn.close()
return render_template('admin_structure.html', zonas=zonas, modulos=modulos)
@app.route('/admin/estructura/delete/<string:type>/<int:id>', methods=['POST'])
@admin_required
def delete_structure(type, id):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
try:
if type == 'zona':
# SQLite doesn't enforce foreign keys by default unless PRAGMA foreign_keys = ON
# But we should manually prevent orphan modules just in case.
c.execute("SELECT id FROM modulos WHERE zona_id=?", (id,))
if c.fetchone():
flash("No puedes eliminar una Zona que tiene Módulos asignados.", "danger")
else:
c.execute("DELETE FROM zonas WHERE id=?", (id,))
flash("Zona eliminada.", "info")
elif type == 'modulo':
c.execute("SELECT id FROM workers WHERE modulo_id=?", (id,))
if c.fetchone():
flash("No puedes eliminar un Módulo que tiene Trabajadores asignados.", "danger")
else:
c.execute("DELETE FROM modulos WHERE id=?", (id,))
flash("Módulo eliminado.", "info")
conn.commit()
except Exception as e:
flash("Error al eliminar el registro.", "danger")
finally:
conn.close()
return redirect(url_for('manage_structure'))
@app.route('/admin/productos', methods=['GET', 'POST'])
@admin_required
def manage_products():
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
if request.method == 'POST':
name = request.form.get('name').strip()
zona_id = request.form.get('zona_id')
# Strip the formatting dots before trying to convert to math
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:
# Force strictly whole numbers
price = int(raw_price)
commission = int(raw_commission)
c.execute("INSERT INTO productos (zona_id, name, price, commission) VALUES (?, ?, ?, ?)",
(zona_id, name, price, commission))
conn.commit()
flash("Producto guardado exitosamente.", "success")
except ValueError:
flash("El precio y la comisión deben ser números enteros válidos.", "danger")
return redirect(url_for('manage_products'))
c.execute("SELECT id, name FROM zonas ORDER BY name")
zonas = c.fetchall()
c.execute('''SELECT p.id, p.name, p.price, p.commission, z.name
FROM productos p
JOIN zonas z ON p.zona_id = z.id
ORDER BY z.name, p.name''')
productos = c.fetchall()
conn.close()
return render_template('admin_productos.html', zonas=zonas, productos=productos)
@app.route('/admin/productos/edit/<int:id>', methods=['GET', 'POST'])
@admin_required
def edit_product(id):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
if request.method == 'POST':
name = request.form.get('name').strip()
zona_id = request.form.get('zona_id')
# Strip formatting dots
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")
# GET request - fetch data for the form
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'])
@admin_required
def delete_product(id):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("DELETE FROM productos WHERE id=?", (id,))
conn.commit()
conn.close()
flash("Producto eliminado.", "info")
return redirect(url_for('manage_products'))
@app.route('/admin/rendiciones')
@admin_required
def admin_rendiciones():
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
# Fetch all rendiciones, newest first
c.execute('''
SELECT r.id, r.fecha, w.name, m.name, r.turno,
(r.venta_tarjeta + r.venta_mp + r.venta_efectivo) as total_declarado,
r.gastos
FROM rendiciones r
JOIN workers w ON r.worker_id = w.id
JOIN modulos m ON r.modulo_id = m.id
ORDER BY r.fecha DESC, r.id DESC
''')
rendiciones = c.fetchall()
conn.close()
return render_template('admin_rendiciones.html', rendiciones=rendiciones)
@app.route('/admin/rendiciones/<int:id>')
@admin_required
def view_rendicion(id):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
# Get Header Info
c.execute('''
SELECT r.id, r.fecha, w.name, m.name, r.turno,
r.venta_tarjeta, r.venta_mp, r.venta_efectivo, r.gastos, r.observaciones
FROM rendiciones r
JOIN workers w ON r.worker_id = w.id
JOIN modulos m ON r.modulo_id = m.id
WHERE r.id = ?
''', (id,))
rendicion = c.fetchone()
if not rendicion:
conn.close()
flash("Rendición no encontrada.", "danger")
return redirect(url_for('admin_rendiciones'))
# Get Line Items
c.execute('''
SELECT p.name, ri.cantidad, ri.precio_historico, ri.comision_historica,
(ri.cantidad * ri.precio_historico) as total_linea,
(ri.cantidad * ri.comision_historica) as total_comision
FROM rendicion_items ri
JOIN productos p ON ri.producto_id = p.id
WHERE ri.rendicion_id = ?
''', (id,))
items = c.fetchall()
conn.close()
# Calculate the mathematical truth vs what they declared
total_calculado = sum(item[4] for item in items)
comision_total = sum(item[5] for item in items)
return render_template('admin_rendicion_detail.html',
rendicion=rendicion,
items=items,
total_calculado=total_calculado,
comision_total=comision_total)
if __name__ == '__main__':
init_db()
app.run(host='0.0.0.0', port=5000, debug=True)

BIN
rendiciones.db Normal file

Binary file not shown.

View File

@@ -0,0 +1,105 @@
{% extends "base.html" %}
{% 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 %}
<div class="card mb-4 shadow-sm">
<div class="card-header bg-secondary text-white">Agregar Nuevo Producto</div>
<div class="card-body">
<form method="POST" action="{{ url_for('manage_products') }}">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Zona</label>
<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 class="col-md-2">
<label class="form-label">Precio de Venta</label>
<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>
</form>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body p-0">
<table class="table table-striped table-hover mb-0">
<thead class="table-dark">
<tr>
<th>Zona</th>
<th>Producto</th>
<th>Precio</th>
<th>Comisión</th>
<th class="text-end">Acciones</th>
</tr>
</thead>
<tbody>
{% for prod in productos %}
<tr>
<td class="align-middle"><span class="badge bg-info text-dark">{{ prod[4] }}</span></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">
<a href="{{ url_for('edit_product', id=prod[0]) }}" class="btn btn-sm btn-outline-info">Editar</a>
<form action="{{ url_for('delete_product', id=prod[0]) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('¿Eliminar este producto?');">Eliminar</button>
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center py-3 text-muted">No hay productos registrados.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<script>
// Automatically format money inputs with standard Chilean dots
document.querySelectorAll('.money-input').forEach(function(input) {
input.addEventListener('input', function(e) {
// Strip everything that isn't a number
let value = this.value.replace(/\D/g, '');
if (value !== '') {
// Parse and format back to a string with dots
this.value = parseInt(value, 10).toLocaleString('es-CL');
} else {
this.value = '';
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,116 @@
{% extends "base.html" %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Detalle de Rendición #{{ rendicion[0] }}</h2>
<a href="{{ url_for('admin_rendiciones') }}" class="btn btn-outline-light">Volver al Historial</a>
</div>
<div class="row">
<div class="col-md-8 mb-4">
<div class="card shadow-sm h-100">
<div class="card-header bg-secondary text-white">Productos Vendidos</div>
<div class="card-body p-0">
<table class="table table-striped mb-0">
<thead class="table-dark">
<tr>
<th>Producto</th>
<th class="text-center">Cant.</th>
<th class="text-end">Precio Un.</th>
<th class="text-end">Total Línea</th>
<th class="text-end">Comisión</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item[0] }}</td>
<td class="text-center fw-bold">{{ item[1] }}</td>
<td class="text-end text-muted">${{ "{:,.0f}".format(item[2]).replace(',', '.') }}</td>
<td class="text-end fw-bold">${{ "{:,.0f}".format(item[4]).replace(',', '.') }}</td>
<td class="text-end text-success">${{ "{:,.0f}".format(item[5]).replace(',', '.') }}</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center py-3">No se registraron productos en esta rendición.</td>
</tr>
{% endfor %}
</tbody>
<tfoot class="table-group-divider">
<tr>
<td colspan="3" class="text-end fw-bold">Total Calculado por Sistema:</td>
<td class="text-end fw-bold fs-5">${{ "{:,.0f}".format(total_calculado).replace(',', '.') }}</td>
<td class="text-end fw-bold text-success">${{ "{:,.0f}".format(comision_total).replace(',', '.') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card shadow-sm mb-4 border-info">
<div class="card-header bg-info text-dark fw-bold">Declaración del Trabajador</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-5 text-muted">Fecha</dt>
<dd class="col-sm-7">{{ rendicion[1] }}</dd>
<dt class="col-sm-5 text-muted">Trabajador</dt>
<dd class="col-sm-7">{{ rendicion[2] }}</dd>
<dt class="col-sm-5 text-muted">Módulo</dt>
<dd class="col-sm-7"><span class="badge bg-secondary">{{ rendicion[3] }}</span></dd>
<dt class="col-sm-5 text-muted">Turno</dt>
<dd class="col-sm-7">{{ rendicion[4] }}</dd>
</dl>
<hr>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Tarjetas:</span>
<span>${{ "{:,.0f}".format(rendicion[5]).replace(',', '.') }}</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Mercado Pago:</span>
<span>${{ "{:,.0f}".format(rendicion[6]).replace(',', '.') }}</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Efectivo:</span>
<span>${{ "{:,.0f}".format(rendicion[7]).replace(',', '.') }}</span>
</div>
{% set total_declarado = rendicion[5] + rendicion[6] + rendicion[7] %}
<div class="d-flex justify-content-between mt-3 pt-2 border-top">
<strong class="fs-5">Total Declarado:</strong>
<strong class="fs-5">${{ "{:,.0f}".format(total_declarado).replace(',', '.') }}</strong>
</div>
{% if total_declarado != total_calculado %}
<div class="alert alert-warning mt-3 mb-0 py-2">
<small>⚠️ Discrepancia: El total declarado (${{ "{:,.0f}".format(total_declarado).replace(',', '.') }}) no coincide con la suma de los productos vendidos (${{ "{:,.0f}".format(total_calculado).replace(',', '.') }}).</small>
</div>
{% endif %}
</div>
</div>
<div class="card shadow-sm border-danger">
<div class="card-header bg-danger text-white">Gastos y Observaciones</div>
<div class="card-body">
<div class="d-flex justify-content-between mb-3">
<strong class="text-danger">Monto Gastos:</strong>
<strong class="text-danger">-${{ "{:,.0f}".format(rendicion[8]).replace(',', '.') }}</strong>
</div>
<div>
<span class="text-muted d-block mb-1">Observaciones:</span>
<p class="mb-0 bg-dark p-2 rounded border border-secondary" style="font-size: 0.9em;">
{{ rendicion[9] if rendicion[9] else "Sin observaciones." }}
</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block content %}
<h2 class="mb-4">Historial de Rendiciones</h2>
<div class="card shadow-sm">
<div class="card-body p-0">
<table class="table table-striped table-hover mb-0">
<thead class="table-dark">
<tr>
<th>Fecha</th>
<th>Trabajador</th>
<th>Módulo</th>
<th>Turno</th>
<th>Total Declarado</th>
<th>Gastos</th>
<th class="text-end">Acciones</th>
</tr>
</thead>
<tbody>
{% for r in rendiciones %}
<tr>
<td class="align-middle">{{ r[1] }}</td>
<td class="align-middle">{{ r[2] }}</td>
<td class="align-middle"><span class="badge bg-info text-dark">{{ r[3] }}</span></td>
<td class="align-middle">{{ r[4] }}</td>
<td class="align-middle">${{ "{:,.0f}".format(r[5]).replace(',', '.') }}</td>
<td class="align-middle text-danger">${{ "{:,.0f}".format(r[6]).replace(',', '.') }}</td>
<td class="text-end">
<a href="{{ url_for('view_rendicion', id=r[0]) }}" class="btn btn-sm btn-primary">Ver Detalle</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="7" class="text-center py-4 text-muted">Aún no hay rendiciones enviadas.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,112 @@
{% extends "base.html" %}
{% 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 %}
<div class="row">
<div class="col-md-6 mb-4">
<div class="card shadow-sm h-100">
<div class="card-header bg-secondary text-white">
<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">
<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>
<button class="btn btn-primary" type="submit">Agregar Zona</button>
</div>
</form>
<table class="table table-sm table-hover">
<thead class="table-dark">
<tr>
<th>Nombre</th>
<th class="text-end">Acciones</th>
</tr>
</thead>
<tbody>
{% for zona in zonas %}
<tr>
<td class="align-middle">{{ zona[1] }}</td>
<td class="text-end">
<form action="{{ url_for('delete_structure', type='zona', id=zona[0]) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('¿Eliminar esta Zona?');">Eliminar</button>
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="2" class="text-center text-muted">No hay zonas registradas.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card shadow-sm h-100">
<div class="card-header bg-secondary text-white">
<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">
<input type="hidden" name="action" value="add_modulo">
<div class="row g-2">
<div class="col-md-5">
<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-5">
<input type="text" class="form-control" name="modulo_name" placeholder="Nombre del Módulo" required>
</div>
<div class="col-md-2">
<button class="btn btn-primary w-100" type="submit">+</button>
</div>
</div>
</form>
<table class="table table-sm table-hover">
<thead class="table-dark">
<tr>
<th>Módulo</th>
<th>Zona</th>
<th class="text-end">Acciones</th>
</tr>
</thead>
<tbody>
{% for modulo in modulos %}
<tr>
<td class="align-middle">{{ modulo[1] }}</td>
<td class="align-middle"><span class="badge bg-info text-dark">{{ modulo[2] }}</span></td>
<td class="text-end">
<form action="{{ url_for('delete_structure', type='modulo', id=modulo[0]) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('¿Eliminar este Módulo?');">Eliminar</button>
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="3" class="text-center text-muted">No hay módulos registrados.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,108 @@
{% extends "base.html" %}
{% 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 %}
<div class="card mb-4">
<div class="card-header bg-secondary text-white">Agregar Nuevo Trabajador</div>
<div class="card-body">
<form method="POST" action="{{ url_for('manage_workers') }}">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">RUT</label>
<input type="text" class="form-control" name="rut" id="rutInput" placeholder="12.345.678-9" value="{{ form.get('rut', '') }}" required>
</div>
<div class="col-md-3">
<label class="form-label">Nombre Completo</label>
<input type="text" class="form-control" name="name" value="{{ form.get('name', '') }}" required>
</div>
<div class="col-md-3">
<label class="form-label">Teléfono</label>
<input type="text" class="form-control" name="phone" id="phoneInput" placeholder="9 1234 5678" value="{{ form.get('phone', '') }}" required>
</div>
<div class="col-md-3">
<label class="form-label">Módulo Asignado</label>
<select class="form-select" name="modulo_id" required>
<option value="" selected disabled>Seleccionar Módulo...</option>
{% for mod in modulos %}
<option value="{{ mod[0] }}">{{ mod[2] }} - {{ mod[1] }}</option>
{% endfor %}
</select>
</div>
</div>
<button type="submit" class="btn btn-primary mt-3">Guardar Trabajador</button>
</form>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body p-0">
<table class="table table-striped table-hover mb-0">
<thead class="table-dark">
<tr>
<th>RUT</th>
<th>Nombre</th>
<th>Teléfono</th>
<th>Módulo</th>
<th class="text-end">Acciones</th>
</tr>
</thead>
<tbody>
{% for worker in workers %}
<tr>
<td class="align-middle">{{ worker[1] }}</td>
<td class="align-middle">{{ worker[2] }}</td>
<td class="align-middle">{{ worker[3] }}</td>
<td class="align-middle"><span class="badge bg-info text-dark">{{ worker[4] }}</span></td>
<td class="text-end">
<a href="{{ url_for('edit_worker', id=worker[0]) }}" class="btn btn-sm btn-outline-info">Editar</a>
<form action="{{ url_for('delete_worker', id=worker[0]) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('¿Eliminar a este trabajador?');">Eliminar</button>
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center py-3 text-muted">No hay trabajadores registrados.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<script>
document.getElementById('rutInput').addEventListener('input', function(e) {
let value = this.value.replace(/[^0-9kK]/g, '').toUpperCase();
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;
}
});
document.getElementById('phoneInput').addEventListener('input', function(e) {
let value = this.value.replace(/\D/g, '');
if (value.startsWith('56')) value = value.substring(2);
value = value.substring(0, 9);
if (value.length > 5) {
this.value = value.replace(/(\d{1})(\d{4})(\d+)/, '$1 $2 $3');
} else if (value.length > 1) {
this.value = value.replace(/(\d{1})(\d+)/, '$1 $2');
} else {
this.value = value;
}
});
</script>
{% endblock %}

19
templates/base.html Normal file
View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="es" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sistema de Rendiciones</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
{% include 'navbar.html' %}
<div class="container">
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,78 @@
{% extends "base.html" %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<h2 class="mb-4">Editar Producto</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 %}
<div class="card shadow-sm">
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label class="form-label">Zona</label>
<select class="form-select" name="zona_id" required>
<option value="" disabled>Seleccionar Zona...</option>
{% for zona in zonas %}
<option value="{{ zona[0] }}" {% if zona[0] == producto[1] %}selected{% endif %}>
{{ zona[1] }}
</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Nombre del Producto</label>
<input type="text" class="form-control" name="name" value="{{ producto[2] }}" required>
</div>
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label">Precio de Venta</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="text" class="form-control money-input" name="price" value="{{ producto[3]|int }}" required>
</div>
</div>
<div class="col-md-6">
<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" value="{{ producto[4]|int }}" required>
</div>
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('manage_products') }}" class="btn btn-secondary">Cancelar</a>
<button type="submit" class="btn btn-primary">Actualizar Producto</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
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 = '';
}
});
});
// Artificially trigger the input event on load to format the raw DB numbers
window.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.money-input').forEach(input => {
input.dispatchEvent(new Event('input'));
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,88 @@
{% extends "base.html" %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<h2 class="mb-4">Editar Trabajador</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 %}
<div class="card mb-4">
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label class="form-label">RUT</label>
<input type="text" class="form-control text-muted" value="{{ worker[1] }}" readonly disabled>
</div>
<div class="mb-3">
<label class="form-label">Nombre Completo</label>
<input type="text" class="form-control" name="name" value="{{ worker[2] }}" required>
</div>
<div class="mb-4">
<label class="form-label">Teléfono</label>
<input type="text" class="form-control" name="phone" id="phoneInput" value="{{ worker[3] }}" required>
</div>
<div class="mb-4">
<label class="form-label">Módulo Asignado</label>
<select class="form-select" name="modulo_id" required>
<option value="" disabled>Seleccionar Módulo...</option>
{% for mod in modulos %}
<option value="{{ mod[0] }}" {% if mod[0] == worker[4] %}selected{% endif %}>
{{ mod[2] }} - {{ mod[1] }}
</option>
{% endfor %}
</select>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('manage_workers') }}" class="btn btn-secondary">Cancelar</a>
<button type="submit" class="btn btn-primary">Actualizar Datos</button>
</div>
</form>
</div>
</div>
<div class="card border-warning">
<div class="card-header bg-warning text-dark border-warning">
<strong>Opciones de Seguridad</strong>
</div>
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1">Restablecer Contraseña</h6>
<small class="text-muted">Genera una nueva contraseña aleatoria si el trabajador olvidó la suya.</small>
</div>
<form action="{{ url_for('admin_reset_password', id=worker[0]) }}" method="POST">
<button type="submit" class="btn btn-outline-warning" onclick="return confirm('¿Generar nueva contraseña? La anterior dejará de funcionar inmediatamente.');">
Generar Nueva
</button>
</form>
</div>
</div>
</div>
</div>
<script>
document.getElementById('phoneInput').addEventListener('input', function(e) {
let value = this.value.replace(/\D/g, '');
if (value.startsWith('56')) value = value.substring(2);
value = value.substring(0, 9);
if (value.length > 5) {
this.value = value.replace(/(\d{1})(\d{4})(\d+)/, '$1 $2 $3');
} else if (value.length > 1) {
this.value = value.replace(/(\d{1})(\d+)/, '$1 $2');
} else {
this.value = value;
}
});
window.addEventListener('DOMContentLoaded', () => {
document.getElementById('phoneInput').dispatchEvent(new Event('input'));
});
</script>
{% endblock %}

47
templates/login.html Normal file
View File

@@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block content %}
<div class="row justify-content-center mt-5">
<div class="col-md-5">
<div class="card shadow">
<div class="card-header bg-primary text-white text-center py-3">
<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 %}
<form method="POST">
<div class="mb-3">
<label class="form-label">RUT</label>
<input type="text" class="form-control" name="rut" id="rutInput" placeholder="12.345.678-9" required autofocus>
</div>
<div class="mb-4">
<label class="form-label">Contraseña</label>
<input type="password" class="form-control" name="password" required>
</div>
<button type="submit" class="btn btn-primary w-100">Ingresar</button>
</form>
</div>
</div>
</div>
</div>
<script>
document.getElementById('rutInput').addEventListener('input', function(e) {
let value = this.value.replace(/[^0-9kK]/g, '').toUpperCase();
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 %}

40
templates/navbar.html Normal file
View File

@@ -0,0 +1,40 @@
<nav class="navbar navbar-expand-lg navbar-dark bg-dark border-bottom mb-4">
<div class="container">
<a class="navbar-brand" href="#">Rendiciones App</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
{% if session.get('user_id') %}
<ul class="navbar-nav me-auto">
{% if session.get('is_admin') %}
<li class="nav-item">
<a class="nav-link {% if 'rendiciones' in request.path %}active{% endif %}" href="{{ url_for('admin_rendiciones') }}">Rendiciones</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'manage_workers' %}active{% endif %}" href="{{ url_for('manage_workers') }}">Trabajadores</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'manage_structure' %}active{% endif %}" href="{{ url_for('manage_structure') }}">Zonas y Módulos</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'manage_products' %}active{% endif %}" href="{{ url_for('manage_products') }}">Productos</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'worker_dashboard' %}active{% endif %}" href="{{ url_for('worker_dashboard') }}">Mis Rendiciones</a>
</li>
{% endif %}
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<span class="nav-link text-muted">RUT: {{ session.get('rut', 'Admin') }}</span>
</li>
<li class="nav-item">
<a class="nav-link text-danger" href="{{ url_for('logout') }}">Cerrar Sesión</a>
</li>
</ul>
{% endif %}
</div>
</div>
</nav>

View File

@@ -0,0 +1,138 @@
{% extends "base.html" %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Rendición de Caja</h2>
<div class="text-end text-muted">
<div><strong>Módulo:</strong> <span class="badge bg-primary">{{ modulo_name }}</span></div>
<div><small>Zona: {{ zona_name }}</small></div>
</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 %}
<form method="POST">
<div class="card mb-4 shadow-sm">
<div class="card-header bg-secondary text-white">Datos del Turno</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Fecha de Venta</label>
<input type="date" class="form-control" name="fecha" value="{{ today }}" required>
</div>
<div class="col-md-6">
<label class="form-label">Turno</label>
<select class="form-select" name="turno" required>
<option value="" disabled selected>Seleccionar...</option>
<option value="Primer Turno">Primer Turno</option>
<option value="Segundo Turno">Segundo Turno</option>
<option value="Part Time">Part Time</option>
</select>
</div>
</div>
</div>
</div>
<div class="card mb-4 shadow-sm">
<div class="card-header bg-secondary text-white">Detalle de Productos Vendidos</div>
<div class="card-body p-0">
<table class="table table-striped table-hover mb-0">
<thead class="table-dark">
<tr>
<th>Producto / Rango de Precio</th>
<th>Precio</th>
{% if has_commission %}
<th>Comisión</th>
{% endif %}
<th style="width: 150px;">Cantidad</th>
</tr>
</thead>
<tbody>
{% for prod in productos %}
<tr>
<td class="align-middle">{{ prod[1] }}</td>
<td class="align-middle text-muted">${{ "{:,.0f}".format(prod[2]).replace(',', '.') }}</td>
{% if has_commission %}
<td class="align-middle text-muted">${{ "{:,.0f}".format(prod[3]).replace(',', '.') }}</td>
{% endif %}
<td>
<input type="number" class="form-control form-control-sm" name="qty_{{ prod[0] }}" min="0" placeholder="0">
</td>
</tr>
{% else %}
<tr>
<td colspan="{% if has_commission %}4{% else %}3{% endif %}" class="text-center py-4">
No hay productos configurados para esta zona.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card mb-4 shadow-sm border-info">
<div class="card-header bg-info text-dark">Resumen Financiero</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Total Tarjetas (Crédito/Débito)</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="text" class="form-control money-input" name="venta_tarjeta" placeholder="0">
</div>
</div>
<div class="col-md-3">
<label class="form-label">Total Mercado Pago</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="text" class="form-control money-input" name="venta_mp" placeholder="0">
</div>
</div>
<div class="col-md-3">
<label class="form-label">Total Efectivo</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="text" class="form-control money-input" name="venta_efectivo" placeholder="0">
</div>
</div>
<div class="col-md-3">
<label class="form-label">Gastos del Módulo</label>
<div class="input-group">
<span class="input-group-text text-danger">-$</span>
<input type="text" class="form-control money-input" name="gastos" placeholder="0">
</div>
</div>
</div>
<div class="mt-3">
<label class="form-label">Observaciones / Concepto de Gastos</label>
<textarea class="form-control" name="observaciones" rows="2" placeholder="Si hubo gastos, anota el motivo aquí..."></textarea>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary w-100 py-3 mb-5" onclick="return confirm('¿Enviar rendición? Revisa bien las cantidades.');">Enviar Rendición Diaria</button>
</form>
<script>
// Reuse our formatting script for the summary money inputs
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 = '';
}
});
});
</script>
{% endblock %}