From c47a64d8315dd4bb9f5eb778b8e581adb9273fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Mon, 25 May 2026 11:26:00 -0300 Subject: [PATCH] feat: display available stock balance on products page based on n8n inventory updates --- backend/index.js | 66 +++++++++++++++++++++++++++++++++++++++ src/components/Layout.tsx | 8 +++-- src/dataService.ts | 19 +++++++++++ src/pages/Products.tsx | 33 ++++++++++++++++++-- 4 files changed, 120 insertions(+), 6 deletions(-) diff --git a/backend/index.js b/backend/index.js index 57d5c49..89b7737 100644 --- a/backend/index.js +++ b/backend/index.js @@ -45,6 +45,16 @@ const initDB = async () => { await pool.query(`CREATE UNIQUE INDEX IF NOT EXISTS unique_order_product ON orders (pedido_id, produto_id);`).catch(err => { console.error("Notice: Could not create unique index (might already exist or there are duplicates):", err.message); }); + + await pool.query(` + CREATE TABLE IF NOT EXISTS stock ( + produto_id VARCHAR(100) PRIMARY KEY, + nome TEXT, + saldo INTEGER DEFAULT 0, + delta_estoque INTEGER DEFAULT 0, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); console.log("Database initialized successfully."); } catch (err) { @@ -106,6 +116,17 @@ app.get('/api/data', verifyToken, async (req, res) => { } }); +// GET stock (for the frontend) +app.get('/api/stock', verifyToken, async (req, res) => { + try { + const result = await pool.query('SELECT * FROM stock'); + res.json(result.rows); + } catch (error) { + console.error("Error fetching stock:", error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}); + // POST data (for n8n) - Protected by API_KEY internally or via middleware if needed // Leaving it as it was, checking API_KEY manually? Wait, the previous version didn't actually use 'authenticate' middleware on the POST! // Let's add the authenticate middleware to the POST endpoint. @@ -178,6 +199,51 @@ app.post('/api/data', authenticateAPIKey, async (req, res) => { })(); }); +// POST stock (for n8n) +app.post('/api/stock', authenticateAPIKey, async (req, res) => { + res.status(201).json({ message: 'Stock data received, processing in background' }); + + const newData = req.body; + const payload = Array.isArray(newData) ? newData : [newData]; + + (async () => { + const client = await pool.connect(); + try { + await client.query('BEGIN'); + + const insertQuery = ` + INSERT INTO stock (produto_id, nome, saldo, delta_estoque) + VALUES ($1, $2, $3, $4) + ON CONFLICT (produto_id) DO UPDATE SET + nome = EXCLUDED.nome, + saldo = EXCLUDED.saldo, + delta_estoque = EXCLUDED.delta_estoque, + updated_at = CURRENT_TIMESTAMP + `; + + for (const item of payload) { + const idProduto = item.idProduto || item.ID_Produto || ''; + if (!idProduto) continue; + + const values = [ + String(idProduto), + item.nome || item.Descricao_Produto || 'Unknown', + parseInt(item.saldo) || 0, + parseInt(item.delta_estoque) || 0 + ]; + await client.query(insertQuery, values); + } + + await client.query('COMMIT'); + } catch (error) { + await client.query('ROLLBACK'); + console.error("Database stock insert error:", error); + } finally { + client.release(); + } + })(); +}); + app.listen(PORT, '0.0.0.0', () => { console.log(`Nexstar Backend running at http://localhost:${PORT}`); console.log(`Endpoint for n8n: POST http://localhost:${PORT}/api/data`); diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 923ec27..c15009a 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { Outlet, Link, useLocation } from 'react-router-dom'; import { LayoutDashboard, Users, BarChart3, ChevronLeft, ChevronRight, Package, Loader2, LogOut } from 'lucide-react'; import type { DateRange, OrderData } from '../types'; -import { fetchData, logout } from '../dataService'; +import { fetchData, fetchStock, logout } from '../dataService'; const Layout = () => { const location = useLocation(); @@ -25,6 +25,7 @@ const Layout = () => { }); const [ordersData, setOrdersData] = useState([]); + const [stockData, setStockData] = useState([]); const [isLoading, setIsLoading] = useState(true); const [refreshInterval, setRefreshInterval] = useState(() => { const saved = localStorage.getItem('nexstar_refresh_interval'); @@ -33,8 +34,9 @@ const Layout = () => { const loadData = async (showLoading = false) => { if (showLoading) setIsLoading(true); - const data = await fetchData(); + const [data, stock] = await Promise.all([fetchData(), fetchStock()]); setOrdersData(data); + setStockData(stock); if (showLoading) setIsLoading(false); }; @@ -146,7 +148,7 @@ const Layout = () => { ) : ( - + )} diff --git a/src/dataService.ts b/src/dataService.ts index 4d0aeee..23f8edb 100644 --- a/src/dataService.ts +++ b/src/dataService.ts @@ -33,6 +33,25 @@ export const isAuthenticated = (): boolean => { return !!localStorage.getItem('auth_token'); }; +export const fetchStock = async (): Promise => { + try { + const token = localStorage.getItem('auth_token'); + const response = await fetch(`${API_URL}/stock`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + if (response.status === 401 || response.status === 403) { + logout(); + return []; + } + if (!response.ok) return []; + return await response.json(); + } catch (error) { + return []; + } +}; + export const fetchData = async (): Promise => { try { const token = localStorage.getItem('auth_token'); diff --git a/src/pages/Products.tsx b/src/pages/Products.tsx index 615ef9c..658269b 100644 --- a/src/pages/Products.tsx +++ b/src/pages/Products.tsx @@ -6,10 +6,11 @@ import type { OrderData, DateRange } from '../types'; import { parseOrderDate, exportToCSV } from '../dataService'; const Products = () => { - const { dateRange, setDateRange, ordersData } = useOutletContext<{ + const { dateRange, setDateRange, ordersData, stockData } = useOutletContext<{ dateRange: DateRange, setDateRange: (range: DateRange) => void, ordersData: OrderData[], + stockData: any[], refreshInterval: number, setRefreshInterval: (interval: number) => void, loadData: (showLoading?: boolean) => void @@ -27,7 +28,21 @@ const Products = () => { const productsData = useMemo(() => { const orders = ordersData; - const productMap: Record = {}; + const productMap: Record = {}; + + // Initialize with stock data + if (stockData && Array.isArray(stockData)) { + stockData.forEach(item => { + productMap[item.produto_id] = { + id: item.produto_id, + name: item.nome, + totalSold: 0, + revenue: 0, + lastPrice: 0, + stock: item.saldo || 0 + }; + }); + } orders.forEach(order => { const orderDate = parseOrderDate(order.Data_Pedido); @@ -39,10 +54,16 @@ const Products = () => { name: order.Descricao_Produto.split(' TAMANHO')[0], totalSold: 0, revenue: 0, - lastPrice: order.Valor_Unitario + lastPrice: order.Valor_Unitario, + stock: 0 }; } + // Update name if we didn't get it from stock + if (productMap[order.ID_Produto].name === 'Unknown' || !productMap[order.ID_Produto].name) { + productMap[order.ID_Produto].name = order.Descricao_Produto.split(' TAMANHO')[0]; + } + productMap[order.ID_Produto].totalSold += order.Quantidade; productMap[order.ID_Produto].revenue += (order.Quantidade * order.Valor_Unitario); productMap[order.ID_Produto].lastPrice = order.Valor_Unitario; @@ -118,6 +139,7 @@ const Products = () => { ID Produto Descrição Total Vendido + Estoque Receita Gerada Ações @@ -136,6 +158,11 @@ const Products = () => { {product.totalSold} un. + + 10 ? 'text-emerald-500' : product.stock > 0 ? 'text-amber-500' : 'text-red-500'}`}> + {product.stock} un. + + {formatCurrency(product.revenue)}