added bulk price update feature, allowing users to select multiple products and apply a new price to all of them at once. The bulk action bar now shows the count of selected items and enables the "OK" button only when at least one product is selected.
This commit is contained in:
21
app.py
21
app.py
@@ -189,6 +189,27 @@ 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_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__':
|
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)
|
||||||
|
|||||||
@@ -157,26 +157,80 @@ th {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
#new-product-prompt {
|
/* --- BULK ACTIONS PERMANENT BAR --- */
|
||||||
display: none;
|
.bulk-actions {
|
||||||
|
display: flex;
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 12px 20px;
|
padding: 10px 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 15px;
|
||||||
animation: slideDown 0.4s ease;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
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 {
|
@keyframes slideDown {
|
||||||
from {
|
from { transform: translateY(-20px); opacity: 0; }
|
||||||
transform: translateY(-20px);
|
to { transform: translateY(0); opacity: 1; }
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -56,11 +56,22 @@
|
|||||||
<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>
|
||||||
<input type="text" id="searchInput" onkeyup="searchTable()" placeholder="Buscar por nombre o código...">
|
|
||||||
|
|
||||||
|
<!-- 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">
|
<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 +81,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for p in products %}
|
{% for p in products %}
|
||||||
<tr>
|
<tr>
|
||||||
|
<td><input type="checkbox" class="product-checkbox" data-barcode="{{ p[0] }}" onclick="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>
|
||||||
@@ -88,6 +100,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var socket = io();
|
var socket = io();
|
||||||
@@ -194,6 +207,51 @@
|
|||||||
tr[i].style.display = content.toUpperCase().indexOf(filter) > -1 ? "" : "none";
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user