Compare commits

..

2 Commits

Author SHA1 Message Date
Cauê Faleiros
1f8baabf69 style: remove dynamic color coding from stock column to match table styling
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m26s
2026-05-25 11:35:15 -03:00
Cauê Faleiros
c47a64d831 feat: display available stock balance on products page based on n8n inventory updates 2026-05-25 11:26:00 -03:00
4 changed files with 120 additions and 6 deletions

View File

@@ -46,6 +46,16 @@ const initDB = async () => {
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) {
console.error("Failed to initialize database:", 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`);

View File

@@ -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<OrderData[]>([]);
const [stockData, setStockData] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [refreshInterval, setRefreshInterval] = useState<number>(() => {
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 = () => {
<Loader2 className="w-8 h-8 text-brand-primary animate-spin" />
</div>
) : (
<Outlet context={{ dateRange, setDateRange, ordersData, refreshInterval, setRefreshInterval, loadData }} />
<Outlet context={{ dateRange, setDateRange, ordersData, stockData, refreshInterval, setRefreshInterval, loadData }} />
)}
</div>
</main>

View File

@@ -33,6 +33,25 @@ export const isAuthenticated = (): boolean => {
return !!localStorage.getItem('auth_token');
};
export const fetchStock = async (): Promise<any[]> => {
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<OrderData[]> => {
try {
const token = localStorage.getItem('auth_token');

View File

@@ -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<string, { id: string, name: string, totalSold: number, revenue: number, lastPrice: number }> = {};
const productMap: Record<string, { id: string, name: string, totalSold: number, revenue: number, lastPrice: number, stock: number }> = {};
// 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 = () => {
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">ID Produto</th>
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Descrição</th>
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Total Vendido</th>
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Estoque</th>
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Receita Gerada</th>
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px] text-right">Ações</th>
</tr>
@@ -136,6 +158,11 @@ const Products = () => {
<span className="font-bold text-zinc-900 dark:text-dark-text">{product.totalSold} un.</span>
</div>
</td>
<td className="px-6 py-2.5">
<span className="font-bold text-zinc-900 dark:text-dark-text">
{product.stock} un.
</span>
</td>
<td className="px-6 py-2.5 text-brand-primary font-bold">{formatCurrency(product.revenue)}</td>
<td className="px-6 py-2.5 text-right">
<Link