modified: static/styleIndex.css
modified: templates/index.html
This commit is contained in:
@@ -1,73 +1,62 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SekiPOS - Inventory Management</title>
|
||||
<title>SekiPOS - Inventory</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
|
||||
<link rel="shortcut icon" href="./static/favicon.png" type="image/x-icon">
|
||||
<link rel="stylesheet" href="./static/styleIndex.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="header-bar">
|
||||
<h2 style="margin:0;">SekiPOS v1.4</h2>
|
||||
<div style="display: flex; align-items: center; gap: 15px;">
|
||||
<button onclick="toggleTheme()" class="theme-toggle-btn" id="theme-text" style="margin: 0;">Modo
|
||||
Oscuro</button>
|
||||
<span style="color: var(--border);">|</span>
|
||||
<span>Usuario: <b>{{ user.username }}</b></span>
|
||||
<span style="color: var(--border);">|</span>
|
||||
<a href="/logout" style="color: var(--danger); text-decoration: none; font-weight: bold;">Cerrar Sesión</a>
|
||||
<h2>SekiPOS v1.5</h2>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<button onclick="toggleTheme()" id="theme-text" style="padding: 5px 10px; font-size: 0.8em;">Modo Oscuro</button>
|
||||
<a href="/logout" style="color: var(--danger); text-decoration: none; font-size: 0.9em; font-weight: bold;">Salir</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container">
|
||||
<div class="column">
|
||||
<div id="new-product-prompt">
|
||||
<span style="flex-grow: 1;">¡Nuevo! Barcode <b><span id="new-barcode-display"></span></b>. ¿Deseas
|
||||
agregarlo?</span>
|
||||
<button onclick="dismissPrompt()"
|
||||
style="background:rgba(0,0,0,0.2); color:white; margin-left: 15px;">Omitir</button>
|
||||
</div>
|
||||
|
||||
<div class="card" id="scan-display">
|
||||
<h2 style="color: var(--text-muted); margin-top:0;">Último Escaneado</h2>
|
||||
<img id="display-img" src="./static/placeholder.png" alt="Producto">
|
||||
<h1 id="display-name" style="margin: 10px 0;">Escanea un producto</h1>
|
||||
<div class="price-tag" id="display-price">$0</div>
|
||||
<p id="display-barcode" style="color: var(--text-muted); font-family: monospace;"></p>
|
||||
<span>Nuevo: <b id="new-barcode-display"></b></span>
|
||||
<button onclick="dismissPrompt()" style="background:rgba(0,0,0,0.2); color:white;">Omitir</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3 id="form-title" style="margin-top:0;">Agregar/Editar Producto</h3>
|
||||
<h3 style="color: var(--text-muted); margin-top:0;">Último Escaneado</h3>
|
||||
<img id="display-img" src="./static/placeholder.png">
|
||||
<h2 id="display-name">Esperando scan...</h2>
|
||||
<div class="price-tag" id="display-price">$0</div>
|
||||
<p id="display-barcode" style="font-family: monospace; opacity: 0.5;"></p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3 id="form-title" style="margin-top:0;">Editar/Crear</h3>
|
||||
<form action="/upsert" method="POST" id="product-form">
|
||||
<input type="text" name="barcode" id="form-barcode" placeholder="Barcode" required>
|
||||
<input type="text" name="name" id="form-name" placeholder="Nombre del Producto" required>
|
||||
<input type="text" name="name" id="form-name" placeholder="Nombre" required>
|
||||
<input type="number" name="price" id="form-price" placeholder="Precio (CLP)" required>
|
||||
<input type="text" name="image_url" id="form-image" placeholder="URL de Imagen">
|
||||
<button type="submit" class="btn-save">Guardar en Inventario</button>
|
||||
<input type="text" name="image_url" id="form-image" placeholder="URL Imagen">
|
||||
<button type="submit" class="btn-save">Guardar Cambios</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<div class="card">
|
||||
<h3 style="text-align: left; margin-top:0;">Inventario</h3>
|
||||
|
||||
<!-- Bulk Action Bar -->
|
||||
<div id="bulk-bar" class="bulk-actions">
|
||||
<span><b id="selected-count">0</b> seleccionados</span>
|
||||
<span><b id="selected-count">0</b> listos</span>
|
||||
<div class="bulk-controls">
|
||||
<input type="number" id="bulk-price-input" placeholder="Precio CLP">
|
||||
<button onclick="applyBulkPrice()" style="background: white; color: var(--accent);">OK</button>
|
||||
<input type="number" id="bulk-price-input" placeholder="Precio">
|
||||
<button onclick="applyBulkPrice()" style="background:white; color:var(--accent);">OK</button>
|
||||
<button onclick="clearSelection()" class="btn-del">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" id="searchInput" onkeyup="searchTable()" placeholder="Buscar...">
|
||||
<div style="overflow-x: auto; width: 100%;">
|
||||
|
||||
<input type="text" id="searchInput" onkeyup="searchTable()" placeholder="Filtrar productos...">
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table id="inventoryTable">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -80,17 +69,15 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in products %}
|
||||
<tr>
|
||||
<td><input type="checkbox" class="product-checkbox" data-barcode="{{ p[0] }}" onclick="updateBulkBar()"></td>
|
||||
<tr data-barcode="{{ p[0] }}">
|
||||
<td><input type="checkbox" class="product-checkbox" onclick="updateBulkBar()"></td>
|
||||
<td style="font-family: monospace;">{{ p[0] }}</td>
|
||||
<td>{{ p[1] }}</td>
|
||||
<td class="name-cell">{{ p[1] }}</td>
|
||||
<td class="price-cell" data-value="{{ p[2] }}"></td>
|
||||
<td style="white-space: nowrap;">
|
||||
<button class="btn-edit"
|
||||
onclick="editProduct('{{ p[0] }}', '{{ p[1] }}', '{{ p[2] }}', '{{ p[3] }}')">Editar</button>
|
||||
<button class="btn-edit" onclick="editProduct('{{ p[0] }}', '{{ p[1] }}', '{{ p[2] }}', '{{ p[3] }}')">E</button>
|
||||
<form action="/delete/{{ p[0] }}" method="POST" style="display:inline;">
|
||||
<button type="submit" class="btn-del"
|
||||
onclick="return confirm('¿Eliminar producto?')">Borrar</button>
|
||||
<button type="submit" class="btn-del" onclick="return confirm('¿Borrar?')">D</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -104,125 +91,54 @@
|
||||
|
||||
<script>
|
||||
var socket = io();
|
||||
const clp = new Intl.NumberFormat('es-CL', { style: 'currency', currency: 'CLP', minimumFractionDigits: 0 });
|
||||
|
||||
const clpFormatter = new Intl.NumberFormat('es-CL', {
|
||||
style: 'currency',
|
||||
currency: 'CLP',
|
||||
minimumFractionDigits: 0
|
||||
});
|
||||
|
||||
function formatTablePrices() {
|
||||
function formatAll() {
|
||||
document.querySelectorAll('.price-cell').forEach(td => {
|
||||
const val = parseFloat(td.getAttribute('data-value'));
|
||||
td.innerText = clpFormatter.format(val);
|
||||
td.innerText = clp.format(td.getAttribute('data-value'));
|
||||
});
|
||||
}
|
||||
formatTablePrices();
|
||||
formatAll();
|
||||
|
||||
function toggleTheme() {
|
||||
const html = document.documentElement;
|
||||
const currentTheme = html.getAttribute('data-theme');
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
html.setAttribute('data-theme', newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
updateThemeUI(newTheme);
|
||||
}
|
||||
|
||||
function updateThemeUI(theme) {
|
||||
const btnText = document.getElementById('theme-text');
|
||||
btnText.innerText = theme === 'dark' ? 'Modo Claro' : 'Modo Oscuro';
|
||||
}
|
||||
|
||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
updateThemeUI(savedTheme);
|
||||
|
||||
socket.on('new_scan', function (data) {
|
||||
// 1. Update the Display Card
|
||||
document.getElementById('display-name').innerText = data.name;
|
||||
document.getElementById('display-price').innerText = clpFormatter.format(data.price);
|
||||
document.getElementById('display-barcode').innerText = data.barcode;
|
||||
document.getElementById('display-img').src = data.image || './static/placeholder.png';
|
||||
|
||||
// 2. Sync the Edit Form to this product
|
||||
updateForm(data.barcode, data.name, data.price, data.image, "Editando: " + data.name);
|
||||
socket.on('new_scan', d => {
|
||||
document.getElementById('display-name').innerText = d.name;
|
||||
document.getElementById('display-price').innerText = clp.format(d.price);
|
||||
document.getElementById('display-barcode').innerText = d.barcode;
|
||||
document.getElementById('display-img').src = d.image || './static/placeholder.png';
|
||||
updateForm(d.barcode, d.name, d.price, d.image, "Editando: " + d.name);
|
||||
});
|
||||
|
||||
socket.on('scan_error', function (data) {
|
||||
// 1. Show the "Add New" prompt at the top
|
||||
socket.on('scan_error', d => {
|
||||
const prompt = document.getElementById('new-product-prompt');
|
||||
document.getElementById('new-barcode-display').innerText = data.barcode;
|
||||
document.getElementById('new-barcode-display').innerText = d.barcode;
|
||||
prompt.style.display = 'flex';
|
||||
|
||||
// 2. Update Display Card with whatever info we have (cached or external)
|
||||
document.getElementById('display-name').innerText = data.name || "Producto Nuevo";
|
||||
document.getElementById('display-price').innerText = clpFormatter.format(0);
|
||||
document.getElementById('display-barcode').innerText = data.barcode;
|
||||
document.getElementById('display-img').src = data.image || './static/placeholder.png';
|
||||
|
||||
// 3. Prepare the form for a new entry
|
||||
updateForm(data.barcode, data.name, '', data.image, "Crear Nuevo: " + (data.name || data.barcode));
|
||||
updateForm(d.barcode, d.name || '', '', d.image || '', "Crear: " + d.barcode);
|
||||
});
|
||||
|
||||
function updateForm(barcode, name, price, image, title) {
|
||||
// Reset the prompt if it was open
|
||||
function updateForm(b, n, p, i, t) {
|
||||
dismissPrompt();
|
||||
|
||||
// Update Form Fields
|
||||
document.getElementById('form-barcode').value = barcode;
|
||||
document.getElementById('form-name').value = name || '';
|
||||
document.getElementById('form-price').value = price || '';
|
||||
document.getElementById('form-image').value = image || '';
|
||||
document.getElementById('form-title').innerText = title;
|
||||
|
||||
// Visual feedback: briefly highlight the form
|
||||
const formCard = document.getElementById('product-form').parentElement;
|
||||
formCard.style.border = "2px solid var(--accent)";
|
||||
setTimeout(() => { formCard.style.border = "none"; }, 500);
|
||||
|
||||
// Focus price because it's usually the only thing missing
|
||||
//document.getElementById('form-price').focus();
|
||||
document.getElementById('form-barcode').value = b;
|
||||
document.getElementById('form-name').value = n;
|
||||
document.getElementById('form-price').value = p;
|
||||
document.getElementById('form-image').value = i;
|
||||
document.getElementById('form-title').innerText = t;
|
||||
}
|
||||
|
||||
function dismissPrompt() {
|
||||
document.getElementById('new-product-prompt').style.display = 'none';
|
||||
}
|
||||
|
||||
function editProduct(barcode, name, price, image) {
|
||||
dismissPrompt();
|
||||
document.getElementById('form-barcode').value = barcode;
|
||||
document.getElementById('form-name').value = name;
|
||||
document.getElementById('form-price').value = price;
|
||||
document.getElementById('form-image').value = image;
|
||||
document.getElementById('form-title').innerText = "Editando: " + name;
|
||||
function dismissPrompt() { document.getElementById('new-product-prompt').style.display = 'none'; }
|
||||
|
||||
function editProduct(b, n, p, i) {
|
||||
updateForm(b, n, p, i, "Editando: " + n);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function searchTable() {
|
||||
var input = document.getElementById("searchInput");
|
||||
var filter = input.value.toUpperCase();
|
||||
var tr = document.getElementById("inventoryTable").getElementsByTagName("tr");
|
||||
for (var i = 1; i < tr.length; i++) {
|
||||
var content = tr[i].textContent || tr[i].innerText;
|
||||
tr[i].style.display = content.toUpperCase().indexOf(filter) > -1 ? "" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAll(source) {
|
||||
const checkboxes = document.querySelectorAll('.product-checkbox');
|
||||
checkboxes.forEach(cb => cb.checked = source.checked);
|
||||
document.querySelectorAll('.product-checkbox').forEach(cb => cb.checked = source.checked);
|
||||
updateBulkBar();
|
||||
}
|
||||
|
||||
function updateBulkBar() {
|
||||
const checked = document.querySelectorAll('.product-checkbox:checked');
|
||||
document.getElementById('selected-count').innerText = checked.length;
|
||||
|
||||
const okBtn = document.querySelector('.bulk-controls button[onclick="applyBulkPrice()"]');
|
||||
if (okBtn) {
|
||||
okBtn.disabled = checked.length === 0;
|
||||
okBtn.style.opacity = checked.length === 0 ? "0.5" : "1";
|
||||
}
|
||||
const count = document.querySelectorAll('.product-checkbox:checked').length;
|
||||
document.getElementById('selected-count').innerText = count;
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
@@ -233,26 +149,41 @@
|
||||
|
||||
async function applyBulkPrice() {
|
||||
const price = document.getElementById('bulk-price-input').value;
|
||||
if (!price || price < 0) return alert("Ingresa un precio válido");
|
||||
if (!price) return;
|
||||
const checked = document.querySelectorAll('.product-checkbox:checked');
|
||||
const barcodes = Array.from(checked).map(cb => cb.closest('tr').getAttribute('data-barcode'));
|
||||
|
||||
const barcodes = Array.from(document.querySelectorAll('.product-checkbox:checked'))
|
||||
.map(cb => cb.getAttribute('data-barcode'));
|
||||
|
||||
if (!confirm(`¿Actualizar el precio de ${barcodes.length} productos a ${clpFormatter.format(price)}?`)) return;
|
||||
|
||||
const response = await fetch('/bulk_price_update', {
|
||||
const res = await fetch('/bulk_price_update', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ barcodes: barcodes, new_price: price })
|
||||
body: JSON.stringify({ barcodes, new_price: price })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert("Error al actualizar productos.");
|
||||
if (res.ok) {
|
||||
checked.forEach(cb => {
|
||||
const row = cb.closest('tr');
|
||||
const cell = row.querySelector('.price-cell');
|
||||
cell.setAttribute('data-value', price);
|
||||
cell.innerText = clp.format(price);
|
||||
cb.checked = false;
|
||||
});
|
||||
updateBulkBar();
|
||||
}
|
||||
}
|
||||
|
||||
function searchTable() {
|
||||
let q = document.getElementById("searchInput").value.toUpperCase();
|
||||
document.querySelectorAll("#inventoryTable tbody tr").forEach(tr => {
|
||||
tr.style.display = tr.innerText.toUpperCase().includes(q) ? "" : "none";
|
||||
});
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const t = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
localStorage.setItem('theme', t);
|
||||
}
|
||||
document.documentElement.setAttribute('data-theme', localStorage.getItem('theme') || 'light');
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user