feat: add context menu functionality and delete buttons for plans and carreras; update dependencies

This commit is contained in:
2025-09-01 07:30:58 -06:00
parent 6c3dd54d5f
commit 0ff3387331
7 changed files with 412 additions and 33 deletions

View File

@@ -14,6 +14,8 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@/components/ui/accordion"
import { Switch } from "@/components/ui/switch"
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from "@/components/ui/context-menu"
import { useDeleteCarreraDialog } from "@/components/carreras/DeleteCarreras"
/* -------------------- Tipos -------------------- */
type FacultadLite = { id: string; nombre: string; color?: string | null; icon?: string | null }
@@ -27,7 +29,7 @@ export type CarreraRow = {
}
/* -------------------- Query Keys & Fetchers -------------------- */
const carrerasKeys = {
export const carrerasKeys = {
root: ["carreras"] as const,
list: () => [...carrerasKeys.root, "list"] as const,
}
@@ -115,11 +117,10 @@ const tint = (hex?: string | null, a = 0.18) => {
}
const StatusPill = ({ active }: { active: boolean }) => (
<span
className={`text-[10px] px-2 py-0.5 rounded-full border ${
active
? "bg-emerald-50 text-emerald-700 border-emerald-200"
: "bg-neutral-100 text-neutral-700 border-neutral-200"
}`}
className={`text-[10px] px-2 py-0.5 rounded-full border ${active
? "bg-emerald-50 text-emerald-700 border-emerald-200"
: "bg-neutral-100 text-neutral-700 border-neutral-200"
}`}
>
{active ? "Activa" : "Inactiva"}
</span>
@@ -212,36 +213,47 @@ function RouteComponent() {
const border = tint(fac?.color, 0.28)
const chip = tint(fac?.color, 0.1)
const IconComp = (fac?.icon && (Icons as any)[fac.icon]) || Icons.Building2
const { setOpen: setDeleteOpen, dialog: deleteDialog } = useDeleteCarreraDialog(c.id, async () => {
await qc.invalidateQueries({ queryKey: carrerasKeys.root })
router.invalidate()
})
return (
<article
key={c.id}
className="group relative overflow-hidden rounded-3xl bg-white shadow-sm ring-1 transition-all hover:shadow-md hover:-translate-y-0.5"
style={{ borderColor: border, background: `linear-gradient(180deg, ${chip}, transparent)` }}
>
<div className="p-5 h-44 flex flex-col justify-between">
<div className="flex items-center gap-3">
<span className="inline-flex items-center justify-center rounded-2xl border px-2.5 py-2 bg-white/70" style={{ borderColor: border }}>
<IconComp className="w-6 h-6" />
</span>
<div className="min-w-0">
<div className="font-semibold truncate">{c.nombre}</div>
<div className="text-xs text-neutral-600 truncate">{fac?.nombre ?? "—"} · {c.semestres} semestres</div>
</div>
</div>
<ContextMenu key={c.id}>
<ContextMenuTrigger onClick={(e) => openContextMenu(e)}>
<article
className="group relative overflow-hidden rounded-3xl bg-white shadow-sm ring-1 transition-all hover:shadow-md hover:-translate-y-0.5"
style={{ borderColor: border, background: `linear-gradient(180deg, ${chip}, transparent)` }}
>
<div className="p-5 h-44 flex flex-col justify-between">
<div className="flex items-center gap-3">
<span className="inline-flex items-center justify-center rounded-2xl border px-2.5 py-2 bg-white/70" style={{ borderColor: border }}>
<IconComp className="w-6 h-6" />
</span>
<div className="min-w-0">
<div className="font-semibold truncate">{c.nombre}</div>
<div className="text-xs text-neutral-600 truncate">{fac?.nombre ?? "—"} · {c.semestres} semestres</div>
</div>
</div>
<div className="mt-2 flex items-center justify-between">
<StatusPill active={c.activo} />
<div className="flex items-center gap-1.5">
<Button variant="ghost" size="sm" onClick={() => setDetail(c)}>
<Icons.Eye className="w-4 h-4 mr-1" /> Ver
</Button>
<Button variant="ghost" size="sm" onClick={() => setEditCarrera(c)}>
<Icons.Pencil className="w-4 h-4 mr-1" /> Editar
</Button>
<div className="mt-2 flex items-center justify-between">
<StatusPill active={c.activo} />
</div>
</div>
</div>
</div>
</article>
</article>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={() => setDetail(c)}>
<Icons.Eye className="w-4 h-4 mr-2" /> Ver
</ContextMenuItem>
<ContextMenuItem onClick={() => setEditCarrera(c)}>
<Icons.Pencil className="w-4 h-4 mr-2" /> Editar
</ContextMenuItem>
<ContextMenuItem onClick={() => setDeleteOpen(true)}>
<Icons.Trash className="w-4 h-4 mr-2" /> Eliminar
</ContextMenuItem>
</ContextMenuContent>
{deleteDialog}
</ContextMenu>
)
})}
</div>
@@ -287,6 +299,22 @@ function RouteComponent() {
)
}
function openContextMenu(e: React.MouseEvent) {
e.preventDefault()
e.stopPropagation()
// Simulate right click by opening context menu
const trigger = e.currentTarget
if (!(trigger instanceof HTMLElement)) return
const event = new window.MouseEvent("contextmenu", {
bubbles: true,
cancelable: true,
view: window,
clientX: e.clientX,
clientY: e.clientY,
})
trigger.dispatchEvent(event)
}
/* -------------------- Form crear/editar -------------------- */
function CarreraFormDialog({
open,

View File

@@ -18,6 +18,7 @@ import confetti from "canvas-confetti"
import { Textarea } from "@/components/ui/textarea"
import { AuroraButton } from "@/components/effect/aurora-button"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { DeletePlanButton } from "@/components/planes/DeletePlan"
type LoaderData = { planId: string }
@@ -94,6 +95,7 @@ function RouteComponent() {
<div className='flex gap-2'>
<EditPlanButton plan={plan} />
<AdjustAIButton plan={plan} />
<DeletePlanButton planId={plan.id} />
</div>
</div>
</CardHeader>