perf: move date filtering to backend to massively reduce payload size and implement smart polling for instant updates without lag
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 54s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 54s
This commit is contained in:
@@ -92,11 +92,37 @@ const formatRow = (row) => ({
|
|||||||
ID_Pedido: row.pedido_id
|
ID_Pedido: row.pedido_id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const parseOrderDate = (dateStr) => {
|
||||||
|
if (!dateStr) return new Date(0);
|
||||||
|
if (dateStr.includes('T')) return new Date(dateStr);
|
||||||
|
const parts = dateStr.split(/[-/]/);
|
||||||
|
if (parts.length === 3) {
|
||||||
|
if (parts[0].length === 4) {
|
||||||
|
return new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
|
||||||
|
} else {
|
||||||
|
return new Date(Number(parts[2]), Number(parts[1]) - 1, Number(parts[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const fallback = new Date(dateStr);
|
||||||
|
return isNaN(fallback.getTime()) ? new Date(0) : fallback;
|
||||||
|
};
|
||||||
|
|
||||||
// GET data (for the frontend)
|
// GET data (for the frontend)
|
||||||
app.get('/api/data', verifyToken, async (req, res) => {
|
app.get('/api/data', verifyToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = await pool.query('SELECT * FROM orders ORDER BY id DESC');
|
const result = await pool.query('SELECT * FROM orders ORDER BY id DESC');
|
||||||
const formattedData = result.rows.map(formatRow);
|
let rows = result.rows;
|
||||||
|
|
||||||
|
if (req.query.start && req.query.end) {
|
||||||
|
const startDate = new Date(req.query.start);
|
||||||
|
const endDate = new Date(req.query.end);
|
||||||
|
rows = rows.filter(row => {
|
||||||
|
const orderDate = parseOrderDate(row.data_pedido);
|
||||||
|
return orderDate >= startDate && orderDate <= endDate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedData = rows.map(formatRow);
|
||||||
res.json(formattedData);
|
res.json(formattedData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching data:", error);
|
console.error("Error fetching data:", error);
|
||||||
@@ -104,6 +130,16 @@ app.get('/api/data', verifyToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Smart Polling endpoint to check for updates instantly
|
||||||
|
app.get('/api/last-update', verifyToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query('SELECT MAX(id) as max_id FROM orders');
|
||||||
|
res.json({ max_id: result.rows[0].max_id || 0 });
|
||||||
|
} catch (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.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } 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, fetchLastUpdate, logout } from '../dataService';
|
||||||
|
|
||||||
const Layout = () => {
|
const Layout = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -18,9 +18,10 @@ const Layout = () => {
|
|||||||
return { start: new Date(parsed.start), end: new Date(parsed.end) };
|
return { start: new Date(parsed.start), end: new Date(parsed.end) };
|
||||||
} catch (e) { console.error(e); }
|
} catch (e) { console.error(e); }
|
||||||
}
|
}
|
||||||
const end = new Date();
|
|
||||||
const start = new Date();
|
const start = new Date();
|
||||||
start.setMonth(start.getMonth() - 1);
|
start.setHours(0,0,0,0);
|
||||||
|
const end = new Date();
|
||||||
|
end.setHours(23,59,59,999);
|
||||||
return { start, end };
|
return { start, end };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -30,27 +31,41 @@ const Layout = () => {
|
|||||||
const saved = localStorage.getItem('nexstar_refresh_interval');
|
const saved = localStorage.getItem('nexstar_refresh_interval');
|
||||||
return saved ? Number(saved) : 0;
|
return saved ? Number(saved) : 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const lastUpdateIdRef = useRef<number>(0);
|
||||||
|
|
||||||
const loadData = async (showLoading = false) => {
|
const loadData = async (showLoading = false) => {
|
||||||
if (showLoading) setIsLoading(true);
|
if (showLoading) setIsLoading(true);
|
||||||
const data = await fetchData();
|
const data = await fetchData(dateRange.start, dateRange.end);
|
||||||
setOrdersData(data);
|
setOrdersData(data);
|
||||||
|
|
||||||
|
// Also update the last known ID so we don't double-fetch immediately
|
||||||
|
const maxId = await fetchLastUpdate();
|
||||||
|
lastUpdateIdRef.current = maxId;
|
||||||
|
|
||||||
if (showLoading) setIsLoading(false);
|
if (showLoading) setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fetch new data immediately whenever the selected date range changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData(true);
|
loadData(true);
|
||||||
}, []);
|
}, [dateRange]);
|
||||||
|
|
||||||
|
// Smart Polling Logic
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (refreshInterval === 0) return;
|
if (refreshInterval === 0) return;
|
||||||
|
|
||||||
const intervalId = setInterval(() => {
|
const checkUpdate = async () => {
|
||||||
loadData(false);
|
const latestId = await fetchLastUpdate();
|
||||||
}, refreshInterval);
|
if (latestId > lastUpdateIdRef.current) {
|
||||||
|
lastUpdateIdRef.current = latestId;
|
||||||
|
loadData(false); // Background fetch of the new payload
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const intervalId = setInterval(checkUpdate, refreshInterval);
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, [refreshInterval]);
|
}, [refreshInterval, dateRange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('nexstar_refresh_interval', refreshInterval.toString());
|
localStorage.setItem('nexstar_refresh_interval', refreshInterval.toString());
|
||||||
|
|||||||
@@ -33,10 +33,35 @@ export const isAuthenticated = (): boolean => {
|
|||||||
return !!localStorage.getItem('auth_token');
|
return !!localStorage.getItem('auth_token');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchData = async (): Promise<OrderData[]> => {
|
export const fetchLastUpdate = async (): Promise<number> => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('auth_token');
|
const token = localStorage.getItem('auth_token');
|
||||||
const response = await fetch(`${API_URL}/data`, {
|
const response = await fetch(`${API_URL}/last-update`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (response.status === 401 || response.status === 403) {
|
||||||
|
logout();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!response.ok) return 0;
|
||||||
|
const data = await response.json();
|
||||||
|
return data.max_id || 0;
|
||||||
|
} catch (error) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchData = async (start?: Date, end?: Date): Promise<OrderData[]> => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('auth_token');
|
||||||
|
let url = `${API_URL}/data`;
|
||||||
|
if (start && end) {
|
||||||
|
url += `?start=${start.toISOString()}&end=${end.toISOString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`
|
'Authorization': `Bearer ${token}`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user