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.
This commit is contained in:
@@ -5,30 +5,36 @@ 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 --------- */
|
||||
/* ---------- Base reutilizable ---------- */
|
||||
function ComboBase({
|
||||
placeholder, value, onChange, options, icon: Icon,
|
||||
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={open} onOpenChange={setOpen}>
|
||||
<Popover open={disabled ? false : open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="w-full justify-between truncate"
|
||||
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">
|
||||
@@ -38,6 +44,7 @@ function ComboBase({
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
align="start"
|
||||
sideOffset={6}
|
||||
@@ -67,38 +74,73 @@ function ComboBase({
|
||||
)
|
||||
}
|
||||
|
||||
/* --------- COMBO FACULTADES --------- */
|
||||
/* ---------- Facultades ---------- */
|
||||
export function FacultadCombobox({
|
||||
value, onChange,
|
||||
}: { value?: string | null; onChange: (id: string) => void }) {
|
||||
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, color").order("nombre", { ascending: true })
|
||||
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="Selecciona facultad…" value={value} onChange={onChange} options={items} icon={Building2} />
|
||||
return (
|
||||
<ComboBase
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={items}
|
||||
icon={Building2}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/* --------- COMBO CARRERAS (filtrado por facultad) --------- */
|
||||
/* ---------- Carreras (filtra por facultad) ---------- */
|
||||
export function CarreraCombobox({
|
||||
facultadId, value, onChange, disabled,
|
||||
}: { facultadId?: string | null; value?: string | null; onChange: (id: string) => void; disabled?: boolean }) {
|
||||
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 })
|
||||
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 (
|
||||
<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>
|
||||
<ComboBase
|
||||
placeholder={ph}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={items}
|
||||
icon={GraduationCap}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user