Implemented sales dashboard
This commit is contained in:
@@ -84,6 +84,7 @@ python app.py
|
||||
|
||||
## 📋 TODOs?
|
||||
- Some form of user registration(?)
|
||||
- Major refactoring of the codebase
|
||||
|
||||
## 🥼 Food Datasets
|
||||
- https://www.ifpsglobal.com/plu-codes-search
|
||||
|
||||
91
app.py
91
app.py
@@ -47,6 +47,23 @@ def init_db():
|
||||
stock REAL DEFAULT 0,
|
||||
unit_type TEXT DEFAULT 'unit')''')
|
||||
|
||||
# Add these two tables for sales history
|
||||
conn.execute('''CREATE TABLE IF NOT EXISTS sales
|
||||
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
date TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
total REAL,
|
||||
payment_method TEXT)''')
|
||||
|
||||
conn.execute('''CREATE TABLE IF NOT EXISTS sale_items
|
||||
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sale_id INTEGER,
|
||||
barcode TEXT,
|
||||
name TEXT,
|
||||
price REAL,
|
||||
quantity REAL,
|
||||
subtotal REAL,
|
||||
FOREIGN KEY(sale_id) REFERENCES sales(id))''')
|
||||
|
||||
# Default user logic remains same...
|
||||
user = conn.execute('SELECT * FROM users WHERE username = ?', ('admin',)).fetchone()
|
||||
if not user:
|
||||
@@ -326,6 +343,80 @@ def update_scale_weight():
|
||||
|
||||
return jsonify({"status": "received"}), 200
|
||||
|
||||
|
||||
@app.route('/api/checkout', methods=['POST'])
|
||||
@login_required
|
||||
def process_checkout():
|
||||
data = request.get_json()
|
||||
cart = data.get('cart', [])
|
||||
payment_method = data.get('payment_method', 'efectivo')
|
||||
|
||||
if not cart:
|
||||
return jsonify({"error": "Cart is empty"}), 400
|
||||
|
||||
# Recalculate total on the server because trusting the frontend is a security risk
|
||||
total = sum(item.get('subtotal', 0) for item in cart)
|
||||
|
||||
try:
|
||||
with sqlite3.connect(DB_FILE) as conn:
|
||||
cur = conn.cursor()
|
||||
|
||||
# 1. Create the main sale record
|
||||
cur.execute('INSERT INTO sales (total, payment_method) VALUES (?, ?)', (total, payment_method))
|
||||
sale_id = cur.lastrowid
|
||||
|
||||
# 2. Record each item and deduct stock
|
||||
for item in cart:
|
||||
cur.execute('''INSERT INTO sale_items (sale_id, barcode, name, price, quantity, subtotal)
|
||||
VALUES (?, ?, ?, ?, ?, ?)''',
|
||||
(sale_id, item['barcode'], item['name'], item['price'], item['qty'], item['subtotal']))
|
||||
|
||||
# Deduct from inventory
|
||||
cur.execute('UPDATE products SET stock = stock - ? WHERE barcode = ?', (item['qty'], item['barcode']))
|
||||
|
||||
conn.commit()
|
||||
|
||||
return jsonify({"status": "success", "sale_id": sale_id}), 200
|
||||
|
||||
except Exception as e:
|
||||
print(f"Checkout Error: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/sales')
|
||||
@login_required
|
||||
def sales():
|
||||
selected_date = request.args.get('date')
|
||||
with sqlite3.connect(DB_FILE) as conn:
|
||||
cur = conn.cursor()
|
||||
|
||||
# Determine the target date for the "Daily" stat
|
||||
target_date = selected_date if selected_date else cur.execute("SELECT date('now', 'localtime')").fetchone()[0]
|
||||
|
||||
stats = {
|
||||
"daily": cur.execute("SELECT SUM(total) FROM sales WHERE date(date, 'localtime') = ?", (target_date,)).fetchone()[0] or 0,
|
||||
"week": cur.execute("SELECT SUM(total) FROM sales WHERE date(date, 'localtime') >= date('now', 'localtime', '-7 days')").fetchone()[0] or 0,
|
||||
"month": cur.execute("SELECT SUM(total) FROM sales WHERE strftime('%Y-%m', date, 'localtime') = strftime('%Y-%m', 'now', 'localtime')").fetchone()[0] or 0
|
||||
}
|
||||
|
||||
if selected_date:
|
||||
sales_data = cur.execute('''SELECT id, date, total, payment_method FROM sales
|
||||
WHERE date(date, 'localtime') = ?
|
||||
ORDER BY date DESC''', (selected_date,)).fetchall()
|
||||
else:
|
||||
sales_data = cur.execute('SELECT id, date, total, payment_method FROM sales ORDER BY date DESC LIMIT 100').fetchall()
|
||||
|
||||
return render_template('sales.html', user=current_user, sales=sales_data, stats=stats, selected_date=selected_date)
|
||||
|
||||
@app.route('/api/sale/<int:sale_id>')
|
||||
@login_required
|
||||
def get_sale_details(sale_id):
|
||||
with sqlite3.connect(DB_FILE) as conn:
|
||||
items = conn.execute('SELECT barcode, name, price, quantity, subtotal FROM sale_items WHERE sale_id = ?', (sale_id,)).fetchall()
|
||||
|
||||
# Format it as a neat list of dictionaries for JavaScript to digest
|
||||
item_list = [{"barcode": i[0], "name": i[1], "price": i[2], "qty": i[3], "subtotal": i[4]} for i in items]
|
||||
return jsonify(item_list), 200
|
||||
|
||||
# @app.route('/process_payment', methods=['POST'])
|
||||
# @login_required
|
||||
# def process_payment():
|
||||
|
||||
@@ -241,14 +241,39 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="paymentModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<h5 class="modal-title w-100 text-center text-muted text-uppercase" style="font-size: 0.8rem;">Total a Pagar</h5>
|
||||
<button type="button" class="btn-close position-absolute end-0 top-0 m-3" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center pt-1 pb-4">
|
||||
<h1 id="payment-modal-total" class="mb-4" style="color: var(--accent); font-weight: 800; font-size: 3rem;">$0</h1>
|
||||
|
||||
<div class="d-grid gap-3 px-3">
|
||||
<button class="btn btn-lg btn-success py-3" onclick="executeCheckout('efectivo')">
|
||||
<i class="bi bi-cash-coin me-2" style="font-size: 1.5rem; vertical-align: middle;"></i> Efectivo
|
||||
</button>
|
||||
<button class="btn btn-lg btn-secondary py-3" onclick="executeCheckout('tarjeta')" disabled>
|
||||
<i class="bi bi-credit-card me-2" style="font-size: 1.5rem; vertical-align: middle;"></i> Tarjeta (Pronto)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="navbar navbar-expand-md sticky-top px-3 mb-3">
|
||||
<span class="navbar-brand">SekiPOS <small class="text-muted fw-normal"
|
||||
style="font-size:0.65rem;">Caja</small></span>
|
||||
|
||||
<div class="ms-3">
|
||||
<div class="ms-3 gap-2 d-flex">
|
||||
<a href="/" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-box-seam me-1"></i>Inventario
|
||||
</a>
|
||||
<a href="/sales" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-receipt me-1"></i>Ventas
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="ms-auto">
|
||||
@@ -442,41 +467,53 @@
|
||||
bootstrap.Modal.getInstance(document.getElementById('clearCartModal')).hide();
|
||||
}
|
||||
|
||||
async function processSale() {
|
||||
function processSale() {
|
||||
if (cart.length === 0) return;
|
||||
|
||||
// Calculate total and show the payment modal
|
||||
const total = cart.reduce((sum, item) => sum + item.subtotal, 0);
|
||||
document.getElementById('payment-modal-total').innerText = clp.format(total);
|
||||
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('paymentModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
async function executeCheckout(method) {
|
||||
if (cart.length === 0) return;
|
||||
|
||||
const total = cart.reduce((sum, item) => sum + item.subtotal, 0);
|
||||
|
||||
// Disable button to prevent spam
|
||||
const btn = document.querySelector('button[onclick="processSale()"]');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Procesando...';
|
||||
|
||||
try {
|
||||
alert("total: " + total)
|
||||
// const response = await fetch('/process_payment', {
|
||||
// method: 'POST',
|
||||
// headers: { 'Content-Type': 'application/json' },
|
||||
// body: JSON.stringify({ total: total })
|
||||
// });
|
||||
const response = await fetch('/api/checkout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ cart: cart, payment_method: method })
|
||||
});
|
||||
|
||||
// const result = await response.json();
|
||||
const result = await response.json();
|
||||
|
||||
// if (response.ok) {
|
||||
// const modal = new bootstrap.Modal(document.getElementById('successModal'));
|
||||
// modal.show();
|
||||
// cart = [];
|
||||
// renderCart();
|
||||
// } else {
|
||||
// alert("Error en el pago: " + (result.message || "Error desconocido"));
|
||||
// }
|
||||
if (response.ok) {
|
||||
// Hide the payment modal
|
||||
bootstrap.Modal.getInstance(document.getElementById('paymentModal')).hide();
|
||||
|
||||
// Show the success checkmark
|
||||
const successModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('successModal'));
|
||||
successModal.show();
|
||||
|
||||
// Nuke the cart and auto-save the empty state
|
||||
cart = [];
|
||||
renderCart();
|
||||
|
||||
// Auto-hide the success modal after 2 seconds so you don't have to click it
|
||||
setTimeout(() => successModal.hide(), 2000);
|
||||
|
||||
} else {
|
||||
alert("Error en la venta: " + (result.error || "Error desconocido"));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Error de conexión con el servidor.");
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-cash-coin"></i> COBRAR';
|
||||
}
|
||||
}
|
||||
|
||||
function confirmWeight() {
|
||||
const weightInput = document.getElementById('weight-input');
|
||||
const weightGrams = parseInt(weightInput.value, 10);
|
||||
|
||||
@@ -278,9 +278,12 @@
|
||||
<nav class="navbar navbar-expand-md sticky-top px-3 mb-3">
|
||||
<span class="navbar-brand">SekiPOS <small class="text-muted fw-normal"
|
||||
style="font-size:0.65rem;">v1.6</small></span>
|
||||
<div class="ms-3">
|
||||
<div class="ms-3 gap-2 d-flex">
|
||||
<a href="/checkout" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-cart-fill me-1"></i>Ir a Caja
|
||||
<i class="bi bi-cart-fill me-1"></i>Caja
|
||||
</a>
|
||||
<a href="/sales" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-receipt me-1"></i>Ventas
|
||||
</a>
|
||||
</div>
|
||||
<!-- Always-visible dropdown on the right -->
|
||||
@@ -664,7 +667,7 @@
|
||||
// Force integers here to nuke the decimals once and for all
|
||||
document.getElementById('form-price').value = p ? parseInt(p, 10) : '';
|
||||
document.getElementById('form-stock').value = stock ? parseInt(stock, 10) : 0;
|
||||
|
||||
|
||||
document.getElementById('form-unit-type').value = unit || 'unit';
|
||||
document.getElementById('form-image').value = i || '';
|
||||
document.getElementById('form-title').innerText = t;
|
||||
|
||||
271
templates/sales.html
Normal file
271
templates/sales.html
Normal file
@@ -0,0 +1,271 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="es" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SekiPOS - Ventas</title>
|
||||
<link rel="shortcut icon" href="./static/favicon.png" type="image/x-icon">
|
||||
<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">
|
||||
<style>
|
||||
/* Reusing your standard theme colors */
|
||||
:root {
|
||||
--bg: #ebedef; --card-bg: #ffffff; --text-main: #2e3338;
|
||||
--text-muted: #4f5660; --border: #e3e5e8; --navbar-bg: #ffffff;
|
||||
--accent: #5865f2; --accent-hover: #4752c4; --input-bg: #e3e5e8;
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
--bg: #36393f; --card-bg: #2f3136; --text-main: #dcddde;
|
||||
--text-muted: #b9bbbe; --border: #202225; --navbar-bg: #202225;
|
||||
--input-bg: #202225;
|
||||
}
|
||||
body { background: var(--bg); color: var(--text-main); font-family: "gg sans", sans-serif; min-height: 100vh; }
|
||||
.navbar { background: var(--navbar-bg) !important; border-bottom: 1px solid var(--border); }
|
||||
.navbar-brand, .nav-link, .dropdown-item { color: var(--text-main) !important; }
|
||||
.dropdown-menu { background: var(--card-bg); border: 1px solid var(--border); }
|
||||
.dropdown-item:hover { background: var(--input-bg); }
|
||||
.discord-card, .modal-content { background: var(--card-bg) !important; border: 1px solid var(--border); border-radius: 8px; }
|
||||
|
||||
.navbar-brand {
|
||||
color: var(--text-main) !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* --- Tables --- */
|
||||
.table {
|
||||
--bs-table-bg: transparent;
|
||||
--bs-table-color: var(--text-main); /* This forces Bootstrap to obey */
|
||||
--bs-table-border-color: var(--border);
|
||||
--bs-table-hover-color: var(--text-main);
|
||||
--bs-table-hover-bg: var(--input-bg);
|
||||
color: var(--text-main) !important;
|
||||
}
|
||||
.table thead th {
|
||||
background: var(--bg);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.table td, .table th {
|
||||
border-bottom: 1px solid var(--border);
|
||||
vertical-align: middle;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
/* --- Buttons --- */
|
||||
.btn-accent { background: var(--accent); color: #fff; border: none; }
|
||||
.btn-accent:hover { background: var(--accent-hover); color: #fff; }
|
||||
|
||||
/* --- Dark Mode Overrides --- */
|
||||
[data-theme="dark"] .modal-content,
|
||||
[data-theme="dark"] .modal-body { color: var(--text-main); }
|
||||
[data-theme="dark"] .table { --bs-table-hover-bg: rgba(255, 255, 255, 0.05); }
|
||||
[data-theme="dark"] .table thead th { background: #292b2f; color: var(--text-muted); }
|
||||
[data-theme="dark"] .btn-close { filter: invert(1) grayscale(100%) brightness(200%); }
|
||||
[data-theme="dark"] .text-muted { color: var(--text-muted) !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-md sticky-top px-3 mb-3">
|
||||
<span class="navbar-brand">SekiPOS <small class="text-muted fw-normal" style="font-size:0.65rem;">Ventas</small></span>
|
||||
<div class="ms-3 gap-2 d-flex">
|
||||
<a href="/" class="btn btn-outline-primary btn-sm"><i class="bi bi-box-seam me-1"></i>Inventario</a>
|
||||
<a href="/checkout" class="btn btn-outline-primary btn-sm"><i class="bi bi-cart-fill me-1"></i>Caja</a>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-accent dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-person-circle me-1"></i> <span class="d-none d-sm-inline">{{ user.username }}</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow">
|
||||
<li><button class="dropdown-item" onclick="toggleTheme()"><i class="bi bi-moon-stars me-2" id="theme-icon"></i><span id="theme-label">Modo Oscuro</span></button></li>
|
||||
<li><hr class="dropdown-divider" style="border-color: var(--border);"></li>
|
||||
<li><a class="dropdown-item text-danger" href="/logout"><i class="bi bi-box-arrow-right me-2"></i>Salir</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid px-3">
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="discord-card p-3 shadow-sm text-center">
|
||||
<h6 class="text-muted text-uppercase mb-1" style="font-size: 0.75rem; font-weight: 700;">
|
||||
{% if selected_date %}Día Seleccionado{% else %}Ventas de Hoy{% endif %}
|
||||
</h6>
|
||||
<h2 class="price-cell mb-0" style="color: var(--accent); font-weight: 800;" data-value="{{ stats.daily }}"></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="discord-card p-3 shadow-sm text-center">
|
||||
<h6 class="text-muted text-uppercase mb-1" style="font-size: 0.75rem; font-weight: 700;">Últimos 7 Días</h6>
|
||||
<h2 class="price-cell mb-0" style="color: var(--accent); font-weight: 800;" data-value="{{ stats.week }}"></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="discord-card p-3 shadow-sm text-center">
|
||||
<h6 class="text-muted text-uppercase mb-1" style="font-size: 0.75rem; font-weight: 700;">Este Mes</h6>
|
||||
<h2 class="price-cell mb-0" style="color: var(--accent); font-weight: 800;" data-value="{{ stats.month }}"></h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="discord-card p-3 shadow-sm">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0"><i class="bi bi-receipt-cutoff me-2"></i>Historial</h4>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<label for="date-filter" class="form-label mb-0 text-muted small d-none d-sm-block">Filtrar Día:</label>
|
||||
<input type="date" id="date-filter" class="form-control form-control-sm"
|
||||
style="background: var(--input-bg); color: var(--text-main); border-color: var(--border);"
|
||||
value="{{ selected_date or '' }}" onchange="filterByDate(this.value)">
|
||||
{% if selected_date %}
|
||||
<a href="/sales" class="btn btn-sm btn-outline-secondary" title="Limpiar filtro"><i class="bi bi-x-lg"></i></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nº Ticket</th>
|
||||
<th>Fecha y Hora</th>
|
||||
<th>Método</th>
|
||||
<th>Total</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in sales %}
|
||||
<tr>
|
||||
<td class="font-monospace text-muted">#{{ s[0] }}</td>
|
||||
<td class="utc-date">{{ s[1] }}</td>
|
||||
<td class="text-capitalize">{{ s[3] }}</td>
|
||||
<td class="price-cell fw-bold" data-value="{{ s[2] }}"></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="viewSale({{ s[0] }}, '{{ s[1] }}', {{ s[2] }})">
|
||||
<i class="bi bi-eye"></i> Ver Detalle
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="receiptModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Detalle de Venta <span id="modal-ticket-id" class="text-muted small"></span></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-muted small mb-2" id="modal-date"></p>
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Producto</th>
|
||||
<th>Cant</th>
|
||||
<th class="text-end">Subtotal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="receipt-items">
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="2" class="text-end">TOTAL:</th>
|
||||
<th class="text-end fs-5" id="modal-total" style="color: var(--accent);"></th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
const clp = new Intl.NumberFormat('es-CL', { style: 'currency', currency: 'CLP', minimumFractionDigits: 0 });
|
||||
|
||||
// Format raw UTC dates from DB into friendly local time
|
||||
document.querySelectorAll('.utc-date').forEach(el => {
|
||||
const date = new Date(el.innerText + " UTC");
|
||||
if (!isNaN(date)) {
|
||||
el.innerText = date.toLocaleString('es-CL', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute:'2-digit'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Format all prices
|
||||
document.querySelectorAll('.price-cell').forEach(td => {
|
||||
td.innerText = clp.format(td.getAttribute('data-value'));
|
||||
});
|
||||
|
||||
async function viewSale(id, rawDate, total) {
|
||||
document.getElementById('modal-ticket-id').innerText = `#${id}`;
|
||||
document.getElementById('modal-total').innerText = clp.format(total);
|
||||
|
||||
const localDate = new Date(rawDate + " UTC").toLocaleString('es-CL');
|
||||
document.getElementById('modal-date').innerText = localDate !== "Invalid Date" ? localDate : rawDate;
|
||||
|
||||
const tbody = document.getElementById('receipt-items');
|
||||
tbody.innerHTML = '<tr><td colspan="3" class="text-center text-muted">Cargando...</td></tr>';
|
||||
|
||||
const modal = new bootstrap.Modal(document.getElementById('receiptModal'));
|
||||
modal.show();
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/sale/${id}`);
|
||||
const items = await res.json();
|
||||
|
||||
tbody.innerHTML = items.map(item => `
|
||||
<tr>
|
||||
<td>
|
||||
${item.name}<br>
|
||||
<small class="text-muted font-monospace" style="font-size: 0.7rem;">${item.barcode}</small>
|
||||
</td>
|
||||
<td>${item.qty}</td>
|
||||
<td class="text-end">${clp.format(item.subtotal)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} catch (err) {
|
||||
tbody.innerHTML = '<tr><td colspan="3" class="text-center text-danger">Error cargando productos</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function filterByDate(dateVal) {
|
||||
if (dateVal) {
|
||||
window.location.href = `/sales?date=${dateVal}`;
|
||||
} else {
|
||||
window.location.href = `/sales`;
|
||||
}
|
||||
}
|
||||
|
||||
/* Theme Management */
|
||||
function applyTheme(t) {
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
const isDark = t === 'dark';
|
||||
const icon = document.getElementById('theme-icon');
|
||||
const label = document.getElementById('theme-label');
|
||||
if (icon) icon.className = isDark ? 'bi bi-sun me-2' : 'bi bi-moon-stars me-2';
|
||||
if (label) label.innerText = isDark ? 'Modo Claro' : 'Modo Oscuro';
|
||||
localStorage.setItem('theme', t);
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
applyTheme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
|
||||
}
|
||||
|
||||
(function initTheme() {
|
||||
applyTheme(localStorage.getItem('theme') || 'light');
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user