Compare commits
6 Commits
2
...
df4ff9171d
| Author | SHA1 | Date | |
|---|---|---|---|
| df4ff9171d | |||
| 1b2e63bc86 | |||
| 80bf539484 | |||
| 13bba33c26 | |||
| ecd98c72ce | |||
| 344229b77b |
@@ -1,4 +1,4 @@
|
|||||||
# SekiPOS v1.2 🍫🥤
|
# SekiPOS v1.4 🍫🥤
|
||||||
|
|
||||||
A reactive POS inventory system for software engineers with a snack addiction. Features real-time UI updates, automatic product discovery via Open Food Facts, and local image caching.
|
A reactive POS inventory system for software engineers with a snack addiction. Features real-time UI updates, automatic product discovery via Open Food Facts, and local image caching.
|
||||||
|
|
||||||
|
|||||||
BIN
ScannerPython/output/scannerV2.exe
Normal file
BIN
ScannerPython/output/scannerV2.exe
Normal file
Binary file not shown.
36
ScannerPython/scannerV2.py
Normal file
36
ScannerPython/scannerV2.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import serial
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def run_bridge():
|
||||||
|
parser = argparse.ArgumentParser(description="Scanner Bridge for the technically impaired")
|
||||||
|
parser.add_argument('--port', default='COM5', help='Serial port (default: COM5)')
|
||||||
|
parser.add_argument('--baud', type=int, default=115200, help='Baud rate (default: 115200)')
|
||||||
|
parser.add_argument('--url', default='https://scanner.sekidesu.xyz/scan', help='Server URL')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
ser = serial.Serial(args.port, args.baud, timeout=0.1)
|
||||||
|
print(f"Connected to {args.port} at {args.baud} bauds.")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if ser.in_waiting > 0:
|
||||||
|
barcode = ser.readline().decode('utf-8', errors='ignore').strip()
|
||||||
|
if barcode:
|
||||||
|
print(f"Scanned: {barcode}")
|
||||||
|
try:
|
||||||
|
resp = requests.get(args.url, params={'content': barcode}, timeout=5)
|
||||||
|
print(f"Server responded: {resp.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to send to server: {e}")
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"Error opening {args.port}: {e}")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nBridge stopped. Finally.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_bridge()
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="header-bar">
|
<div class="header-bar">
|
||||||
<h2 style="margin:0;">SekiPOS v1.2</h2>
|
<h2 style="margin:0;">SekiPOS v1.4</h2>
|
||||||
<div style="display: flex; align-items: center; gap: 15px;">
|
<div style="display: flex; align-items: center; gap: 15px;">
|
||||||
<button onclick="toggleTheme()" class="theme-toggle-btn" id="theme-text" style="margin: 0;">Modo
|
<button onclick="toggleTheme()" class="theme-toggle-btn" id="theme-text" style="margin: 0;">Modo
|
||||||
Oscuro</button>
|
Oscuro</button>
|
||||||
@@ -56,35 +56,48 @@
|
|||||||
<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 -->
|
||||||
<table id="inventoryTable">
|
<div id="bulk-bar" class="bulk-actions">
|
||||||
<thead>
|
<span><b id="selected-count">0</b> seleccionados</span>
|
||||||
<tr>
|
<div class="bulk-controls">
|
||||||
<th>Código</th>
|
<input type="number" id="bulk-price-input" placeholder="Precio CLP">
|
||||||
<th>Nombre</th>
|
<button onclick="applyBulkPrice()" style="background: white; color: var(--accent);">OK</button>
|
||||||
<th>Precio</th>
|
<button onclick="clearSelection()" class="btn-del">✕</button>
|
||||||
<th>Acciones</th>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
<input type="text" id="searchInput" onkeyup="searchTable()" placeholder="Buscar...">
|
||||||
<tbody>
|
<div style="overflow-x: auto; width: 100%;">
|
||||||
{% for p in products %}
|
<table id="inventoryTable">
|
||||||
<tr>
|
<thead>
|
||||||
<td style="font-family: monospace;">{{ p[0] }}</td>
|
<tr>
|
||||||
<td>{{ p[1] }}</td>
|
<th><input type="checkbox" id="select-all" onclick="toggleAll(this)"></th>
|
||||||
<td class="price-cell" data-value="{{ p[2] }}"></td>
|
<th>Código</th>
|
||||||
<td style="white-space: nowrap;">
|
<th>Nombre</th>
|
||||||
<button class="btn-edit"
|
<th>Precio</th>
|
||||||
onclick="editProduct('{{ p[0] }}', '{{ p[1] }}', '{{ p[2] }}', '{{ p[3] }}')">Editar</button>
|
<th>Acciones</th>
|
||||||
<form action="/delete/{{ p[0] }}" method="POST" style="display:inline;">
|
</tr>
|
||||||
<button type="submit" class="btn-del"
|
</thead>
|
||||||
onclick="return confirm('¿Eliminar producto?')">Borrar</button>
|
<tbody>
|
||||||
</form>
|
{% for p in products %}
|
||||||
</td>
|
<tr>
|
||||||
</tr>
|
<td><input type="checkbox" class="product-checkbox" data-barcode="{{ p[0] }}" onclick="updateBulkBar()"></td>
|
||||||
{% endfor %}
|
<td style="font-family: monospace;">{{ p[0] }}</td>
|
||||||
</tbody>
|
<td>{{ p[1] }}</td>
|
||||||
</table>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,7 +181,7 @@
|
|||||||
setTimeout(() => { formCard.style.border = "none"; }, 500);
|
setTimeout(() => { formCard.style.border = "none"; }, 500);
|
||||||
|
|
||||||
// Focus price because it's usually the only thing missing
|
// Focus price because it's usually the only thing missing
|
||||||
document.getElementById('form-price').focus();
|
//document.getElementById('form-price').focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismissPrompt() {
|
function dismissPrompt() {
|
||||||
@@ -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