This repository has been archived on 2026-01-21. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Acad-IA/src/components/users/procedencia-combobox.tsx
Alejandro Rosales ca3fed69b2 feat: implement dashboard with KPIs, recent activity, and health metrics
- Added dashboard route with loader fetching KPIs, recent plans and subjects, and health metrics.
- Created visual components for displaying KPIs and recent activity.
- Implemented gradient background and user greeting based on role.
- Added input for global search and quick links for creating new plans and subjects.

refactor: update facultad progress ring rendering

- Fixed rendering of progress ring in facultad detail view.

fix: remove unnecessary link to subjects in plan detail view

- Removed link to view subjects from the plan detail page for cleaner UI.

feat: add create plan dialog in planes route

- Introduced a dialog for creating new plans with form validation and role-based field visibility.
- Integrated Supabase for creating plans and handling user roles.

feat: enhance user management with create user dialog

- Added functionality to create new users with role and claims management.
- Implemented password generation and input handling for user creation.

fix: update login redirect to dashboard

- Changed default redirect after login from /planes to /dashboard for better user experience.
2025-08-22 14:32:43 -06:00

147 lines
4.3 KiB
TypeScript

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"
const cls = (...a: (string | false | undefined)[]) => a.filter(Boolean).join(" ")
/* ---------- Base reutilizable ---------- */
function ComboBase({
placeholder,
value,
onChange,
options,
icon: Icon,
disabled = false,
}: {
placeholder: string
value?: string | null
onChange: (id: string) => void
options: { id: string; label: string }[]
icon?: any
disabled?: boolean
}) {
const [open, setOpen] = useState(false)
const current = useMemo(() => options.find(o => o.id === value), [options, value])
return (
<Popover open={disabled ? false : open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
role="combobox"
disabled={disabled}
className={cls("w-full justify-between truncate", disabled && "opacity-60 cursor-not-allowed")}
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>
)
}
/* ---------- Facultades ---------- */
export function FacultadCombobox({
value,
onChange,
disabled = false,
placeholder = "Selecciona facultad…",
}: {
value?: string | null
onChange: (id: string) => void
disabled?: boolean
placeholder?: string
}) {
const [items, setItems] = useState<{ id: string; label: string }[]>([])
useEffect(() => {
supabase
.from("facultades")
.select("id, nombre")
.order("nombre", { ascending: true })
.then(({ data }) => setItems((data ?? []).map(f => ({ id: f.id, label: f.nombre }))))
}, [])
return (
<ComboBase
placeholder={placeholder}
value={value}
onChange={onChange}
options={items}
icon={Building2}
disabled={disabled}
/>
)
}
/* ---------- Carreras (filtra por facultad) ---------- */
export function CarreraCombobox({
facultadId,
value,
onChange,
disabled = false,
placeholder,
}: {
facultadId?: string | null
value?: string | null
onChange: (id: string) => void
disabled?: boolean
placeholder?: string
}) {
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])
const ph = placeholder ?? (facultadId ? "Selecciona carrera…" : "Selecciona una facultad primero")
return (
<ComboBase
placeholder={ph}
value={value}
onChange={onChange}
options={items}
icon={GraduationCap}
disabled={disabled}
/>
)
}