diff --git a/bun.lock b/bun.lock
index 18b91f7..502ee82 100644
--- a/bun.lock
+++ b/bun.lock
@@ -6,6 +6,7 @@
"dependencies": {
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-avatar": "^1.1.10",
+ "@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
@@ -221,6 +222,8 @@
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+ "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="],
+
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
diff --git a/package.json b/package.json
index 37ce939..d5b89ef 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"dependencies": {
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-avatar": "^1.1.10",
+ "@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
diff --git a/src/components/carreras/DeleteCarreras.tsx b/src/components/carreras/DeleteCarreras.tsx
new file mode 100644
index 0000000..cf54e14
--- /dev/null
+++ b/src/components/carreras/DeleteCarreras.tsx
@@ -0,0 +1,54 @@
+import { supabase } from "@/auth/supabase";
+import { useQueryClient } from "@tanstack/react-query";
+import { useRouter } from "@tanstack/react-router";
+import { useState } from "react";
+import { Button } from "../ui/button";
+import { Trash } from "lucide-react";
+import { carrerasKeys } from "@/routes/_authenticated/carreras";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "../ui/dialog";
+
+export function useDeleteCarreraDialog(carreraId: string, onDeleted?: () => void) {
+ const [open, setOpen] = useState(false)
+ const [loading, setLoading] = useState(false)
+ const qc = useQueryClient()
+ const router = useRouter()
+
+ async function handleDelete() {
+ setLoading(true)
+ try {
+ const { error } = await supabase.from("carreras").delete().eq("id", carreraId)
+ if (error) throw error
+ setOpen(false)
+ if (onDeleted) onDeleted()
+ await qc.invalidateQueries({ queryKey: carrerasKeys.root })
+ router.invalidate()
+ } catch (e: any) {
+ alert(e?.message || "Error al eliminar la carrera")
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const dialog = (
+
+ )
+
+ return { open, setOpen, dialog }
+}
\ No newline at end of file
diff --git a/src/components/planes/DeletePlan.tsx b/src/components/planes/DeletePlan.tsx
new file mode 100644
index 0000000..b561a02
--- /dev/null
+++ b/src/components/planes/DeletePlan.tsx
@@ -0,0 +1,41 @@
+import { supabase } from "@/auth/supabase";
+import { useRouter } from "@tanstack/react-router";
+import { useState } from "react";
+import { Button } from "../ui/button";
+
+export function DeletePlanButton({ planId, onDeleted }: { planId: string; onDeleted?: () => void }) {
+ const [confirm, setConfirm] = useState(false)
+ const [loading, setLoading] = useState(false)
+ const router = useRouter()
+
+ async function handleDelete() {
+ setLoading(true)
+ try {
+ const { error, status, statusText} = await supabase.from("plan_estudios").delete().eq("id", planId)
+ console.log({status, statusText});
+
+
+ if (error) throw error
+ setConfirm(false)
+ if (onDeleted) onDeleted()
+ router.navigate({ to: "/planes", search: { plan: '' } })
+ } catch (e: any) {
+ alert(e?.message || "Error al eliminar el plan")
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return confirm ? (
+
+
+
+
+ ) : (
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/ui/context-menu.tsx b/src/components/ui/context-menu.tsx
new file mode 100644
index 0000000..08ec121
--- /dev/null
+++ b/src/components/ui/context-menu.tsx
@@ -0,0 +1,250 @@
+import * as React from "react"
+import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
+import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function ContextMenu({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function ContextMenuTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function ContextMenuGroup({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function ContextMenuPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function ContextMenuSub({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function ContextMenuRadioGroup({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function ContextMenuSubTrigger({
+ className,
+ inset,
+ children,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ {children}
+
+
+ )
+}
+
+function ContextMenuSubContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function ContextMenuContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function ContextMenuItem({
+ className,
+ inset,
+ variant = "default",
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+ variant?: "default" | "destructive"
+}) {
+ return (
+
+ )
+}
+
+function ContextMenuCheckboxItem({
+ className,
+ children,
+ checked,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function ContextMenuRadioItem({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function ContextMenuLabel({
+ className,
+ inset,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ )
+}
+
+function ContextMenuSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function ContextMenuShortcut({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+export {
+ ContextMenu,
+ ContextMenuTrigger,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuCheckboxItem,
+ ContextMenuRadioItem,
+ ContextMenuLabel,
+ ContextMenuSeparator,
+ ContextMenuShortcut,
+ ContextMenuGroup,
+ ContextMenuPortal,
+ ContextMenuSub,
+ ContextMenuSubContent,
+ ContextMenuSubTrigger,
+ ContextMenuRadioGroup,
+}
diff --git a/src/routes/_authenticated/carreras.tsx b/src/routes/_authenticated/carreras.tsx
index 8bb6cd5..c07c8b6 100644
--- a/src/routes/_authenticated/carreras.tsx
+++ b/src/routes/_authenticated/carreras.tsx
@@ -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 }) => (
{active ? "Activa" : "Inactiva"}
@@ -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 (
-
-
-
-
-
-
-
-
{c.nombre}
-
{fac?.nombre ?? "—"} · {c.semestres} semestres
-
-
+
+ openContextMenu(e)}>
+
+
+
+
+
+
+
+
{c.nombre}
+
{fac?.nombre ?? "—"} · {c.semestres} semestres
+
+
-
-
-
-
-
+
+
+
-
-
-
+
+
+
+ setDetail(c)}>
+ Ver
+
+ setEditCarrera(c)}>
+ Editar
+
+ setDeleteOpen(true)}>
+ Eliminar
+
+
+ {deleteDialog}
+
)
})}
@@ -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,
diff --git a/src/routes/_authenticated/plan/$planId.tsx b/src/routes/_authenticated/plan/$planId.tsx
index efeda6f..9d9e4be 100644
--- a/src/routes/_authenticated/plan/$planId.tsx
+++ b/src/routes/_authenticated/plan/$planId.tsx
@@ -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() {