feat: add Usuarios route and user management functionality

- Introduced a new route for user management under /usuarios.
- Implemented user listing with search and edit capabilities.
- Added role management with visual indicators for user roles.
- Created a modal for editing user details, including role and permissions.
- Integrated Supabase for user data retrieval and updates.
- Enhanced UI components for better user experience.
- Removed unused planes route and related components.
- Added a new plan detail modal for displaying plan information.
- Updated navigation to include new Usuarios link.
This commit is contained in:
2025-08-21 15:30:50 -06:00
parent fe471bcfc2
commit 02ad043ed6
16 changed files with 1542 additions and 97 deletions

View File

@@ -0,0 +1,104 @@
import { useEffect, useMemo, useState } from "react"
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"
import { Command, CommandInput, CommandList, CommandGroup, CommandItem, CommandEmpty } from "@/components/ui/command"
import { Button } from "@/components/ui/button"
import { Check, ChevronsUpDown, Building2, GraduationCap } from "lucide-react"
import { supabase } from "@/auth/supabase"
/* Util simple */
const cls = (...a: (string|false|undefined)[]) => a.filter(Boolean).join(" ")
/* --------- COMBOBOX BASE --------- */
function ComboBase({
placeholder, value, onChange, options, icon:Icon,
}: {
placeholder: string
value?: string | null
onChange: (id: string) => void
options: { id: string; label: string }[]
icon?: any
}) {
const [open, setOpen] = useState(false)
const current = useMemo(() => options.find(o => o.id === value), [options, value])
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
role="combobox"
className="w-full sm:w-[420px] justify-between truncate"
title={current?.label ?? placeholder}
>
<span className="flex items-center gap-2 truncate">
{Icon ? <Icon className="h-4 w-4 opacity-70" /> : null}
<span className="truncate">{current?.label ?? placeholder}</span>
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
align="start"
sideOffset={6}
className="w-[--radix-popover-trigger-width] max-w-[min(92vw,28rem)] p-0"
>
<Command>
<CommandInput placeholder="Buscar..." />
<CommandList className="max-h-72">
<CommandEmpty>Sin resultados</CommandEmpty>
<CommandGroup>
{options.map(o => (
<CommandItem
key={o.id}
value={o.label}
onSelect={() => { onChange(o.id); setOpen(false) }}
className="whitespace-normal"
>
<Check className={cls("mr-2 h-4 w-4", value === o.id ? "opacity-100" : "opacity-0")} />
{o.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
/* --------- COMBO FACULTADES --------- */
export function FacultadCombobox({
value, onChange,
}: { value?: string | null; onChange: (id: string) => void }) {
const [items, setItems] = useState<{ id: string; label: string }[]>([])
useEffect(() => {
supabase.from("facultades").select("id, nombre, color").order("nombre", { ascending: true })
.then(({ data }) => setItems((data ?? []).map(f => ({ id: f.id, label: f.nombre }))))
}, [])
return <ComboBase placeholder="Selecciona facultad…" value={value} onChange={onChange} options={items} icon={Building2} />
}
/* --------- COMBO CARRERAS (filtrado por facultad) --------- */
export function CarreraCombobox({
facultadId, value, onChange, disabled,
}: { facultadId?: string | null; value?: string | null; onChange: (id: string) => void; disabled?: boolean }) {
const [items, setItems] = useState<{ id: string; label: string }[]>([])
useEffect(() => {
if (!facultadId) { setItems([]); return }
supabase.from("carreras")
.select("id, nombre").eq("facultad_id", facultadId).order("nombre", { ascending: true })
.then(({ data }) => setItems((data ?? []).map(c => ({ id: c.id, label: c.nombre }))))
}, [facultadId])
return (
<div className={disabled ? "opacity-60 pointer-events-none" : ""}>
<ComboBase
placeholder={facultadId ? "Selecciona carrera…" : "Selecciona una facultad primero"}
value={value}
onChange={onChange}
options={items}
icon={GraduationCap}
/>
</div>
)
}