diff --git a/backend/index.js b/backend/index.js index 9f494ba..c855703 100644 --- a/backend/index.js +++ b/backend/index.js @@ -457,6 +457,78 @@ apiRouter.post('/users/:id/avatar', upload.single('avatar'), async (req, res) => }); +// --- Global Search --- +apiRouter.get('/search', async (req, res) => { + const { q } = req.query; + if (!q || q.length < 2) return res.json({ members: [], teams: [], attendances: [] }); + + const queryStr = `%${q}%`; + const results = { members: [], teams: [], attendances: [] }; + + try { + // 1. Search Members + let membersQ = 'SELECT id, name, email, slug, role, team_id, avatar_url FROM users WHERE (name LIKE ? OR email LIKE ?)'; + const membersParams = [queryStr, queryStr]; + + if (req.user.role === 'super_admin') { + // No extra filters + } else if (req.user.role === 'admin' || req.user.role === 'owner') { + membersQ += ' AND tenant_id = ?'; + membersParams.push(req.user.tenant_id); + } else if (req.user.role === 'manager') { + membersQ += ' AND tenant_id = ? AND (team_id = ? OR id = ?)'; + membersParams.push(req.user.tenant_id, req.user.team_id, req.user.id); + } else { + membersQ += ' AND id = ?'; + membersParams.push(req.user.id); + } + const [members] = await pool.query(membersQ, membersParams); + results.members = members; + + // 2. Search Teams (only for roles above agent) + if (req.user.role !== 'agent') { + let teamsQ = 'SELECT id, name, description FROM teams WHERE name LIKE ?'; + const teamsParams = [queryStr]; + + if (req.user.role === 'super_admin') { + // No extra filters + } else if (req.user.role === 'admin' || req.user.role === 'owner') { + teamsQ += ' AND tenant_id = ?'; + teamsParams.push(req.user.tenant_id); + } else if (req.user.role === 'manager') { + teamsQ += ' AND tenant_id = ? AND id = ?'; + teamsParams.push(req.user.tenant_id, req.user.team_id); + } + const [teams] = await pool.query(teamsQ, teamsParams); + results.teams = teams; + } + + // 3. Search Attendances + let attendancesQ = 'SELECT a.id, a.summary, a.created_at, u.name as user_name FROM attendances a JOIN users u ON a.user_id = u.id WHERE a.summary LIKE ?'; + const attendancesParams = [queryStr]; + + if (req.user.role === 'super_admin') { + // No extra filters + } else if (req.user.role === 'admin' || req.user.role === 'owner') { + attendancesQ += ' AND a.tenant_id = ?'; + attendancesParams.push(req.user.tenant_id); + } else if (req.user.role === 'manager') { + attendancesQ += ' AND a.tenant_id = ? AND u.team_id = ?'; + attendancesParams.push(req.user.tenant_id, req.user.team_id); + } else { + attendancesQ += ' AND a.user_id = ?'; + attendancesParams.push(req.user.id); + } + const [attendances] = await pool.query(attendancesQ, attendancesParams); + results.attendances = attendances; + + res.json(results); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + + // --- Attendance Routes --- apiRouter.get('/attendances', async (req, res) => { try { diff --git a/components/Layout.tsx b/components/Layout.tsx index a05dc09..9f98354 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -2,9 +2,9 @@ import React, { useState, useEffect } from 'react'; import { NavLink, useLocation, useNavigate } from 'react-router-dom'; import { LayoutDashboard, Users, UserCircle, Bell, Search, Menu, X, LogOut, - Hexagon, Settings, Building2, Sun, Moon + Hexagon, Settings, Building2, Sun, Moon, Loader2 } from 'lucide-react'; -import { getAttendances, getUsers, getUserById, logout } from '../services/dataService'; +import { getAttendances, getUsers, getUserById, logout, searchGlobal } from '../services/dataService'; import { User } from '../types'; const SidebarItem = ({ to, icon: Icon, label, collapsed }: { to: string, icon: any, label: string, collapsed: boolean }) => ( @@ -30,6 +30,29 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => const navigate = useNavigate(); const [currentUser, setCurrentUser] = useState(null); + // Search State + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState<{ members: User[], teams: any[], attendances: any[] }>({ members: [], teams: [], attendances: [] }); + const [isSearching, setIsSearching] = useState(false); + const [showSearchResults, setShowSearchResults] = useState(false); + + useEffect(() => { + const delayDebounceFn = setTimeout(async () => { + if (searchQuery.length >= 2) { + setIsSearching(true); + const results = await searchGlobal(searchQuery); + setSearchResults(results); + setIsSearching(false); + setShowSearchResults(true); + } else { + setSearchResults({ members: [], teams: [], attendances: [] }); + setShowSearchResults(false); + } + }, 300); + + return () => clearTimeout(delayDebounceFn); + }, [searchQuery]); + useEffect(() => { const fetchCurrentUser = async () => { const storedUserId = localStorage.getItem('ctms_user_id'); @@ -189,15 +212,120 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {/* Search Bar */} -
- - +
+
+ {isSearching ? : } + setSearchQuery(e.target.value)} + onFocus={() => searchQuery.length >= 2 && setShowSearchResults(true)} + className="bg-transparent border-none outline-none text-sm ml-2 w-full text-zinc-700 dark:text-dark-text placeholder-zinc-400 dark:placeholder-dark-muted" + /> +
+ + {/* Search Results Dropdown */} + {showSearchResults && ( +
+
+ {/* Members Section */} + {searchResults.members.length > 0 && ( +
+
Membros
+ {searchResults.members.map(m => ( + + ))} +
+ )} + + {/* Teams Section */} + {searchResults.teams.length > 0 && ( +
+
Equipes
+ {searchResults.teams.map(t => ( + + ))} +
+ )} + + {/* Attendances Section */} + {searchResults.attendances.length > 0 && ( +
+
Atendimentos
+ {searchResults.attendances.map(a => ( + + ))} +
+ )} + + {searchResults.members.length === 0 && searchResults.teams.length === 0 && searchResults.attendances.length === 0 && ( +
+ Nenhum resultado encontrado para "{searchQuery}" +
+ )} +
+
+ )}
+ {/* Close results when clicking outside */} + {showSearchResults &&
setShowSearchResults(false)} />} + {/* Notifications */}