From b986eafb98e6431cce3a7493e374e47d48385135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Thu, 7 May 2026 14:52:45 -0300 Subject: [PATCH] feat: replace SSE with Grafana-style client polling and rich date presets --- backend/index.js | 32 ------ src/components/DateRangePicker.tsx | 155 +++++++++++++++-------------- src/components/Layout.tsx | 50 ++++------ src/pages/Dashboard.tsx | 17 +++- src/pages/ProductDetails.tsx | 18 +++- src/pages/Products.tsx | 17 +++- 6 files changed, 145 insertions(+), 144 deletions(-) diff --git a/backend/index.js b/backend/index.js index a4d5757..aa12b77 100644 --- a/backend/index.js +++ b/backend/index.js @@ -61,32 +61,6 @@ const verifyToken = (req, res, next) => { }); }; -const sseClients = new Set(); - -// SSE Endpoint for real-time updates -app.get('/api/stream', (req, res) => { - res.setHeader('Content-Type', 'text/event-stream'); - res.setHeader('Cache-Control', 'no-cache'); - res.setHeader('Connection', 'keep-alive'); - res.setHeader('X-Accel-Buffering', 'no'); // Bypass Nginx Proxy Manager buffering - res.flushHeaders(); - - // Send initial connection event - res.write('data: {"connected": true}\n\n'); - - // Keep connection alive through Nginx/Cloudflare - const keepAlive = setInterval(() => { - res.write(':\n\n'); // SSE comment to prevent idle timeout - }, 15000); - - sseClients.add(res); - - req.on('close', () => { - clearInterval(keepAlive); - sseClients.delete(res); - }); -}); - // Login Endpoint app.post('/api/login', (req, res) => { const { email, password } = req.body; @@ -169,12 +143,6 @@ app.post('/api/data', authenticateAPIKey, async (req, res) => { } await client.query('COMMIT'); - - // Broadcast update to all connected SSE clients - const broadcastMessage = `data: {"type": "update"}\n\n`; - for (const clientRes of sseClients) { - clientRes.write(broadcastMessage); - } } catch (error) { await client.query('ROLLBACK'); console.error("Database insert error:", error); diff --git a/src/components/DateRangePicker.tsx b/src/components/DateRangePicker.tsx index 751c688..9ffb720 100644 --- a/src/components/DateRangePicker.tsx +++ b/src/components/DateRangePicker.tsx @@ -1,96 +1,101 @@ -import React, { useRef } from 'react'; -import { Calendar } from 'lucide-react'; +import React, { useState } from 'react'; +import { Calendar, RefreshCw, ChevronDown } from 'lucide-react'; import type { DateRange } from '../types'; interface DateRangePickerProps { dateRange: DateRange; onChange: (range: DateRange) => void; + refreshInterval: number; + setRefreshInterval: (interval: number) => void; + onManualRefresh: () => void; } -const DateRangePicker: React.FC = ({ dateRange, onChange }) => { - const startRef = useRef(null); - const endRef = useRef(null); +const PRESETS = [ + { label: 'Hoje', getRange: () => { const d = new Date(); d.setHours(0,0,0,0); return { start: d, end: new Date() }; } }, + { label: 'Ontem', getRange: () => { const d = new Date(); d.setDate(d.getDate() - 1); d.setHours(0,0,0,0); const end = new Date(d); end.setHours(23,59,59,999); return { start: d, end }; } }, + { label: 'Últimos 7 dias', getRange: () => { const end = new Date(); const start = new Date(); start.setDate(start.getDate() - 7); start.setHours(0,0,0,0); return { start, end }; } }, + { label: 'Últimos 30 dias', getRange: () => { const end = new Date(); const start = new Date(); start.setDate(start.getDate() - 30); start.setHours(0,0,0,0); return { start, end }; } }, + { label: 'Este Mês', getRange: () => { const end = new Date(); const start = new Date(end.getFullYear(), end.getMonth(), 1); return { start, end }; } }, + { label: 'Mês Passado', getRange: () => { const d = new Date(); const start = new Date(d.getFullYear(), d.getMonth() - 1, 1); const end = new Date(d.getFullYear(), d.getMonth(), 0, 23, 59, 59, 999); return { start, end }; } }, + { label: 'Últimos 90 dias', getRange: () => { const end = new Date(); const start = new Date(); start.setDate(start.getDate() - 90); start.setHours(0,0,0,0); return { start, end }; } }, + { label: 'Este Ano', getRange: () => { const end = new Date(); const start = new Date(end.getFullYear(), 0, 1); return { start, end }; } }, + { label: 'Todo o Período', getRange: () => { const end = new Date(); const start = new Date(2000, 0, 1); return { start, end }; } }, +]; - const formatDateForInput = (date: Date) => { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; - }; +const REFRESH_OPTIONS = [ + { label: 'Desligado', value: 0 }, + { label: '5s', value: 5000 }, + { label: '10s', value: 10000 }, + { label: '30s', value: 30000 }, + { label: '1m', value: 60000 }, + { label: '5m', value: 300000 }, +]; + +const DateRangePicker: React.FC = ({ dateRange, onChange, refreshInterval, setRefreshInterval, onManualRefresh }) => { + const [isPresetOpen, setIsPresetOpen] = useState(false); const formatShortDate = (date: Date) => { return date.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: '2-digit' }); }; - const parseLocalDate = (value: string) => { - if (!value) return null; - const [year, month, day] = value.split('-'); - return new Date(parseInt(year), parseInt(month) - 1, parseInt(day)); - }; - - const handleStartChange = (e: React.ChangeEvent) => { - const newStart = parseLocalDate(e.target.value); - if (newStart && !isNaN(newStart.getTime())) { - onChange({ ...dateRange, start: newStart }); - } - }; - - const handleEndChange = (e: React.ChangeEvent) => { - const newEnd = parseLocalDate(e.target.value); - if (newEnd && !isNaN(newEnd.getTime())) { - newEnd.setHours(23, 59, 59, 999); - onChange({ ...dateRange, end: newEnd }); - } - }; - - const openPicker = (ref: React.RefObject) => { - if (ref.current) { - try { - if ('showPicker' in HTMLInputElement.prototype) { - ref.current.showPicker(); - } else { - ref.current.focus(); - } - } catch (e) { - ref.current.focus(); - } - } - }; - return ( -
- -
-
openPicker(startRef)} +
+ {/* Date Presets Dropdown */} +
+
- até -
openPicker(endRef)} + + {formatShortDate(dateRange.start)} - {formatShortDate(dateRange.end)} + + + + {isPresetOpen && ( + <> +
setIsPresetOpen(false)}>
+
+ {PRESETS.map((preset) => ( + + ))} +
+ + )} +
+ + {/* Auto Refresh Dropdown */} +
+
+ + +
); }; -export default DateRangePicker; +export default DateRangePicker; \ No newline at end of file diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 5b9e0a7..c08d689 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -26,39 +26,29 @@ const Layout = () => { const [ordersData, setOrdersData] = useState([]); const [isLoading, setIsLoading] = useState(true); + const [refreshInterval, setRefreshInterval] = useState(0); + + const loadData = async (showLoading = false) => { + if (showLoading) setIsLoading(true); + const data = await fetchData(); + setOrdersData(data); + if (showLoading) setIsLoading(false); + }; useEffect(() => { - const loadInitial = async () => { - setIsLoading(true); - const data = await fetchData(); - setOrdersData(data); - setIsLoading(false); - }; - - loadInitial(); - - // Set up SSE for real-time updates - const API_URL = import.meta.env.VITE_API_URL || '/api'; - const sse = new EventSource(`${API_URL}/stream`); - - sse.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - if (data.type === 'update') { - fetchData().then(newData => { - setOrdersData(newData); - }); - } - } catch (e) { - console.error("SSE parse error", e); - } - }; - - return () => { - sse.close(); - }; + loadData(true); }, []); + useEffect(() => { + if (refreshInterval === 0) return; + + const intervalId = setInterval(() => { + loadData(false); + }, refreshInterval); + + return () => clearInterval(intervalId); + }, [refreshInterval]); + useEffect(() => { localStorage.setItem('nexstar_date_range', JSON.stringify({ start: dateRange.start.toISOString(), @@ -149,7 +139,7 @@ const Layout = () => {
) : ( - + )}
diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 7c6e762..614c136 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -47,7 +47,14 @@ const CustomTooltip = ({ active, payload, label, isCurrency }: any) => { }; const Dashboard = () => { - const { dateRange, setDateRange, ordersData } = useOutletContext<{ dateRange: DateRange, setDateRange: (range: DateRange) => void, ordersData: OrderData[] }>(); + const { dateRange, setDateRange, ordersData, refreshInterval, setRefreshInterval, loadData } = useOutletContext<{ + dateRange: DateRange, + setDateRange: (range: DateRange) => void, + ordersData: OrderData[], + refreshInterval: number, + setRefreshInterval: (interval: number) => void, + loadData: (showLoading?: boolean) => void + }>(); const filteredData = useMemo(() => { const orders = ordersData; @@ -112,7 +119,13 @@ const Dashboard = () => {

Visão Geral

Resumo de vendas e performance dos produtos.

- + loadData(true)} + />
diff --git a/src/pages/ProductDetails.tsx b/src/pages/ProductDetails.tsx index 29b482b..a287c58 100644 --- a/src/pages/ProductDetails.tsx +++ b/src/pages/ProductDetails.tsx @@ -20,7 +20,14 @@ const CustomTooltip = ({ active, payload, label }: any) => { const ProductDetails = () => { const { id } = useParams<{ id: string }>(); - const { dateRange, setDateRange, ordersData } = useOutletContext<{ dateRange: DateRange, setDateRange: (range: DateRange) => void, ordersData: OrderData[] }>(); + const { dateRange, setDateRange, ordersData, refreshInterval, setRefreshInterval, loadData } = useOutletContext<{ + dateRange: DateRange, + setDateRange: (range: DateRange) => void, + ordersData: OrderData[], + refreshInterval: number, + setRefreshInterval: (interval: number) => void, + loadData: (showLoading?: boolean) => void + }>(); const { productInfo, chartData, totalSold, totalRevenue } = useMemo(() => { const orders = ordersData.filter(order => order.ID_Produto === id); @@ -92,8 +99,13 @@ const ProductDetails = () => {
- - + loadData(true)} + />
diff --git a/src/pages/Products.tsx b/src/pages/Products.tsx index c955e79..4f391fd 100644 --- a/src/pages/Products.tsx +++ b/src/pages/Products.tsx @@ -6,7 +6,14 @@ import type { OrderData, DateRange } from '../types'; import { parseOrderDate } from '../dataService'; const Products = () => { - const { dateRange, setDateRange, ordersData } = useOutletContext<{ dateRange: DateRange, setDateRange: (range: DateRange) => void, ordersData: OrderData[] }>(); + const { dateRange, setDateRange, ordersData, refreshInterval, setRefreshInterval, loadData } = useOutletContext<{ + dateRange: DateRange, + setDateRange: (range: DateRange) => void, + ordersData: OrderData[], + refreshInterval: number, + setRefreshInterval: (interval: number) => void, + loadData: (showLoading?: boolean) => void + }>(); const [searchTerm, setSearchTerm] = useState(''); // Pagination state @@ -67,7 +74,13 @@ const Products = () => {
- + loadData(true)} + />