modified: app.py
modified: static/styleIndex.css modified: templates/index.html
This commit is contained in:
27
app.py
27
app.py
@@ -189,6 +189,33 @@ def scan():
|
||||
def serve_cache(filename):
|
||||
return send_from_directory(CACHE_DIR, filename)
|
||||
|
||||
@app.route('/bulk_delete', methods=['POST'])
|
||||
@login_required
|
||||
def bulk_delete():
|
||||
barcodes = request.json.get('barcodes', [])
|
||||
with sqlite3.connect(DB_FILE) as conn:
|
||||
conn.executemany('DELETE FROM products WHERE barcode = ?', [(b,) for b in barcodes])
|
||||
conn.commit()
|
||||
# Optional: Logic to delete cached images could go here
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
@app.route('/bulk_edit_price', methods=['POST'])
|
||||
@login_required
|
||||
def bulk_edit_price():
|
||||
data = request.json
|
||||
barcodes = data.get('barcodes', [])
|
||||
new_price = data.get('price')
|
||||
|
||||
try:
|
||||
price = float(new_price)
|
||||
with sqlite3.connect(DB_FILE) as conn:
|
||||
conn.executemany('UPDATE products SET price = ? WHERE barcode = ?',
|
||||
[(price, b) for b in barcodes])
|
||||
conn.commit()
|
||||
return jsonify({"status": "ok"})
|
||||
except ValueError:
|
||||
return jsonify({"status": "error", "message": "Invalid price"}), 400
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_db()
|
||||
socketio.run(app, host='0.0.0.0', port=5000, debug=True)
|
||||
|
||||
@@ -179,4 +179,83 @@ th {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#bulk-action-bar {
|
||||
display: none;
|
||||
/* Controlled by JS */
|
||||
background: var(--accent);
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
color: white;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
animation: slideDown 0.3s ease;
|
||||
}
|
||||
|
||||
#bulk-action-bar span {
|
||||
font-weight: bold;
|
||||
font-size: 0.9em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#bulk-action-bar input[type="number"] {
|
||||
width: 120px;
|
||||
margin: 0;
|
||||
height: 38px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Ensure the parent container can hold the absolute bar */
|
||||
.column {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#bulk-action-bar {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 25px; /* Matches card padding */
|
||||
right: 25px;
|
||||
z-index: 10;
|
||||
background: var(--accent);
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||
animation: slideIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateY(-10px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
.btn-bulk-save {
|
||||
background: #2ecc71;
|
||||
/* A more professional green */
|
||||
color: white;
|
||||
flex-grow: 1;
|
||||
font-weight: bold;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.btn-bulk-save:hover {
|
||||
background: #27ae60;
|
||||
}
|
||||
|
||||
.btn-bulk-del {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.btn-bulk-del:hover {
|
||||
background: var(--danger);
|
||||
}
|
||||
@@ -56,11 +56,19 @@
|
||||
<div class="column">
|
||||
<div class="card">
|
||||
<h3 style="text-align: left; margin-top:0;">Inventario</h3>
|
||||
<div id="bulk-action-bar">
|
||||
<span id="selected-count">0 seleccionados</span>
|
||||
<input type="number" id="bulk-price-input" placeholder="Precio CLP">
|
||||
<button onclick="applyBulkPrice()" class="btn-bulk-save">Actualizar Precio</button>
|
||||
<button onclick="applyBulkDelete()" class="btn-bulk-del">Borrar</button>
|
||||
</div>
|
||||
|
||||
<input type="text" id="searchInput" onkeyup="searchTable()" placeholder="Buscar por nombre o código...">
|
||||
|
||||
<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>
|
||||
@@ -70,6 +78,8 @@
|
||||
<tbody>
|
||||
{% for p in products %}
|
||||
<tr>
|
||||
<td><input type="checkbox" class="product-checkbox" value="{{ p[0] }}"
|
||||
onchange="updateBulkBar()"></td>
|
||||
<td style="font-family: monospace;">{{ p[0] }}</td>
|
||||
<td>{{ p[1] }}</td>
|
||||
<td class="price-cell" data-value="{{ p[2] }}"></td>
|
||||
@@ -194,6 +204,61 @@
|
||||
tr[i].style.display = content.toUpperCase().indexOf(filter) > -1 ? "" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAll(source) {
|
||||
document.querySelectorAll('.product-checkbox').forEach(cb => cb.checked = source.checked);
|
||||
updateBulkBar();
|
||||
}
|
||||
|
||||
function updateBulkBar() {
|
||||
const selected = document.querySelectorAll('.product-checkbox:checked');
|
||||
const bar = document.getElementById('bulk-action-bar');
|
||||
const countDisplay = document.getElementById('selected-count');
|
||||
|
||||
if (selected.length > 0) {
|
||||
bar.style.display = 'flex';
|
||||
countDisplay.innerText = `${selected.length} seleccionados`;
|
||||
} else {
|
||||
bar.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedBarcodes() {
|
||||
return Array.from(document.querySelectorAll('.product-checkbox:checked')).map(cb => cb.value);
|
||||
}
|
||||
|
||||
async function applyBulkPrice() {
|
||||
const barcodes = getSelectedBarcodes();
|
||||
const price = document.getElementById('bulk-price-input').value;
|
||||
if (!price) return alert("Ingresa un precio");
|
||||
|
||||
const resp = await fetch('/bulk_edit_price', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ barcodes, price })
|
||||
});
|
||||
if (resp.ok) location.reload();
|
||||
}
|
||||
|
||||
async function applyBulkDelete() {
|
||||
if (!confirm("¿Eliminar todos los productos seleccionados?")) return;
|
||||
const barcodes = getSelectedBarcodes();
|
||||
|
||||
const resp = await fetch('/bulk_delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ barcodes })
|
||||
});
|
||||
if (resp.ok) location.reload();
|
||||
}
|
||||
|
||||
// Clear checkboxes on reload to prevent ghost selections
|
||||
window.addEventListener('load', () => {
|
||||
document.querySelectorAll('.product-checkbox').forEach(cb => cb.checked = false);
|
||||
const selectAll = document.getElementById('select-all');
|
||||
if (selectAll) selectAll.checked = false;
|
||||
updateBulkBar();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user