Refactor user management in usuarios.tsx: integrate react-query for data fetching and mutations, streamline role handling, and enhance user ban/unban functionality.

This commit is contained in:
2025-08-27 16:15:42 -06:00
parent 234c41d0b6
commit 3bc4498e4f
11 changed files with 1279 additions and 1313 deletions

View File

@@ -1,4 +1,5 @@
import { createFileRoute, Link, useRouter } from '@tanstack/react-router'
import { useSuspenseQuery, queryOptions, useQueryClient } from '@tanstack/react-query'
import { supabase } from '@/auth/supabase'
import * as Icons from 'lucide-react'
import { useMemo, useState } from 'react'
@@ -6,7 +7,6 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select'
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu'
// NEW
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import { toast } from 'sonner'
@@ -35,11 +35,99 @@ type Asignatura = {
plan_id?: string | null
}
type LoaderData = {
asignaturas: Asignatura[]
planes: PlanMini[] // NEW: para elegir destino
type SearchState = {
q: string
planId: string
carreraId: string
facultadId: string
f: 'sinBibliografia' | 'sinCriterios' | 'sinContenidos' | ''
}
/* ================== Query Keys & Options ================== */
const asignaturasKeys = {
root: ['asignaturas'] as const,
list: (search: SearchState) => [...asignaturasKeys.root, { search }] as const,
}
const planesKeys = {
root: ['planes'] as const,
all: () => [...planesKeys.root, 'all'] as const,
}
async function fetchPlanIdsByScope(search: Pick<SearchState, 'planId'|'carreraId'|'facultadId'>): Promise<string[] | null> {
const { planId, carreraId, facultadId } = search
if (planId) return [planId]
if (carreraId) {
const { data, error } = await supabase.from('plan_estudios').select('id').eq('carrera_id', carreraId)
if (error) throw error
return (data ?? []).map(p => p.id)
}
if (facultadId) {
const { data: carreras, error: carErr } = await supabase.from('carreras').select('id').eq('facultad_id', facultadId)
if (carErr) throw carErr
const cIds = (carreras ?? []).map(c => c.id)
if (!cIds.length) return []
const { data: planesFac, error: plaErr } = await supabase
.from('plan_estudios')
.select('id')
.in('carrera_id', cIds)
if (plaErr) throw plaErr
return (planesFac ?? []).map(p => p.id)
}
return null
}
async function fetchAsignaturas(search: SearchState): Promise<Asignatura[]> {
const planIds = await fetchPlanIdsByScope(search)
if (planIds && planIds.length === 0) return []
let query = supabase
.from('asignaturas')
.select(`
id, nombre, clave, tipo, semestre, creditos, horas_teoricas, horas_practicas,
objetivos, contenidos, bibliografia, criterios_evaluacion, fecha_creacion, plan_id,
plan:plan_estudios (
id, nombre,
carrera:carreras (
id, nombre,
facultad:facultades ( id, nombre, color, icon )
)
)
`)
.order('semestre', { ascending: true })
.order('nombre', { ascending: true })
if (planIds) query = query.in('plan_id', planIds)
const { data, error } = await query
if (error) throw error
return (data ?? []) as unknown as Asignatura[]
}
async function fetchPlanes(): Promise<PlanMini[]> {
const { data, error } = await supabase
.from('plan_estudios')
.select(`
id, nombre,
carrera:carreras(
id, nombre,
facultad:facultades(id, nombre, color, icon)
)
`)
.order('nombre', { ascending: true })
if (error) throw error
return (data ?? []) as unknown as PlanMini[]
}
const asignaturasOptions = (search: SearchState) => queryOptions({
queryKey: asignaturasKeys.list(search),
queryFn: () => fetchAsignaturas(search),
staleTime: 60_000,
})
const planesOptions = () => queryOptions({
queryKey: planesKeys.all(),
queryFn: fetchPlanes,
staleTime: 5 * 60_000,
})
/* ================== Ruta ================== */
export const Route = createFileRoute('/_authenticated/asignaturas')({
component: RouteComponent,
@@ -53,94 +141,26 @@ export const Route = createFileRoute('/_authenticated/asignaturas')({
f: (search.f as 'sinBibliografia' | 'sinCriterios' | 'sinContenidos' | '') ?? '',
}
},
loader: async (ctx): Promise<LoaderData> => {
const search = (ctx.location?.search ?? {}) as {
q?: string
planId?: string
carreraId?: string
facultadId?: string
f?: 'sinBibliografia' | 'sinCriterios' | 'sinContenidos' | ''
}
const { planId, carreraId, facultadId } = search
let planIds: string[] | null = null
if (planId) {
planIds = [planId]
} else if (carreraId) {
const { data: planesCar, error } = await supabase
.from('plan_estudios')
.select('id')
.eq('carrera_id', carreraId)
if (error) throw error
planIds = (planesCar ?? []).map(p => p.id)
} else if (facultadId) {
const { data: carreras, error: carErr } = await supabase
.from('carreras')
.select('id')
.eq('facultad_id', facultadId)
if (carErr) throw carErr
const cIds = (carreras ?? []).map(c => c.id)
if (!cIds.length) {
return { asignaturas: [], planes: [] }
}
const { data: planesFac, error: plaErr } = await supabase
.from('plan_estudios')
.select('id, nombre, carrera:carreras(id, nombre, facultad:facultades(id, nombre, color, icon))')
.in('carrera_id', cIds)
if (plaErr) throw plaErr
planIds = (planesFac ?? []).map(p => p.id)
}
if (planIds && planIds.length === 0) {
return { asignaturas: [], planes: [] }
}
// Traer asignaturas
let query = supabase
.from('asignaturas')
.select(`
id, nombre, clave, tipo, semestre, creditos, horas_teoricas, horas_practicas,
objetivos, contenidos, bibliografia, criterios_evaluacion, fecha_creacion, plan_id,
plan:plan_estudios (
id, nombre,
carrera:carreras (
id, nombre,
facultad:facultades ( id, nombre, color, icon )
)
)
`)
.order('semestre', { ascending: true })
.order('nombre', { ascending: true })
if (planIds) query = query.in('plan_id', planIds)
const { data, error: aErr } = await query
if (aErr) throw aErr
// Traer planes (para selector destino)
const { data: planesAll, error: pErr } = await supabase
.from('plan_estudios')
.select(`
id, nombre,
carrera:carreras(
id, nombre,
facultad:facultades(id, nombre, color, icon)
)
`)
.order('nombre', { ascending: true })
if (pErr) throw pErr
return {
asignaturas: (data ?? []) as unknown as Asignatura[],
planes: (planesAll ?? []) as unknown as PlanMini[],
}
loader: async ({ context: { queryClient }, location }) => {
const search = (location?.search ?? {}) as SearchState
// Pre-hydrate ambas queries con QueryClient (sin llamadas "sueltas" aquí)
await Promise.all([
queryClient.ensureQueryData(asignaturasOptions(search)),
queryClient.ensureQueryData(planesOptions()),
])
return null
},
})
/* ================== Página ================== */
function RouteComponent() {
const { asignaturas, planes } = Route.useLoaderData() as LoaderData
const router = useRouter()
const search = Route.useSearch() as { q: string; f: '' | 'sinBibliografia' | 'sinCriterios' | 'sinContenidos' }
const qc = useQueryClient()
const search = Route.useSearch() as SearchState
// Datos por TanStack Query (suspense-friendly)
const { data: asignaturas } = useSuspenseQuery(asignaturasOptions(search))
const { data: planes } = useSuspenseQuery(planesOptions())
// Filtros
const [q, setQ] = useState(search.q ?? '')
@@ -323,6 +343,11 @@ function RouteComponent() {
toast.success(`Clonadas ${cart.length} asignaturas`)
setBulkOpen(false)
clearCart()
// Invalida ambas queries y la ruta
await Promise.all([
qc.invalidateQueries({ queryKey: asignaturasKeys.root }),
qc.invalidateQueries({ queryKey: planesKeys.root }),
])
router.invalidate()
} catch (e: any) {
console.error(e)
@@ -362,7 +387,7 @@ function RouteComponent() {
</span>
)}
</Button>
<Button variant="outline" size="icon" onClick={() => router.invalidate()} title="Recargar">
<Button variant="outline" size="icon" onClick={() => { qc.invalidateQueries({ queryKey: asignaturasKeys.root }); router.invalidate() }} title="Recargar">
<Icons.RefreshCcw className="w-4 h-4" />
</Button>
</div>
@@ -541,6 +566,7 @@ function RouteComponent() {
})
toast.success('Asignatura clonada')
setCloneOpen(false)
await qc.invalidateQueries({ queryKey: asignaturasKeys.root })
router.invalidate()
} catch (e: any) {
console.error(e)