// routes/_authenticated/usuarios.tsx import { createFileRoute, useRouter } from "@tanstack/react-router" import { useMemo, useState } from "react" import { useSuspenseQuery, queryOptions, useMutation, useQueryClient } from "@tanstack/react-query" import { supabase, useSupabaseAuth } from "@/auth/supabase" import type { Role, UserClaims } from "@/auth/supabase" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog" import { Label } from "@/components/ui/label" import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select" import * as Icons from "lucide-react" import { CarreraCombobox, FacultadCombobox } from "@/components/users/procedencia-combobox" import { toast } from "sonner" /* -------------------- Query Keys & Fetcher -------------------- */ const usersKeys = { root: ["usuarios"] as const, list: () => [...usersKeys.root, "list"] as const, } async function fetchUsers(): Promise { try { const { data: perfiles, error } = await supabase.from("perfiles").select("id"); if (error) { console.error("Error al obtener usuarios:", error.message); return []; // Devuelve un arreglo vacío en caso de error } if (!perfiles || perfiles.length === 0) { console.log("No se encontraron perfiles."); return []; // Devuelve un arreglo vacío si no hay datos } // Llama a `obtener_claims_usuario` para cada perfil const usuarios = await Promise.all( perfiles.map(async (perfil) => { const { data: claims, error: rpcError } = await supabase.rpc("obtener_claims_usuario", { p_user_id: perfil.id, // Pasa el ID del perfil como parámetro }); console.log("Claims para perfil", perfil.id, claims[0]); if (rpcError) { console.error(`Error al obtener claims para el perfil ${perfil.id}:`, rpcError.message); return null; // Devuelve null si hay un error } return { id: perfil.id, role: claims[0]?.role, title: claims[0]?.title, facultad_id: claims[0]?.facultad_id, carrera_id: claims[0]?.carrera_id, facultad_color: claims[0]?.facultad_color, clave: claims[0]?.clave, nombre: claims[0]?.nombre, apellidos: claims[0]?.apellidos, avatar: claims[0]?.avatar, }; }) ); // Filtra los resultados nulos (errores en las llamadas RPC) return usuarios.filter((u) => u !== null) as UserClaims[]; } catch (err) { console.error("Error inesperado:", err); return []; // Devuelve un arreglo vacío en caso de error inesperado } } const usersOptions = () => queryOptions({ queryKey: usersKeys.list(), queryFn: fetchUsers, staleTime: 60_000 }) /* -------------------- Ruta -------------------- */ export const Route = createFileRoute("/_authenticated/usuarios")({ component: RouteComponent, loader: async ({ context: { queryClient } }) => { await queryClient.ensureQueryData(usersOptions()) return null }, }) /* -------------------- Página -------------------- */ function RouteComponent() { const auth = useSupabaseAuth() if (auth.claims?.role !== "lci" && auth.claims?.role !== "vicerrectoria") { return
No tienes permisos para administrar usuarios.
} const { ROLES, ROLE_META } = useMemo(() => { if (!auth.roles) return { ROLES: [], ROLE_META: {} }; // Construir ROLES como un arreglo de strings const rolesArray = auth.roles.map((role) => role.nombre); // Construir ROLE_META como un objeto basado en ROLES const rolesMeta = auth.roles.reduce((acc, role) => { acc[role.nombre] = { id: role.id, label: role.label, Icon: (Icons as any)[role.icono] || Icons.Cpu, // Icono por defecto si no está definido className: /* role.nombre_clase || */ "bg-gray-500 text-white", // Clase por defecto si no está definida }; return acc; }, {} as Record>; className: string }>); return { ROLES: rolesArray, ROLE_META: rolesMeta }; }, [auth.roles]); const router = useRouter() const qc = useQueryClient() const { data } = useSuspenseQuery(usersOptions()) const [q, setQ] = useState("") const [editing, setEditing] = useState(null) const [form, setForm] = useState<{ role?: Role claims_admin?: boolean nombre?: string; apellidos?: string; title?: string; clave?: string; avatar?: string facultad_id?: string | null carrera_id?: string | null }>({}) const [createOpen, setCreateOpen] = useState(false) const [showPwd, setShowPwd] = useState(false) const [createForm, setCreateForm] = useState<{ email: string password: string role?: Role claims_admin?: boolean nombre?: string; apellidos?: string; title?: string; clave?: string; avatar?: string facultad_id?: string | null carrera_id?: string | null }>({ email: "", password: "" }) function RolePill({ role }: { role: Role }) { const meta = ROLE_META[role] if (!meta) return null const { Icon, className, label } = meta return ( {label} ) } /* ---------- Mutations ---------- */ const invalidateAll = async () => { await qc.invalidateQueries({ queryKey: usersKeys.root }) router.invalidate() } const createUser = useMutation({ mutationFn: async (payload: typeof createForm) => { // Validaciones previas if (!payload.role) { throw new Error("Selecciona un rol para el usuario."); } if ((payload.role === "secretario_academico" || payload.role === "director_facultad") && !payload.facultad_id) { throw new Error("Selecciona una facultad para este rol."); } if (payload.role === "jefe_carrera" && (!payload.facultad_id || !payload.carrera_id)) { throw new Error("Selecciona una facultad y una carrera para este rol."); } const password = payload.password?.trim() const { data, error } = await supabase.auth.signUp({ email: payload.email.trim(), password, options: { data: { nombre: payload.nombre ?? "", apellidos: payload.apellidos ?? "", title: payload.title ?? "", clave: payload.clave ?? "", avatar: payload.avatar ?? "", role: payload.role, role_id: payload.role ? ROLE_META[payload.role]?.id : null, facultad_id: payload.facultad_id ?? null, carrera_id: payload.carrera_id ?? null, } } }); if (error) throw new Error(error.message) const uid = data.user?.id if(!uid) { throw new Error("No se pudo obtener el ID del usuario creado."); } }, onSuccess: async () => { toast.success("Usuario creado") setCreateOpen(false) setCreateForm({ email: "", password: "" }) await invalidateAll() }, onError: (e: any) => toast.error(e?.message || "No se pudo crear el usuario"), }) const saveUser = useMutation({ mutationFn: async ({ u, f }: { u: UserClaims; f: typeof form }) => { const { error } = await supabase.rpc('actualizar_perfil_y_rol', { datos: { user_id: u.id, rol_nombre: f.role, titulo: f.title, facultad_id: f.facultad_id, carrera_id: f.carrera_id, nombre: f.nombre, apellidos: f.apellidos, avatar: f.avatar, } }); if (error) { throw new Error(error.message); } }, onSuccess: async () => { toast.success("Cambios guardados") setEditing(null) await invalidateAll() }, onError: (e: any) => toast.error(e?.message || "No se pudo guardar"), }) const filtered = useMemo(() => { const t = q.trim().toLowerCase() if (!t) return data return data.filter((u) => { const role: Role | undefined = u.role const label = role ? ROLE_META[role]?.label : "" return [u.nombre, u.apellidos, label] .filter(Boolean) .some((v) => String(v).toLowerCase().includes(t)) }) }, [q, data]) function openEdit(u: UserClaims) { setEditing(u) setForm({ role: u.role, nombre: u.nombre ?? "", apellidos: u.apellidos ?? "", title: u.title ?? "", clave: u.clave ?? "", avatar: u.avatar ?? "", facultad_id: u.facultad_id ?? null, carrera_id: u.carrera_id ?? null, }) } function validateScopeForSave(): string | null { if (!editing) return "Sin usuario" if (form.role === "director_facultad" || form.role === "secretario_academico") { if (!form.facultad_id) return "Selecciona una facultad" } if (form.role === "jefe_carrera") { if (!form.facultad_id || !form.carrera_id) return "Selecciona facultad y carrera" } return null } return (
Usuarios
setQ(e.target.value)} className="w-full" />
{filtered.map((u) => { const roleCode: Role | undefined = u.role const banned = false // cuando se tenga acceso a ese campo // const banned = !!(u as any).banned_until && new Date((u as any).banned_until).getTime() > Date.now() return (
{u.title ? `${u.title} ` : ""}{u.nombre ? `${u.nombre} ${u.apellidos ?? ""}` : /* (u.email ?? "—") */ "—"}
{roleCode && } {u.role === "lci" || u.role === "vicerrectoria" ? ( Admin ) : ( Usuario )} {banned ? : } {banned ? "Baneado" : "Activo"}
{/* Cuando se tenga acceso a esta info, se mostrará {u.email ?? "—"} Creado: {new Date(u.created_at).toLocaleDateString()} Último acceso: {u.last_sign_in_at ? new Date(u.last_sign_in_at).toLocaleDateString() : "—"} */}
) })} {!filtered.length &&
Sin usuarios
}
{/* Dialog de edición */} { if (!o) setEditing(null) }}> Editar usuario
setForm((s) => ({ ...s, nombre: e.target.value }))} />
setForm((s) => ({ ...s, apellidos: e.target.value }))} />
setForm((s) => ({ ...s, title: e.target.value }))} />
setForm((s) => ({ ...s, clave: e.target.value }))} />
setForm((s) => ({ ...s, avatar: e.target.value }))} />
{(form.role === "secretario_academico" || form.role === "director_facultad") && (
setForm((s) => ({ ...s, facultad_id: id, carrera_id: null }))} />

Este rol requiere Facultad.

)} {form.role === "jefe_carrera" && (
setForm((s) => ({ ...s, facultad_id: id, carrera_id: "" }))} />
setForm((s) => ({ ...s, carrera_id: id }))} disabled={!form.facultad_id} />
)} {/* Probablemente ya no sea necesario
*/}
{/* Modal: Nuevo usuario */} Nuevo usuario
setCreateForm((s) => ({ ...s, email: e.target.value }))} placeholder="usuario@lasalle.mx" />
setCreateForm((s) => ({ ...s, password: e.target.value }))} placeholder="abCD12&;" /> {/* */}

Pídeles cambiarla al iniciar sesión.

setCreateForm((s) => ({ ...s, nombre: e.target.value }))} />
setCreateForm((s) => ({ ...s, apellidos: e.target.value }))} />
setCreateForm((s) => ({ ...s, title: e.target.value }))} />
setCreateForm((s) => ({ ...s, clave: e.target.value }))} />
setCreateForm((s) => ({ ...s, avatar: e.target.value }))} />
{(createForm.role === "secretario_academico" || createForm.role === "director_facultad") && (
setCreateForm((s) => ({ ...s, facultad_id: id, carrera_id: null }))} />
)} {createForm.role === "jefe_carrera" && (
setCreateForm((s) => ({ ...s, facultad_id: id, carrera_id: "" }))} />
setCreateForm((s) => ({ ...s, carrera_id: id }))} disabled={!createForm.facultad_id} />
)} {/* Probablemente ya no sea necesario
*/}
) }