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:
104
src/components/users/procedencia-combobox.tsx
Normal file
104
src/components/users/procedencia-combobox.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user