diff --git a/frontend/src/app/(admin)/(tabs)/adminHome.tsx b/frontend/src/app/(admin)/(tabs)/adminHome.tsx index a201156..8a18af0 100644 --- a/frontend/src/app/(admin)/(tabs)/adminHome.tsx +++ b/frontend/src/app/(admin)/(tabs)/adminHome.tsx @@ -1,10 +1,12 @@ -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 { useIsFocused } from "@react-navigation/native"; import { NotificationDropdown } from "../../../components/notifications/NotificationDropdown"; import { useNotifications } from "@/contexts/NotificationContext"; +import api from "@/services/api"; const recentTickets = [ { @@ -37,6 +39,58 @@ export default function Home() { const router = useRouter(); 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 selectedCategoryRef = React.useRef(selectedCategory); + selectedCategoryRef.current = selectedCategory; + + 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(); + // Fetch initial metrics once on mount + 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 ( @@ -71,51 +125,93 @@ export default function Home() { )} - {/* Cards principais */} - - - - TOTAL DE CHAMADOS - - +12.5% - - - 1.284 - - - + {/* Filtro de Setor */} + {categories.length > 0 && ( + + + handleSelectCategory('')} + style={{ + backgroundColor: selectedCategory === '' ? '#f97316' : '#ffffff', + borderColor: selectedCategory === '' ? '#f97316' : '#e5e7eb', + }} + className="px-4 py-2 rounded-full mx-1 border shadow-sm" + > + + Geral da Empresa + + + {categories.map((cat) => { + const catId = cat.id || cat._id; + const isSelected = selectedCategory === catId; + return ( + handleSelectCategory(catId)} + style={{ + backgroundColor: isSelected ? '#f97316' : '#ffffff', + borderColor: isSelected ? '#f97316' : '#e5e7eb', + }} + className="px-4 py-2 rounded-full mx-1 border shadow-sm" + > + + {cat.name} + + + ); + })} + - + )} - - - - RESOLVIDOS - - +5.2% - - - 942 - - - + {/* Cards principais */} + {loading ? ( + + - + ) : ( + <> + + + TOTAL DE CHAMADOS + {/* Mocked positive variation */} + +12.5% + + {total} + + + + - - - - TEMPO MÉDIO - - -8.1% - - - 4.2h - - - - - + + + RESOLVIDOS + +5.2% + + {metrics?.closedTickets || 0} + + + + + + + + TEMPO MÉDIO + -8.1% + + {formatAverageTime(metrics?.averageResolutionTime)} + + + + + + )} {/* Acesso Rápido */} @@ -181,9 +277,9 @@ export default function Home() { {/* Barra proporcional */} - - - + + + {/* Legenda */} @@ -194,7 +290,7 @@ export default function Home() { Em Aberto - 70% + {openPercent}% @@ -202,7 +298,7 @@ export default function Home() { Resolvidos - 25% + {closedPercent}% @@ -210,7 +306,7 @@ export default function Home() { Críticos - 5% + {escalatedPercent}% @@ -234,8 +330,7 @@ export default function Home() { {item.status} diff --git a/frontend/src/app/(agent)/(tabs)/agentHome.tsx b/frontend/src/app/(agent)/(tabs)/agentHome.tsx index 33936af..5c4f643 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 { useIsFocused } from "@react-navigation/native"; import { NotificationDropdown } from "../../../components/notifications/NotificationDropdown"; import { useNotifications } from "@/contexts/NotificationContext"; +import { useAuth } from "@/contexts/AuthContext"; +import api from "@/services/api"; const recentTickets = [ { @@ -33,13 +36,58 @@ const recentTickets = [ ]; export default function Dashboard() { + const { user } = useAuth(); 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 selectedCategoryRef = React.useRef(selectedCategory); + selectedCategoryRef.current = selectedCategory; + + 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'); + const userCats = response.data.filter((c: any) => user?.categories?.includes(c.id || c._id)); + setCategories(userCats); + } catch (error) { + console.error("Erro ao buscar categorias:", error); + } + }; + if (user?.categories) { + fetchCategories(); + } + // Fetch initial metrics + fetchMetrics(''); + }, [user]); + + const handleSelectCategory = (catId: string) => { + setSelectedCategory(catId); + fetchMetrics(catId); + }; + return ( setShowNotifications(false)}> + @@ -70,54 +118,105 @@ export default function Dashboard() { )} + {/* 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} + + + ); + })} + + + )} + {/* Cards */} - - - {/* Pendentes */} - - - PENDENTES - - - 12 - - + {loading ? ( + + + ) : ( + + + {/* Pendentes */} + + + PENDENTES + + + {metrics?.openTickets || 0} + + + - {/* Em atendimento */} - - - EM ATENDIMENTO - - - 5 + {/* Em atendimento */} + + + EM ATENDIMENTO + + + {metrics?.inProgressTickets || 0} + - - {/* Escalonados */} - - - ESCALONADOS - - - 3 - + {/* Escalonados */} + + + ESCALONADOS + + + {metrics?.escalatedTickets || 0} + + - - {/* Resolvidos */} - - - RESOLVIDOS - - - 28 - + {/* Resolvidos */} + + + RESOLVIDOS + + + {metrics?.closedTickets || 0} + + - + + )} - {/* Chamados */} @@ -129,41 +228,44 @@ export default function Dashboard() { - item.id} - showsVerticalScrollIndicator={false} - renderItem={({ item }) => ( - - - - - - + + item.id} + showsVerticalScrollIndicator={false} + scrollEnabled={false} + renderItem={({ item }) => ( + + + + + + - - - {item.title} - - - ID: #{item.id} • {item.subtitle} - + + + {item.title} + + + ID: #{item.id} • {item.subtitle} + + - - - - )} - /> + + + )} + /> + + diff --git a/frontend/src/app/(agent)/(tabs)/tickets.tsx b/frontend/src/app/(agent)/(tabs)/tickets.tsx index 1ad60ab..ea4a7e7 100644 --- a/frontend/src/app/(agent)/(tabs)/tickets.tsx +++ b/frontend/src/app/(agent)/(tabs)/tickets.tsx @@ -28,11 +28,18 @@ const fetchTickets = async () => { 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/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)