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):
|
def serve_cache(filename):
|
||||||
return send_from_directory(CACHE_DIR, 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__':
|
if __name__ == '__main__':
|
||||||
init_db()
|
init_db()
|
||||||
socketio.run(app, host='0.0.0.0', port=5000, debug=True)
|
socketio.run(app, host='0.0.0.0', port=5000, debug=True)
|
||||||
|
|||||||
@@ -179,4 +179,83 @@ th {
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
opacity: 1;
|
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="column">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3 style="text-align: left; margin-top:0;">Inventario</h3>
|
<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...">
|
<input type="text" id="searchInput" onkeyup="searchTable()" placeholder="Buscar por nombre o código...">
|
||||||
|
|
||||||
<table id="inventoryTable">
|
<table id="inventoryTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th><input type="checkbox" id="select-all" onclick="toggleAll(this)"></th>
|
||||||
<th>Código</th>
|
<th>Código</th>
|
||||||
<th>Nombre</th>
|
<th>Nombre</th>
|
||||||
<th>Precio</th>
|
<th>Precio</th>
|
||||||
@@ -70,6 +78,8 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for p in products %}
|
{% for p in products %}
|
||||||
<tr>
|
<tr>
|
||||||
|
<td><input type="checkbox" class="product-checkbox" value="{{ p[0] }}"
|
||||||
|
onchange="updateBulkBar()"></td>
|
||||||
<td style="font-family: monospace;">{{ p[0] }}</td>
|
<td style="font-family: monospace;">{{ p[0] }}</td>
|
||||||
<td>{{ p[1] }}</td>
|
<td>{{ p[1] }}</td>
|
||||||
<td class="price-cell" data-value="{{ p[2] }}"></td>
|
<td class="price-cell" data-value="{{ p[2] }}"></td>
|
||||||
@@ -194,6 +204,61 @@
|
|||||||
tr[i].style.display = content.toUpperCase().indexOf(filter) > -1 ? "" : "none";
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user