From fdcabc1db0ae896bb8ad9b2ab53e018ebdfb4347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matheus=20Sim=C3=B5es?= Date: Mon, 25 May 2026 20:56:08 -0300 Subject: [PATCH 1/4] feat: aprimorado request pra admin view --- src/store/useAdminStore.ts | 7 ++++++- supabase/functions/admin-users/index.ts | 15 ++++++++++----- .../20260525000003_admin_perf_indexes.sql | 15 +++++++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 supabase/migrations/20260525000003_admin_perf_indexes.sql diff --git a/src/store/useAdminStore.ts b/src/store/useAdminStore.ts index e12142c..7397062 100644 --- a/src/store/useAdminStore.ts +++ b/src/store/useAdminStore.ts @@ -130,7 +130,12 @@ export const useAdminStore = create()( set({ loadingInitial: true, error: null }) try { const { fetchClients, fetchUsers, fetchUserClientsMap, fetchPendingUsers } = get() - await Promise.all([fetchClients(), fetchUsers(), fetchUserClientsMap(), fetchPendingUsers()]) + await Promise.all([ + fetchClients(), + fetchUsers(), + fetchUserClientsMap(), + fetchPendingUsers().catch(() => undefined), + ]) set({ initialized: true }) } catch (err) { set({ error: toSafeUiErrorMessage(err instanceof Error ? err.message : null) }) diff --git a/supabase/functions/admin-users/index.ts b/supabase/functions/admin-users/index.ts index f9bd3a6..ed4f076 100644 --- a/supabase/functions/admin-users/index.ts +++ b/supabase/functions/admin-users/index.ts @@ -34,12 +34,17 @@ Deno.serve(async (req) => { if (action === 'listPending') { const allowedDomain = Deno.env.get('ALLOWED_DOMAIN') - const { data, error } = await db.auth.admin.listUsers() - if (error || !data) return json({ error: error?.message ?? 'Failed' }, 500) - const { data: members } = await db.from('members').select('auth_user_id') - const linked = new Set((members ?? []).map((m) => m.auth_user_id).filter(Boolean)) + + const [{ data: members }, { data: authData, error }] = await Promise.all([ + db.from('members').select('auth_user_id'), + db.auth.admin.listUsers({ perPage: 1000, page: 1 }), + ]) + + if (error || !authData) return json({ error: error?.message ?? 'Failed' }, 500) + + const linked = new Set((members ?? []).map((m: { auth_user_id: string | null }) => m.auth_user_id).filter(Boolean)) const pending = [] - for (const u of data.users) { + for (const u of authData.users) { if (!u.email) continue const domain = u.email.split('@')[1] if (allowedDomain && domain !== allowedDomain) continue diff --git a/supabase/migrations/20260525000003_admin_perf_indexes.sql b/supabase/migrations/20260525000003_admin_perf_indexes.sql new file mode 100644 index 0000000..ddd65f0 --- /dev/null +++ b/supabase/migrations/20260525000003_admin_perf_indexes.sql @@ -0,0 +1,15 @@ +-- Índices de performance para queries do painel admin + +-- audit_logs: colunas usadas nos filtros de adminFetchAuditLogs +CREATE INDEX IF NOT EXISTS idx_audit_logs_client_id ON audit_logs(client_id); +CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id); +CREATE INDEX IF NOT EXISTS idx_audit_logs_entity ON audit_logs(entity); +CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON audit_logs(created_at DESC); +-- ilike em entity_name não beneficia de btree; pg_trgm seria o ideal mas requer extensão +-- este índice parcial cobre o caso de busca sem filtro de nome +CREATE INDEX IF NOT EXISTS idx_audit_logs_entity_name ON audit_logs(entity_name); + +-- user_clients: client_id é usado em JOINs e filtros por cliente +CREATE INDEX IF NOT EXISTS idx_user_clients_client_id ON user_clients(client_id); +-- user_id já deve ter índice via FK, mas garantimos +CREATE INDEX IF NOT EXISTS idx_user_clients_user_id ON user_clients(user_id); From 9baecd842c281792c2fb3d70e6ae680fe00e47ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matheus=20Sim=C3=B5es?= Date: Mon, 25 May 2026 20:56:27 -0300 Subject: [PATCH 2/4] feat: :lipstick: atualizado stylo do popup de perfil --- .claude/settings.local.json | 2 +- src/components/AppSidebar.tsx | 163 +++++++++++++----- src/components/ClientPickerLayout.tsx | 2 +- src/views/client-picker/ClientPickerView.tsx | 83 +++++++-- .../client-picker/components/ClientCard.tsx | 98 +++++++++-- 5 files changed, 275 insertions(+), 73 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index eda2924..fc2f304 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -85,7 +85,7 @@ "hooks": [ { "type": "prompt", - "prompt": "Verifique se as alterações feitas nesta sessão afetam alguma informação documentada no CLAUDE.md ou nos ficheiros em docs/. Se sim, atualize os ficheiros relevantes para refletir as mudanças (novos componentes, mudanças de arquitetura, novos utilitários, alterações no modelo de dados, etc.). Se nenhuma alteração relevante foi feita, não é necessário atualizar." + "prompt": "Se as alterações desta sessão introduziram novos componentes públicos exportados, mudanças de arquitetura, novos hooks/utilitários partilhados, alterações no modelo de dados ou novos padrões de código — atualize os ficheiros relevantes em CLAUDE.md ou docs/. Mudanças puramente visuais (animações, z-index, estilos, refactors internos de componente) não requerem atualização de documentação e esta verificação pode terminar sem acção." } ] } diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index e503ef0..e48b223 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -25,12 +25,6 @@ import { Tooltip, TooltipTrigger, TooltipContent, - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, } from "@/components/ui" import { canAccessView } from "@/lib/accessControl" @@ -103,6 +97,121 @@ function getInitials(email?: string) { return name.slice(0, 2).toUpperCase() } +interface UserIdentityPanelProps { + userEmail?: string + userAvatarUrl?: string | null + onGoToProfile: () => void + onSignOut: () => void +} + +function UserIdentityPanel({ userEmail, userAvatarUrl, onGoToProfile, onSignOut }: UserIdentityPanelProps) { + const [open, setOpen] = useState(false) + const triggerRef = useRef(null) + const panelRef = useRef(null) + + useEffect(() => { + if (!open) return + function handleClick(e: MouseEvent) { + if ( + !triggerRef.current?.contains(e.target as Node) && + !panelRef.current?.contains(e.target as Node) + ) { + setOpen(false) + } + } + document.addEventListener("mousedown", handleClick) + return () => document.removeEventListener("mousedown", handleClick) + }, [open]) + + const initials = getInitials(userEmail) + const displayName = userEmail?.split("@")[0].replace(/[._-]/g, " ") ?? "" + const formattedName = displayName + .split(" ") + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" ") + + return ( +
+ setOpen((v) => !v)} + whileHover={{ scale: 1.06 }} + whileTap={{ scale: 0.94 }} + transition={{ type: "spring", stiffness: 400, damping: 28 }} + className={cn( + "w-9 h-9 rounded-xl flex items-center justify-center text-[11px] font-bold overflow-hidden shrink-0 cursor-pointer", + "bg-foreground text-background", + open && "ring-2 ring-foreground/30 ring-offset-1 ring-offset-[oklch(0.948_0.005_250)] dark:ring-offset-[oklch(0.138_0.009_250)]" + )} + aria-label="Menu do utilizador" + aria-expanded={open} + > + {userAvatarUrl ? ( + + ) : ( + initials + )} + + + + {open && ( + + {/* Identity header */} +
+
+ {userAvatarUrl ? ( + + ) : ( + initials + )} +
+
+

{formattedName}

+

{userEmail}

+
+
+ + {/* Actions */} +
+ {[ + { icon: UserCircle, label: "Meu Perfil", action: () => { onGoToProfile(); setOpen(false) }, delay: 0.04 }, + { icon: LogOut, label: "Sair", action: () => { onSignOut(); setOpen(false) }, delay: 0.08 }, + ].map(({ icon: Icon, label, action, delay }) => ( + + + {label} + + ))} +
+
+ )} +
+
+ ) +} + function getClientInitials(name: string) { const parts = name.trim().split(/\s+/) if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase() @@ -615,41 +724,13 @@ export function AppSidebar() { Ajuda - {/* User avatar */} - - - - - - - {userEmail} - - - onViewChange("profile")}> - - Meu Perfil - - - - - Sair - - - + {/* User avatar — identity panel */} + onViewChange("profile")} + onSignOut={onSignOut} + /> ) diff --git a/src/components/ClientPickerLayout.tsx b/src/components/ClientPickerLayout.tsx index 11f2603..d0b0bcd 100644 --- a/src/components/ClientPickerLayout.tsx +++ b/src/components/ClientPickerLayout.tsx @@ -70,7 +70,7 @@ export function ClientPickerLayout({
{/* Mini-sidebar — sempre recolhida */} -