feat: refine global search RBAC and fix image loading
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m24s
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user