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:
2025-08-22 14:32:43 -06:00
parent 9727f4c691
commit ca3fed69b2
16 changed files with 2274 additions and 118 deletions

View File

@@ -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}
/>
)
}