feat(inventory): dynamic tab badges with real tenant data
- Add /pos/api/inventory/stats endpoint returning counts per tab - Replace hardcoded badge numbers (4,817, 14, 3, 23) with dynamic values - Frontend auto-fetches stats on page load and updates badges
This commit is contained in:
@@ -809,6 +809,52 @@ def physical_count_approve():
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Stats Summary ─────────────────────────────
|
||||||
|
|
||||||
|
@inventory_bp.route('/stats', methods=['GET'])
|
||||||
|
@require_auth('inventory.view')
|
||||||
|
def api_inventory_stats():
|
||||||
|
"""Get inventory summary counts for dashboard badges."""
|
||||||
|
conn = get_tenant_conn(g.tenant_id)
|
||||||
|
cur = conn.cursor()
|
||||||
|
branch_id = getattr(g, 'branch_id', None)
|
||||||
|
|
||||||
|
# Stock count
|
||||||
|
cur.execute("SELECT COUNT(*) FROM inventory WHERE is_active = true AND (branch_id = %s OR %s IS NULL)", (branch_id, branch_id))
|
||||||
|
stock = cur.fetchone()[0]
|
||||||
|
|
||||||
|
# Operations counts by type
|
||||||
|
cur.execute("""
|
||||||
|
SELECT operation_type, COUNT(*)
|
||||||
|
FROM inventory_operations
|
||||||
|
WHERE (branch_id = %s OR %s IS NULL)
|
||||||
|
GROUP BY operation_type
|
||||||
|
""", (branch_id, branch_id))
|
||||||
|
op_counts = {row[0]: row[1] for row in cur.fetchall()}
|
||||||
|
|
||||||
|
# Physical counts
|
||||||
|
cur.execute("SELECT COUNT(*) FROM physical_counts WHERE (branch_id = %s OR %s IS NULL)", (branch_id, branch_id))
|
||||||
|
physical = cur.fetchone()[0]
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Alerts (reuse existing function)
|
||||||
|
conn2 = get_tenant_conn(g.tenant_id)
|
||||||
|
alerts_list = get_alerts(conn2, branch_id)
|
||||||
|
conn2.close()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'stock': stock,
|
||||||
|
'entradas': op_counts.get('PURCHASE', 0),
|
||||||
|
'salidas': op_counts.get('SALE', 0),
|
||||||
|
'traspasos': op_counts.get('TRANSFER', 0),
|
||||||
|
'ajustes': op_counts.get('ADJUST', 0),
|
||||||
|
'conteos': physical,
|
||||||
|
'alertas': len(alerts_list)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
# ─── Alerts and History ────────────────────────
|
# ─── Alerts and History ────────────────────────
|
||||||
|
|
||||||
@inventory_bp.route('/alerts', methods=['GET'])
|
@inventory_bp.route('/alerts', methods=['GET'])
|
||||||
|
|||||||
@@ -238,25 +238,25 @@
|
|||||||
<!-- Tabs Row -->
|
<!-- Tabs Row -->
|
||||||
<div class="tabs-row" role="tablist" aria-label="Módulos de Inventario">
|
<div class="tabs-row" role="tablist" aria-label="Módulos de Inventario">
|
||||||
<button class="tab-btn is-active" role="tab" aria-selected="true" aria-controls="panel-stock" onclick="switchTab('stock')">
|
<button class="tab-btn is-active" role="tab" aria-selected="true" aria-controls="panel-stock" onclick="switchTab('stock')">
|
||||||
Stock Actual <span class="tab-btn__badge">4,817</span>
|
Stock Actual <span class="tab-btn__badge" id="badge-stock">0</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-entradas" onclick="switchTab('entradas')">
|
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-entradas" onclick="switchTab('entradas')">
|
||||||
Entradas <span class="tab-btn__badge">14</span>
|
Entradas <span class="tab-btn__badge" id="badge-entradas">0</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-salidas" onclick="switchTab('salidas')">
|
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-salidas" onclick="switchTab('salidas')">
|
||||||
Salidas
|
Salidas <span class="tab-btn__badge" id="badge-salidas">0</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-traspasos" onclick="switchTab('traspasos')">
|
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-traspasos" onclick="switchTab('traspasos')">
|
||||||
Traspasos <span class="tab-btn__badge">3</span>
|
Traspasos <span class="tab-btn__badge" id="badge-traspasos">0</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-ajustes" onclick="switchTab('ajustes')">
|
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-ajustes" onclick="switchTab('ajustes')">
|
||||||
Ajustes
|
Ajustes <span class="tab-btn__badge" id="badge-ajustes">0</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-conteos" onclick="switchTab('conteos')">
|
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-conteos" onclick="switchTab('conteos')">
|
||||||
Conteos
|
Conteos <span class="tab-btn__badge" id="badge-conteos">0</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-alertas" onclick="switchTab('alertas')">
|
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-alertas" onclick="switchTab('alertas')">
|
||||||
Alertas <span class="tab-btn__badge tab-btn__badge--alert">23</span>
|
Alertas <span class="tab-btn__badge tab-btn__badge--alert" id="badge-alertas">0</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -821,5 +821,34 @@
|
|||||||
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
|
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
|
||||||
<script src="/pos/static/js/pwa-install.js" defer></script>
|
<script src="/pos/static/js/pwa-install.js" defer></script>
|
||||||
<script src="/pos/static/js/chat.js" defer></script>
|
<script src="/pos/static/js/chat.js" defer></script>
|
||||||
|
<script>
|
||||||
|
// Load inventory stats for tab badges
|
||||||
|
(async function loadInventoryStats() {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
if (!token) return;
|
||||||
|
const res = await fetch('/pos/api/inventory/stats', {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + token }
|
||||||
|
});
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
const map = {
|
||||||
|
'badge-stock': data.stock,
|
||||||
|
'badge-entradas': data.entradas,
|
||||||
|
'badge-salidas': data.salidas,
|
||||||
|
'badge-traspasos': data.traspasos,
|
||||||
|
'badge-ajustes': data.ajustes,
|
||||||
|
'badge-conteos': data.conteos,
|
||||||
|
'badge-alertas': data.alertas
|
||||||
|
};
|
||||||
|
for (const [id, value] of Object.entries(map)) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.textContent = value || 0;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load inventory stats:', e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user