modified: static/styleIndex.css

modified:   templates/index.html
This commit is contained in:
2026-02-26 21:46:42 -03:00
parent df4ff9171d
commit 0dcf0bc930
2 changed files with 135 additions and 310 deletions

View File

@@ -1,5 +1,4 @@
:root { :root {
/* Discord Light Mode Palette */
--bg: #ebedef; --bg: #ebedef;
--card-bg: #ffffff; --card-bg: #ffffff;
--text-main: #2e3338; --text-main: #2e3338;
@@ -10,14 +9,11 @@
--table-head: #f2f3f5; --table-head: #f2f3f5;
--price-color: #2e3338; --price-color: #2e3338;
--accent: #5865F2; --accent: #5865F2;
/* Discord Burple */
--accent-hover: #4752c4; --accent-hover: #4752c4;
--danger: #ed4245; --danger: #ed4245;
--warning: #fee75c;
} }
[data-theme="dark"] { [data-theme="dark"] {
/* Discord Dark Mode Palette */
--bg: #36393f; --bg: #36393f;
--card-bg: #2f3136; --card-bg: #2f3136;
--text-main: #ffffff; --text-main: #ffffff;
@@ -30,14 +26,14 @@
} }
body { body {
font-family: 'gg sans', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'gg sans', 'Segoe UI', sans-serif;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 20px; padding: 20px;
background: var(--bg); background: var(--bg);
color: var(--text-main); color: var(--text-main);
margin: 0; margin: 0;
transition: background 0.2s, color 0.2s; transition: background 0.2s;
} }
.header-bar { .header-bar {
@@ -45,192 +41,90 @@ body {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background: var(--header-bg); background: var(--header-bg);
padding: 10px 25px; padding: 0 20px;
border-radius: 8px; border-radius: 8px;
margin-bottom: 20px; margin-bottom: 15px;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); height: 50px;
flex-shrink: 0;
} }
.main-container { .main-container { display: flex; gap: 20px; }
display: flex; .column { flex: 1; min-width: 0; } /* min-width: 0 prevents flex blow-out */
gap: 25px;
}
.column {
flex: 1;
min-width: 400px;
}
.card { .card {
background: var(--card-bg); background: var(--card-bg);
padding: 25px; padding: 20px;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
margin-bottom: 20px; margin-bottom: 20px;
border: 1px solid var(--border); border: 1px solid var(--border);
text-align: center;
} }
#display-img { #display-img { max-width: 200px; max-height: 200px; object-fit: contain; margin-bottom: 10px; }
max-width: 250px; .price-tag { font-size: 2.5em; font-weight: 800; margin: 5px 0; }
max-height: 250px;
border-radius: 4px;
margin-bottom: 15px;
object-fit: contain;
}
.price-tag {
font-size: 3em;
color: var(--price-color);
font-weight: 800;
margin: 10px 0;
}
input { input {
display: block; display: block;
width: 100%; width: 100%;
margin: 12px 0; padding: 10px;
padding: 12px; margin: 10px 0;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
box-sizing: border-box;
background: var(--input-bg); background: var(--input-bg);
color: var(--text-main); color: var(--text-main);
box-sizing: border-box;
} }
button { button { padding: 10px 15px; cursor: pointer; border-radius: 4px; border: none; font-weight: 600; }
padding: 10px 15px; .btn-save { background: var(--accent); color: white; width: 100%; }
cursor: pointer; .btn-edit { background: var(--accent); color: white; margin-right: 5px; }
border-radius: 4px; .btn-del { background: var(--danger); color: white; }
border: none;
transition: 0.2s;
font-weight: 500;
}
.btn-save { /* Table Handling */
background: var(--accent); .table-wrapper { overflow-x: auto; width: 100%; border-radius: 8px; }
color: white; table { width: 100%; border-collapse: collapse; min-width: 450px; }
width: 100%; th, td { padding: 12px; border-bottom: 1px solid var(--border); text-align: left; }
font-size: 1.1em; th { background: var(--table-head); color: var(--text-muted); font-size: 0.75em; text-transform: uppercase; }
}
.btn-save:hover { input[type="checkbox"] { width: 20px !important; height: 20px !important; margin: 0; cursor: pointer; display: inline-block; }
background: var(--accent-hover);
}
.btn-edit { /* Bulk Actions Bar */
background: var(--accent);
color: white;
}
.btn-del {
background: var(--danger);
color: white;
}
table {
width: 100%;
border-collapse: collapse;
background: var(--card-bg);
border-radius: 8px;
overflow: hidden;
}
th,
td {
padding: 15px;
border-bottom: 1px solid var(--border);
text-align: left;
}
th {
background: var(--table-head);
color: var(--text-muted);
text-transform: uppercase;
font-size: 0.8em;
letter-spacing: 0.5px;
}
.theme-toggle-btn {
background: var(--text-main);
color: var(--bg);
font-weight: bold;
}
/* --- BULK ACTIONS PERMANENT BAR --- */
.bulk-actions { .bulk-actions {
display: flex; display: flex;
background: var(--accent); background: var(--accent);
color: white; color: white;
padding: 10px 20px; padding: 10px 15px;
border-radius: 8px; border-radius: 8px;
margin-bottom: 15px; margin-bottom: 10px;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
min-height: 60px; min-height: 55px;
box-sizing: border-box;
width: 100%;
} }
.bulk-controls { display: flex; align-items: center; gap: 8px; }
.bulk-actions input#bulk-price-input { width: 100px !important; margin: 0 !important; height: 35px !important; background: rgba(0,0,0,0.2) !important; color: white !important; border: 1px solid rgba(255,255,255,0.2) !important; }
.bulk-controls { #new-product-prompt {
display: flex; display: none;
background: var(--accent);
color: white;
padding: 10px 15px;
border-radius: 8px;
margin-bottom: 10px;
justify-content: space-between;
align-items: center; align-items: center;
gap: 10px;
flex-shrink: 0;
} }
/* Override the global 100% width for this specific input */ /* Responsive Overrides */
.bulk-actions input#bulk-price-input {
width: 130px !important;
margin: 0 !important;
padding: 8px !important;
height: 36px !important;
background: rgba(0,0,0,0.2) !important;
border: 1px solid rgba(255,255,255,0.3) !important;
color: white !important;
}
.bulk-actions button {
height: 36px;
white-space: nowrap;
padding: 0 15px;
margin: 0;
}
/* --- RESPONSIVE DESIGN (MOBILE) --- */
@media (max-width: 900px) { @media (max-width: 900px) {
.main-container { .main-container { flex-direction: column; }
flex-direction: column; .header-bar span { display: none; } /* Hide user text on tiny screens */
}
.column {
min-width: 100%;
}
.bulk-actions {
flex-direction: column;
height: auto;
padding: 15px;
gap: 10px;
text-align: center;
}
.bulk-controls {
width: 100%;
justify-content: center;
}
.bulk-actions input#bulk-price-input {
width: 100% !important; /* On mobile, let it be wide */
}
/* Hide barcode on very small screens to save space */
td:nth-child(2), th:nth-child(2) {
display: none;
}
} }
@keyframes slideDown { @media (max-width: 600px) {
from { transform: translateY(-20px); opacity: 0; } body { padding: 10px; }
to { transform: translateY(0); opacity: 1; } th, td { padding: 8px 5px; font-size: 0.8em; }
} .btn-edit, .btn-del { padding: 5px; font-size: 0.75em; }
/* Hide barcode text column on mobile to fit */
td:nth-child(2), th:nth-child(2) { display: none; }
}

View File

@@ -1,73 +1,62 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="es"> <html lang="es">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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> <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"> <link rel="stylesheet" href="./static/styleIndex.css">
</head> </head>
<body> <body>
<div class="header-bar"> <div class="header-bar">
<h2 style="margin:0;">SekiPOS v1.4</h2> <h2>SekiPOS v1.5</h2>
<div style="display: flex; align-items: center; gap: 15px;"> <div style="display: flex; align-items: center; gap: 10px;">
<button onclick="toggleTheme()" class="theme-toggle-btn" id="theme-text" style="margin: 0;">Modo <button onclick="toggleTheme()" id="theme-text" style="padding: 5px 10px; font-size: 0.8em;">Modo Oscuro</button>
Oscuro</button> <a href="/logout" style="color: var(--danger); text-decoration: none; font-size: 0.9em; font-weight: bold;">Salir</a>
<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>
</div> </div>
</div> </div>
<div class="main-container"> <div class="main-container">
<div class="column"> <div class="column">
<div id="new-product-prompt"> <div id="new-product-prompt">
<span style="flex-grow: 1;">¡Nuevo! Barcode <b><span id="new-barcode-display"></span></b>. ¿Deseas <span>Nuevo: <b id="new-barcode-display"></b></span>
agregarlo?</span> <button onclick="dismissPrompt()" style="background:rgba(0,0,0,0.2); color:white;">Omitir</button>
<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>
</div> </div>
<div class="card"> <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"> <form action="/upsert" method="POST" id="product-form">
<input type="text" name="barcode" id="form-barcode" placeholder="Barcode" required> <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="number" name="price" id="form-price" placeholder="Precio (CLP)" required>
<input type="text" name="image_url" id="form-image" placeholder="URL de Imagen"> <input type="text" name="image_url" id="form-image" placeholder="URL Imagen">
<button type="submit" class="btn-save">Guardar en Inventario</button> <button type="submit" class="btn-save">Guardar Cambios</button>
</form> </form>
</div> </div>
</div> </div>
<div class="column"> <div class="column">
<div class="card"> <div class="card">
<h3 style="text-align: left; margin-top:0;">Inventario</h3>
<!-- Bulk Action Bar -->
<div id="bulk-bar" class="bulk-actions"> <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"> <div class="bulk-controls">
<input type="number" id="bulk-price-input" placeholder="Precio CLP"> <input type="number" id="bulk-price-input" placeholder="Precio">
<button onclick="applyBulkPrice()" style="background: white; color: var(--accent);">OK</button> <button onclick="applyBulkPrice()" style="background:white; color:var(--accent);">OK</button>
<button onclick="clearSelection()" class="btn-del"></button> <button onclick="clearSelection()" class="btn-del"></button>
</div> </div>
</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"> <table id="inventoryTable">
<thead> <thead>
<tr> <tr>
@@ -80,17 +69,15 @@
</thead> </thead>
<tbody> <tbody>
{% for p in products %} {% for p in products %}
<tr> <tr data-barcode="{{ p[0] }}">
<td><input type="checkbox" class="product-checkbox" data-barcode="{{ p[0] }}" onclick="updateBulkBar()"></td> <td><input type="checkbox" class="product-checkbox" onclick="updateBulkBar()"></td>
<td style="font-family: monospace;">{{ p[0] }}</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 class="price-cell" data-value="{{ p[2] }}"></td>
<td style="white-space: nowrap;"> <td style="white-space: nowrap;">
<button class="btn-edit" <button class="btn-edit" onclick="editProduct('{{ p[0] }}', '{{ p[1] }}', '{{ p[2] }}', '{{ p[3] }}')">E</button>
onclick="editProduct('{{ p[0] }}', '{{ p[1] }}', '{{ p[2] }}', '{{ p[3] }}')">Editar</button>
<form action="/delete/{{ p[0] }}" method="POST" style="display:inline;"> <form action="/delete/{{ p[0] }}" method="POST" style="display:inline;">
<button type="submit" class="btn-del" <button type="submit" class="btn-del" onclick="return confirm('¿Borrar?')">D</button>
onclick="return confirm('¿Eliminar producto?')">Borrar</button>
</form> </form>
</td> </td>
</tr> </tr>
@@ -104,125 +91,54 @@
<script> <script>
var socket = io(); var socket = io();
const clp = new Intl.NumberFormat('es-CL', { style: 'currency', currency: 'CLP', minimumFractionDigits: 0 });
const clpFormatter = new Intl.NumberFormat('es-CL', { function formatAll() {
style: 'currency',
currency: 'CLP',
minimumFractionDigits: 0
});
function formatTablePrices() {
document.querySelectorAll('.price-cell').forEach(td => { document.querySelectorAll('.price-cell').forEach(td => {
const val = parseFloat(td.getAttribute('data-value')); td.innerText = clp.format(td.getAttribute('data-value'));
td.innerText = clpFormatter.format(val);
}); });
} }
formatTablePrices(); formatAll();
function toggleTheme() { socket.on('new_scan', d => {
const html = document.documentElement; document.getElementById('display-name').innerText = d.name;
const currentTheme = html.getAttribute('data-theme'); document.getElementById('display-price').innerText = clp.format(d.price);
const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; document.getElementById('display-barcode').innerText = d.barcode;
html.setAttribute('data-theme', newTheme); document.getElementById('display-img').src = d.image || './static/placeholder.png';
localStorage.setItem('theme', newTheme); updateForm(d.barcode, d.name, d.price, d.image, "Editando: " + d.name);
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('scan_error', function (data) { socket.on('scan_error', d => {
// 1. Show the "Add New" prompt at the top
const prompt = document.getElementById('new-product-prompt'); 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'; prompt.style.display = 'flex';
updateForm(d.barcode, d.name || '', '', d.image || '', "Crear: " + d.barcode);
// 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));
}); });
function updateForm(barcode, name, price, image, title) { function updateForm(b, n, p, i, t) {
// Reset the prompt if it was open
dismissPrompt(); dismissPrompt();
document.getElementById('form-barcode').value = b;
// Update Form Fields document.getElementById('form-name').value = n;
document.getElementById('form-barcode').value = barcode; document.getElementById('form-price').value = p;
document.getElementById('form-name').value = name || ''; document.getElementById('form-image').value = i;
document.getElementById('form-price').value = price || ''; document.getElementById('form-title').innerText = t;
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();
} }
function dismissPrompt() { function dismissPrompt() { document.getElementById('new-product-prompt').style.display = 'none'; }
document.getElementById('new-product-prompt').style.display = 'none';
} function editProduct(b, n, p, i) {
updateForm(b, n, p, i, "Editando: " + n);
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;
window.scrollTo({ top: 0, behavior: 'smooth' }); 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) { function toggleAll(source) {
const checkboxes = document.querySelectorAll('.product-checkbox'); document.querySelectorAll('.product-checkbox').forEach(cb => cb.checked = source.checked);
checkboxes.forEach(cb => cb.checked = source.checked);
updateBulkBar(); updateBulkBar();
} }
function updateBulkBar() { function updateBulkBar() {
const checked = document.querySelectorAll('.product-checkbox:checked'); const count = document.querySelectorAll('.product-checkbox:checked').length;
document.getElementById('selected-count').innerText = checked.length; document.getElementById('selected-count').innerText = count;
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";
}
} }
function clearSelection() { function clearSelection() {
@@ -233,26 +149,41 @@
async function applyBulkPrice() { async function applyBulkPrice() {
const price = document.getElementById('bulk-price-input').value; 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')) const res = await fetch('/bulk_price_update', {
.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', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ barcodes: barcodes, new_price: price }) body: JSON.stringify({ barcodes, new_price: price })
}); });
if (response.ok) { if (res.ok) {
window.location.reload(); checked.forEach(cb => {
} else { const row = cb.closest('tr');
alert("Error al actualizar productos."); 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> </script>
</body> </body>
</html> </html>