From 1b87613c47252c674bd3d2c677433bce6c14acec Mon Sep 17 00:00:00 2001 From: DaviMBDev Date: Sun, 31 May 2026 13:45:07 -0300 Subject: [PATCH] feat:dados em tempo real na home --- frontend/src/app/(admin)/(tabs)/adminHome.tsx | 333 ++++++++++-------- frontend/src/app/(agent)/(tabs)/agentHome.tsx | 196 ++++++++--- frontend/src/app/(agent)/(tabs)/tickets.tsx | 11 +- .../src/app/(agent)/ticket/details/[id].tsx | 10 +- frontend/src/contexts/AuthContext.tsx | 4 +- 5 files changed, 345 insertions(+), 209 deletions(-) diff --git a/frontend/src/app/(admin)/(tabs)/adminHome.tsx b/frontend/src/app/(admin)/(tabs)/adminHome.tsx index a201156..316bf28 100644 --- a/frontend/src/app/(admin)/(tabs)/adminHome.tsx +++ b/frontend/src/app/(admin)/(tabs)/adminHome.tsx @@ -1,10 +1,11 @@ -import React, { useState } from "react"; -import { View, Text, ScrollView, TouchableOpacity, TouchableWithoutFeedback } from "react-native"; +import React, { useState, useEffect } from "react"; +import { View, Text, ScrollView, TouchableOpacity, TouchableWithoutFeedback, ActivityIndicator } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { MaterialIcons, Feather } from "@expo/vector-icons"; import { useRouter } from "expo-router"; import { NotificationDropdown } from "../../../components/notifications/NotificationDropdown"; import { useNotifications } from "@/contexts/NotificationContext"; +import api from "@/services/api"; const recentTickets = [ { @@ -38,6 +39,54 @@ export default function Home() { const [showNotifications, setShowNotifications] = useState(false); const { unreadCount } = useNotifications(); + const [metrics, setMetrics] = useState(null); + const [loading, setLoading] = useState(true); + const [categories, setCategories] = useState([]); + const [selectedCategory, setSelectedCategory] = useState(''); + + const fetchMetrics = async (categoryId: string) => { + setLoading(true); + try { + const url = categoryId ? `/tickets/metrics?categoryId=${categoryId}` : '/tickets/metrics'; + const response = await api.get(url); + setMetrics(response.data); + } catch (error) { + console.error("Erro ao buscar métricas:", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + const fetchCategories = async () => { + try { + const response = await api.get('/category'); + setCategories(response.data); + } catch (error) { + console.error("Erro ao buscar categorias:", error); + } + }; + fetchCategories(); + fetchMetrics(''); + }, []); + + const handleSelectCategory = (catId: string) => { + setSelectedCategory(catId); + fetchMetrics(catId); + }; + + const formatAverageTime = (avgTime: any) => { + if (!avgTime || avgTime.count === 0) return "--"; + if (avgTime.avgDays >= 1) return `${avgTime.avgDays.toFixed(1)}d`; + if (avgTime.avgHours >= 1) return `${avgTime.avgHours.toFixed(1)}h`; + return `${Math.round(avgTime.avgMinutes)}m`; + }; + + const total = metrics?.totalTickets || 0; + const openPercent = total ? Math.round(((metrics?.openTickets || 0) / total) * 100) : 0; + const closedPercent = total ? Math.round(((metrics?.closedTickets || 0) / total) * 100) : 0; + const escalatedPercent = total ? Math.round(((metrics?.escalatedTickets || 0) / total) * 100) : 0; + return ( setShowNotifications(false)}> @@ -70,52 +119,136 @@ export default function Home() { setShowNotifications(false)} /> )} + + {/* Filtro de Setor */} + {categories.length > 0 && ( + + + handleSelectCategory('')} + style={{ + backgroundColor: selectedCategory === '' ? '#f97316' : '#f9fafb', + borderColor: selectedCategory === '' ? '#f97316' : '#e5e7eb', + }} + className="px-4 py-2 rounded-full mx-1 border" + > + + Geral da Empresa + + + {categories.map((cat) => { + const catId = cat.id || cat._id; + const isSelected = selectedCategory === catId; + return ( + handleSelectCategory(catId)} + style={{ + backgroundColor: isSelected ? '#f97316' : '#f9fafb', + borderColor: isSelected ? '#f97316' : '#e5e7eb', + }} + className="px-4 py-2 rounded-full mx-1 border" + > + + {cat.name} + + + ); + })} + + + )} - {/* Cards principais */} - - - - TOTAL DE CHAMADOS - - +12.5% - - - 1.284 - - - + {loading ? ( + + - + ) : ( + <> + {/* Cards principais */} + + + + TOTAL DE CHAMADOS + + + + {metrics?.totalTickets || 0} + + - - - - RESOLVIDOS - - +5.2% - - - 942 - - - - - + + + + RESOLVIDOS + + + + {metrics?.closedTickets || 0} + + - - - - TEMPO MÉDIO - - -8.1% - - - 4.2h - - - - - + + + + TEMPO MÉDIO + + + + {formatAverageTime(metrics?.averageResolutionTime)} + + + + {/* Status */} + + + Status dos Chamados + + + {/* Barra proporcional */} + + + + + + + {/* Legenda */} + + + + + + Em Aberto + + {openPercent}% + + + + + + Resolvidos + + {closedPercent}% + + + + + + Críticos + + {escalatedPercent}% + + + + + + )} {/* Acesso Rápido */} @@ -153,118 +286,6 @@ export default function Home() { - {/* Chamados por setor */} - - - - Chamados por Setor - - - últimos 30 dias - - - - - {["S1", "S2", "S3", "Dev", "Outros"].map((item) => ( - - {item} - - ))} - - - - {/* Status */} - - - Status dos Chamados - - - {/* Barra proporcional */} - - - - - - - {/* Legenda */} - - - - - - Em Aberto - - 70% - - - - - - Resolvidos - - 25% - - - - - - Críticos - - 5% - - - - - - {/* Recentes */} - - - Chamados Recentes - - - - Ver todos - - - - - - {recentTickets.map((item) => ( - - - - - - - - - - {item.title} - - - {item.subtitle} - - - - - - - {item.status} - - - - ))} - diff --git a/frontend/src/app/(agent)/(tabs)/agentHome.tsx b/frontend/src/app/(agent)/(tabs)/agentHome.tsx index 33936af..e343275 100644 --- a/frontend/src/app/(agent)/(tabs)/agentHome.tsx +++ b/frontend/src/app/(agent)/(tabs)/agentHome.tsx @@ -1,9 +1,12 @@ -import React, { useState } from "react"; -import { View, Text, FlatList, TouchableOpacity, TouchableWithoutFeedback } from "react-native"; +import React, { useState, useEffect } from "react"; +import { View, Text, FlatList, TouchableOpacity, TouchableWithoutFeedback, ActivityIndicator, ScrollView } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { Feather, MaterialIcons } from "@expo/vector-icons"; +import { useRouter } from "expo-router"; import { NotificationDropdown } from "../../../components/notifications/NotificationDropdown"; import { useNotifications } from "@/contexts/NotificationContext"; +import { useAuth } from "@/contexts/AuthContext"; +import api from "@/services/api"; const recentTickets = [ { @@ -33,8 +36,52 @@ const recentTickets = [ ]; export default function Dashboard() { + const router = useRouter(); const [showNotifications, setShowNotifications] = useState(false); const { unreadCount } = useNotifications(); + const { user } = useAuth(); + + const [metrics, setMetrics] = useState(null); + const [loading, setLoading] = useState(true); + const [categories, setCategories] = useState([]); + const [selectedCategory, setSelectedCategory] = useState(''); + + const fetchMetrics = async (categoryId: string) => { + setLoading(true); + try { + const url = categoryId ? `/tickets/metrics?categoryId=${categoryId}` : '/tickets/metrics'; + const response = await api.get(url); + setMetrics(response.data); + } catch (error) { + console.error("Erro ao buscar métricas:", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + const fetchCategories = async () => { + try { + const response = await api.get('/category'); + // Filter categories based on user's assigned categories + const userCats = user?.categories || []; + const filtered = response.data.filter((cat: any) => { + const catId = cat.id || cat._id; + return userCats.includes(catId); + }); + setCategories(filtered); + } catch (error) { + console.error("Erro ao buscar categorias:", error); + } + }; + fetchCategories(); + fetchMetrics(''); + }, [user]); + + const handleSelectCategory = (catId: string) => { + setSelectedCategory(catId); + fetchMetrics(catId); + }; return ( @@ -47,10 +94,10 @@ export default function Dashboard() { - Olá, Atendente + Olá, {user?.name || 'Atendente'} - Setor: Suporte Técnico + Setor de Atendimento @@ -70,63 +117,116 @@ export default function Dashboard() { )} - {/* Cards */} - - - {/* Pendentes */} - - - PENDENTES - - - 12 - + {/* Filtro de Setor */} + {categories.length > 0 && ( + + + handleSelectCategory('')} + style={{ + backgroundColor: selectedCategory === '' ? '#f97316' : '#f9fafb', + borderColor: selectedCategory === '' ? '#f97316' : '#e5e7eb', + }} + className="px-4 py-2 rounded-full mx-1 border" + > + + Meus Setores + + + {categories.map((cat) => { + const catId = cat.id || cat._id; + const isSelected = selectedCategory === catId; + return ( + handleSelectCategory(catId)} + style={{ + backgroundColor: isSelected ? '#f97316' : '#f9fafb', + borderColor: isSelected ? '#f97316' : '#e5e7eb', + }} + className="px-4 py-2 rounded-full mx-1 border" + > + + {cat.name} + + + ); + })} + - + )} - {/* Em atendimento */} - - - EM ATENDIMENTO - - - 5 + {/* Cards */} + {loading ? ( + + - - - {/* Escalonados */} - - - ESCALONADOS - - - 3 + ) : ( + - - + {/* Pendentes */} + + + PENDENTES + + + {metrics?.openTickets || 0} + + + - {/* Resolvidos */} - - - RESOLVIDOS - - - 28 - - - + {/* Em atendimento */} + + + EM ATENDIMENTO + + + {metrics?.inProgressTickets || 0} + + + + {/* Escalonados */} + + + ESCALONADOS + + + {metrics?.escalatedTickets || 0} + + + + {/* Resolvidos */} + + + RESOLVIDOS + + + {metrics?.closedTickets || 0} + + + + + + )} - {/* Chamados */} Chamados Recentes - - Ver todos - + router.push('/(agent)/(tabs)/tickets')}> + + Ver todos + + { categoryDictionary[catId] = cat.name; }); const formattedTickets = ticketsData.map((t: any) => { - const rawCategory = t.category || t.props?.category; + const catObj = t.category || t.props?.category; + + let categoryName = 'Sem Categoria'; + if (typeof catObj === 'string') { + categoryName = categoryDictionary[catObj] || catObj; + } else if (catObj && typeof catObj === 'object') { + categoryName = catObj.name || 'Sem Categoria'; + } return { ...t, - category: categoryDictionary[rawCategory] || rawCategory || 'Sem Categoria' + category: categoryName }; }); diff --git a/frontend/src/app/(agent)/ticket/details/[id].tsx b/frontend/src/app/(agent)/ticket/details/[id].tsx index 033a08f..5e65a02 100644 --- a/frontend/src/app/(agent)/ticket/details/[id].tsx +++ b/frontend/src/app/(agent)/ticket/details/[id].tsx @@ -267,8 +267,14 @@ export default function TicketDetails() { ); } - const categoryId = ticket?.category; - const categoryName = categories.find(cat => (cat.id || cat._id) === categoryId)?.name || categoryId || 'Geral'; + const rawCat = ticket?.category; + let categoryName = 'Geral'; + + if (typeof rawCat === 'string') { + categoryName = categories.find(cat => (cat.id || cat._id) === rawCat)?.name || rawCat; + } else if (rawCat && typeof rawCat === 'object') { + categoryName = rawCat.name || 'Geral'; + } return ( diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index f732f47..60386d2 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -12,6 +12,7 @@ export type User = { role: UserRole; token: string; profileImage?: string | null; + categories?: string[]; } export type AuthContextData = { @@ -56,7 +57,8 @@ export const AuthProvider = ({ children }: { children: React.ReactNode}) => { name: decoded.name || 'Usuário', email: decoded.email, role: decoded.role.toLowerCase() as UserRole, - token: token + token: token, + categories: decoded.categories || [] } await storage.setItem('prodesk_token', token)