Compare commits
2 Commits
80bf539484
...
df4ff9171d
| Author | SHA1 | Date | |
|---|---|---|---|
| df4ff9171d | |||
| 1b2e63bc86 |
21
app.py
21
app.py
@@ -189,6 +189,27 @@ def scan():
|
||||
def serve_cache(filename):
|
||||
return send_from_directory(CACHE_DIR, filename)
|
||||
|
||||
@app.route('/bulk_price_update', methods=['POST'])
|
||||
@login_required
|
||||
def bulk_price_update():
|
||||
data = request.get_json()
|
||||
barcodes = data.get('barcodes', [])
|
||||
new_price = data.get('new_price')
|
||||
|
||||
if not barcodes or new_price is None:
|
||||
return jsonify({"error": "Missing data"}), 400
|
||||
|
||||
try:
|
||||
with sqlite3.connect(DB_FILE) as conn:
|
||||
# Use executemany for efficiency
|
||||
params = [(float(new_price), b) for b in barcodes]
|
||||
conn.executemany('UPDATE products SET price = ? WHERE barcode = ?', params)
|
||||
conn.commit()
|
||||
return jsonify({"status": "success"}), 200
|
||||
except Exception as e:
|
||||
print(f"Bulk update failed: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_db()
|
||||
socketio.run(app, host='0.0.0.0', port=5000, debug=True)
|
||||
|
||||
@@ -157,26 +157,80 @@ th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#new-product-prompt {
|
||||
display: none;
|
||||
/* --- BULK ACTIONS PERMANENT BAR --- */
|
||||
.bulk-actions {
|
||||
display: flex;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
animation: slideDown 0.4s ease;
|
||||
margin-bottom: 15px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-height: 60px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bulk-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Override the global 100% width for this specific input */
|
||||
.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) {
|
||||
.main-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.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 {
|
||||
from {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
from { transform: translateY(-20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@@ -56,35 +56,48 @@
|
||||
<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>
|
||||
|
||||
<!-- Bulk Action Bar -->
|
||||
<div id="bulk-bar" class="bulk-actions">
|
||||
<span><b id="selected-count">0</b> seleccionados</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>
|
||||
<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%;">
|
||||
<table id="inventoryTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="select-all" onclick="toggleAll(this)"></th>
|
||||
<th>Código</th>
|
||||
<th>Nombre</th>
|
||||
<th>Precio</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in products %}
|
||||
<tr>
|
||||
<td><input type="checkbox" class="product-checkbox" data-barcode="{{ p[0] }}" onclick="updateBulkBar()"></td>
|
||||
<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>
|
||||
</div>
|
||||
@@ -194,6 +207,51 @@
|
||||
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);
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
document.querySelectorAll('.product-checkbox').forEach(cb => cb.checked = false);
|
||||
document.getElementById('select-all').checked = false;
|
||||
updateBulkBar();
|
||||
}
|
||||
|
||||
async function applyBulkPrice() {
|
||||
const price = document.getElementById('bulk-price-input').value;
|
||||
if (!price || price < 0) return alert("Ingresa un precio válido");
|
||||
|
||||
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', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ barcodes: barcodes, new_price: price })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert("Error al actualizar productos.");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user