Compare commits
2 Commits
4ce1e9aedb
...
1f8baabf69
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f8baabf69 | ||
|
|
c47a64d831 |
@@ -46,6 +46,16 @@ const initDB = async () => {
|
|||||||
console.error("Notice: Could not create unique index (might already exist or there are duplicates):", err.message);
|
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.");
|
console.log("Database initialized successfully.");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to initialize database:", 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
|
// 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!
|
// 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.
|
// 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', () => {
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log(`Nexstar Backend running at http://localhost:${PORT}`);
|
console.log(`Nexstar Backend running at http://localhost:${PORT}`);
|
||||||
console.log(`Endpoint for n8n: POST http://localhost:${PORT}/api/data`);
|
console.log(`Endpoint for n8n: POST http://localhost:${PORT}/api/data`);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { Outlet, Link, useLocation } from 'react-router-dom';
|
import { Outlet, Link, useLocation } from 'react-router-dom';
|
||||||
import { LayoutDashboard, Users, BarChart3, ChevronLeft, ChevronRight, Package, Loader2, LogOut } from 'lucide-react';
|
import { LayoutDashboard, Users, BarChart3, ChevronLeft, ChevronRight, Package, Loader2, LogOut } from 'lucide-react';
|
||||||
import type { DateRange, OrderData } from '../types';
|
import type { DateRange, OrderData } from '../types';
|
||||||
import { fetchData, logout } from '../dataService';
|
import { fetchData, fetchStock, logout } from '../dataService';
|
||||||
|
|
||||||
const Layout = () => {
|
const Layout = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -25,6 +25,7 @@ const Layout = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [ordersData, setOrdersData] = useState<OrderData[]>([]);
|
const [ordersData, setOrdersData] = useState<OrderData[]>([]);
|
||||||
|
const [stockData, setStockData] = useState<any[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [refreshInterval, setRefreshInterval] = useState<number>(() => {
|
const [refreshInterval, setRefreshInterval] = useState<number>(() => {
|
||||||
const saved = localStorage.getItem('nexstar_refresh_interval');
|
const saved = localStorage.getItem('nexstar_refresh_interval');
|
||||||
@@ -33,8 +34,9 @@ const Layout = () => {
|
|||||||
|
|
||||||
const loadData = async (showLoading = false) => {
|
const loadData = async (showLoading = false) => {
|
||||||
if (showLoading) setIsLoading(true);
|
if (showLoading) setIsLoading(true);
|
||||||
const data = await fetchData();
|
const [data, stock] = await Promise.all([fetchData(), fetchStock()]);
|
||||||
setOrdersData(data);
|
setOrdersData(data);
|
||||||
|
setStockData(stock);
|
||||||
if (showLoading) setIsLoading(false);
|
if (showLoading) setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -146,7 +148,7 @@ const Layout = () => {
|
|||||||
<Loader2 className="w-8 h-8 text-brand-primary animate-spin" />
|
<Loader2 className="w-8 h-8 text-brand-primary animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Outlet context={{ dateRange, setDateRange, ordersData, refreshInterval, setRefreshInterval, loadData }} />
|
<Outlet context={{ dateRange, setDateRange, ordersData, stockData, refreshInterval, setRefreshInterval, loadData }} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -33,6 +33,25 @@ export const isAuthenticated = (): boolean => {
|
|||||||
return !!localStorage.getItem('auth_token');
|
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[]> => {
|
export const fetchData = async (): Promise<OrderData[]> => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('auth_token');
|
const token = localStorage.getItem('auth_token');
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import type { OrderData, DateRange } from '../types';
|
|||||||
import { parseOrderDate, exportToCSV } from '../dataService';
|
import { parseOrderDate, exportToCSV } from '../dataService';
|
||||||
|
|
||||||
const Products = () => {
|
const Products = () => {
|
||||||
const { dateRange, setDateRange, ordersData } = useOutletContext<{
|
const { dateRange, setDateRange, ordersData, stockData } = useOutletContext<{
|
||||||
dateRange: DateRange,
|
dateRange: DateRange,
|
||||||
setDateRange: (range: DateRange) => void,
|
setDateRange: (range: DateRange) => void,
|
||||||
ordersData: OrderData[],
|
ordersData: OrderData[],
|
||||||
|
stockData: any[],
|
||||||
refreshInterval: number,
|
refreshInterval: number,
|
||||||
setRefreshInterval: (interval: number) => void,
|
setRefreshInterval: (interval: number) => void,
|
||||||
loadData: (showLoading?: boolean) => void
|
loadData: (showLoading?: boolean) => void
|
||||||
@@ -27,7 +28,21 @@ const Products = () => {
|
|||||||
|
|
||||||
const productsData = useMemo(() => {
|
const productsData = useMemo(() => {
|
||||||
const orders = ordersData;
|
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 => {
|
orders.forEach(order => {
|
||||||
const orderDate = parseOrderDate(order.Data_Pedido);
|
const orderDate = parseOrderDate(order.Data_Pedido);
|
||||||
@@ -39,10 +54,16 @@ const Products = () => {
|
|||||||
name: order.Descricao_Produto.split(' TAMANHO')[0],
|
name: order.Descricao_Produto.split(' TAMANHO')[0],
|
||||||
totalSold: 0,
|
totalSold: 0,
|
||||||
revenue: 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].totalSold += order.Quantidade;
|
||||||
productMap[order.ID_Produto].revenue += (order.Quantidade * order.Valor_Unitario);
|
productMap[order.ID_Produto].revenue += (order.Quantidade * order.Valor_Unitario);
|
||||||
productMap[order.ID_Produto].lastPrice = 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]">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]">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]">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]">Receita Gerada</th>
|
||||||
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px] text-right">Ações</th>
|
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px] text-right">Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -136,6 +158,11 @@ const Products = () => {
|
|||||||
<span className="font-bold text-zinc-900 dark:text-dark-text">{product.totalSold} un.</span>
|
<span className="font-bold text-zinc-900 dark:text-dark-text">{product.totalSold} un.</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</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-brand-primary font-bold">{formatCurrency(product.revenue)}</td>
|
||||||
<td className="px-6 py-2.5 text-right">
|
<td className="px-6 py-2.5 text-right">
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
Reference in New Issue
Block a user