feat: refine global search RBAC and fix image loading
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m24s

- Restricted Agent search to Attendances only.

- Enabled Super Admin search for Organizations (Tenants).

- Fixed user avatar URL construction in search results.

- Added Organizations category to search dropdown for Super Admins.
This commit is contained in:
Cauê Faleiros
2026-03-09 16:09:41 -03:00
parent 13bcfc1314
commit 12d24e9255
3 changed files with 76 additions and 38 deletions

View File

@@ -32,7 +32,7 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
// Search State
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState<{ members: User[], teams: any[], attendances: any[] }>({ members: [], teams: [], attendances: [] });
const [searchResults, setSearchResults] = useState<{ members: User[], teams: any[], attendances: any[], organizations?: any[] }>({ members: [], teams: [], attendances: [], organizations: [] });
const [isSearching, setIsSearching] = useState(false);
const [showSearchResults, setShowSearchResults] = useState(false);
@@ -45,7 +45,7 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
setIsSearching(false);
setShowSearchResults(true);
} else {
setSearchResults({ members: [], teams: [], attendances: [] });
setSearchResults({ members: [], teams: [], attendances: [], organizations: [] });
setShowSearchResults(false);
}
}, 300);
@@ -219,35 +219,68 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
{showSearchResults && (
<div className="absolute top-full mt-2 left-0 w-full bg-white dark:bg-dark-card border border-zinc-200 dark:border-dark-border rounded-2xl shadow-2xl overflow-hidden z-50 animate-in fade-in slide-in-from-top-2 duration-200">
<div className="max-h-[480px] overflow-y-auto p-2">
{/* Members Section */}
{searchResults.members.length > 0 && (
{/* Organizations Section (Super Admin only) */}
{searchResults.organizations && searchResults.organizations.length > 0 && (
<div className="mb-4">
<div className="px-3 py-2 text-[10px] font-bold text-zinc-400 dark:text-dark-muted uppercase tracking-widest border-b border-zinc-50 dark:border-dark-border/50 mb-1">Membros</div>
{searchResults.members.map(m => (
<div className="px-3 py-2 text-[10px] font-bold text-zinc-400 dark:text-dark-muted uppercase tracking-widest border-b border-zinc-50 dark:border-dark-border/50 mb-1">Organizações</div>
{searchResults.organizations.map(o => (
<button
key={m.id}
key={o.id}
onClick={() => {
navigate(`/users/${m.slug || m.id}`);
navigate(`/super-admin`);
setShowSearchResults(false);
setSearchQuery('');
}}
className="w-full flex items-center gap-3 p-2 hover:bg-zinc-50 dark:hover:bg-dark-border rounded-xl transition-colors text-left"
>
<img
src={m.avatar_url?.startsWith('http') ? m.avatar_url : `${import.meta.env.PROD ? '' : 'http://localhost:3001'}${m.avatar_url || ''}`}
alt={m.name}
className="w-9 h-9 rounded-full border border-zinc-100 dark:border-dark-border object-cover"
onError={(e) => { (e.target as HTMLImageElement).src = `https://ui-avatars.com/api/?name=${encodeURIComponent(m.name)}&background=random`; }}
/>
<div className="w-9 h-9 rounded-lg bg-zinc-100 dark:bg-dark-bg flex items-center justify-center text-brand-yellow border border-zinc-200 dark:border-dark-border">
<Hexagon size={18} fill="currentColor" />
</div>
<div>
<div className="text-sm font-semibold text-zinc-900 dark:text-dark-text">{m.name}</div>
<div className="text-xs text-zinc-500 dark:text-dark-muted">{m.email}</div>
<div className="text-sm font-semibold text-zinc-900 dark:text-dark-text">{o.name}</div>
<div className="text-xs text-zinc-500 dark:text-dark-muted">{o.slug} {o.status}</div>
</div>
</button>
))}
</div>
)}
{/* Members Section */}
{searchResults.members.length > 0 && (
<div className="mb-4">
<div className="px-3 py-2 text-[10px] font-bold text-zinc-400 dark:text-dark-muted uppercase tracking-widest border-b border-zinc-50 dark:border-dark-border/50 mb-1">Membros</div>
{searchResults.members.map(m => {
const backendUrl = import.meta.env.PROD ? '' : 'http://localhost:3001';
const avatarSrc = m.avatar_url
? (m.avatar_url.startsWith('http') ? m.avatar_url : `${backendUrl}${m.avatar_url}`)
: `https://ui-avatars.com/api/?name=${encodeURIComponent(m.name)}&background=random`;
return (
<button
key={m.id}
onClick={() => {
navigate(`/users/${m.slug || m.id}`);
setShowSearchResults(false);
setSearchQuery('');
}}
className="w-full flex items-center gap-3 p-2 hover:bg-zinc-50 dark:hover:bg-dark-border rounded-xl transition-colors text-left"
>
<img
src={avatarSrc}
alt={m.name}
className="w-9 h-9 rounded-full border border-zinc-100 dark:border-dark-border object-cover"
onError={(e) => { (e.target as HTMLImageElement).src = `https://ui-avatars.com/api/?name=${encodeURIComponent(m.name)}&background=random`; }}
/>
<div>
<div className="text-sm font-semibold text-zinc-900 dark:text-dark-text">{m.name}</div>
<div className="text-xs text-zinc-500 dark:text-dark-muted">{m.email}</div>
</div>
</button>
);
})}
</div>
)}
{/* Teams Section */}
{searchResults.teams.length > 0 && (
<div className="mb-4">
@@ -303,7 +336,7 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
</div>
)}
{searchResults.members.length === 0 && searchResults.teams.length === 0 && searchResults.attendances.length === 0 && (
{searchResults.members.length === 0 && searchResults.teams.length === 0 && searchResults.attendances.length === 0 && (!searchResults.organizations || searchResults.organizations.length === 0) && (
<div className="p-8 text-center text-zinc-500 dark:text-dark-muted text-sm">
Nenhum resultado encontrado para "{searchQuery}"
</div>