342 lines
12 KiB
HTML
342 lines
12 KiB
HTML
<!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>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
|
|
<style>
|
|
:root {
|
|
/* Discord Light Mode Palette */
|
|
--bg: #ebedef;
|
|
--card-bg: #ffffff;
|
|
--text-main: #2e3338;
|
|
--text-muted: #4f5660;
|
|
--border: #e3e5e8;
|
|
--header-bg: #ffffff;
|
|
--input-bg: #e3e5e8;
|
|
--table-head: #f2f3f5;
|
|
--price-color: #2e3338;
|
|
--accent: #5865F2; /* Discord Burple */
|
|
--accent-hover: #4752c4;
|
|
--danger: #ed4245;
|
|
--warning: #fee75c;
|
|
}
|
|
|
|
[data-theme="dark"] {
|
|
/* Discord Dark Mode Palette */
|
|
--bg: #36393f;
|
|
--card-bg: #2f3136;
|
|
--text-main: #ffffff;
|
|
--text-muted: #b9bbbe;
|
|
--border: #202225;
|
|
--header-bg: #202225;
|
|
--input-bg: #202225;
|
|
--table-head: #292b2f;
|
|
--price-color: #ffffff;
|
|
}
|
|
|
|
body {
|
|
font-family: 'gg sans', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 20px;
|
|
background: var(--bg);
|
|
color: var(--text-main);
|
|
margin: 0;
|
|
transition: background 0.2s, color 0.2s;
|
|
}
|
|
|
|
.header-bar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
background: var(--header-bg);
|
|
padding: 10px 25px;
|
|
border-radius: 8px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 1px 0 rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.main-container {
|
|
display: flex;
|
|
gap: 25px;
|
|
}
|
|
|
|
.column {
|
|
flex: 1;
|
|
min-width: 400px;
|
|
}
|
|
|
|
.card {
|
|
background: var(--card-bg);
|
|
padding: 25px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
text-align: center;
|
|
margin-bottom: 20px;
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
#display-img {
|
|
max-width: 250px;
|
|
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 {
|
|
display: block;
|
|
width: 100%;
|
|
margin: 12px 0;
|
|
padding: 12px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
box-sizing: border-box;
|
|
background: var(--input-bg);
|
|
color: var(--text-main);
|
|
}
|
|
|
|
button {
|
|
padding: 10px 15px;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
border: none;
|
|
transition: 0.2s;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.btn-save {
|
|
background: var(--accent);
|
|
color: white;
|
|
width: 100%;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.btn-save:hover {
|
|
background: var(--accent-hover);
|
|
}
|
|
|
|
.btn-edit {
|
|
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;
|
|
}
|
|
|
|
#new-product-prompt {
|
|
display: none;
|
|
background: var(--accent);
|
|
color: white;
|
|
padding: 12px 20px;
|
|
border-radius: 8px;
|
|
margin-bottom: 20px;
|
|
animation: slideDown 0.4s ease;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
@keyframes slideDown {
|
|
from { transform: translateY(-20px); opacity: 0; }
|
|
to { transform: translateY(0); opacity: 1; }
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div class="header-bar">
|
|
<h2 style="margin:0;">SekiPOS v1.2</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>
|
|
</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>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3 id="form-title" style="margin-top:0;">Agregar/Editar Producto</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="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>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="column">
|
|
<div class="card">
|
|
<h3 style="text-align: left; margin-top:0;">Inventario</h3>
|
|
<input type="text" id="searchInput" onkeyup="searchTable()" placeholder="Buscar por nombre o código...">
|
|
|
|
<table id="inventoryTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Código</th>
|
|
<th>Nombre</th>
|
|
<th>Precio</th>
|
|
<th>Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for p in products %}
|
|
<tr>
|
|
<td style="font-family: monospace;">{{ p[0] }}</td>
|
|
<td>{{ 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>
|
|
<form action="/delete/{{ p[0] }}" method="POST" style="display:inline;">
|
|
<button type="submit" class="btn-del" onclick="return confirm('¿Eliminar producto?')">Borrar</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
var socket = io();
|
|
|
|
const clpFormatter = new Intl.NumberFormat('es-CL', {
|
|
style: 'currency',
|
|
currency: 'CLP',
|
|
minimumFractionDigits: 0
|
|
});
|
|
|
|
function formatTablePrices() {
|
|
document.querySelectorAll('.price-cell').forEach(td => {
|
|
const val = parseFloat(td.getAttribute('data-value'));
|
|
td.innerText = clpFormatter.format(val);
|
|
});
|
|
}
|
|
formatTablePrices();
|
|
|
|
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) {
|
|
dismissPrompt();
|
|
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';
|
|
});
|
|
|
|
socket.on('scan_error', function (data) {
|
|
const prompt = document.getElementById('new-product-prompt');
|
|
document.getElementById('new-barcode-display').innerText = data.barcode;
|
|
prompt.style.display = 'flex';
|
|
document.getElementById('form-barcode').value = data.barcode;
|
|
document.getElementById('form-name').value = data.name || '';
|
|
document.getElementById('form-image').value = data.image || '';
|
|
document.getElementById('form-price').value = '';
|
|
document.getElementById('form-title').innerText = "Crear Nuevo: " + (data.name || data.barcode);
|
|
document.getElementById('form-price').focus();
|
|
});
|
|
|
|
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;
|
|
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";
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html> |