From 76170421aa759861e9e04b0367829a93d5568628 Mon Sep 17 00:00:00 2001 From: Alejandro Rosales Date: Fri, 28 Nov 2025 09:52:53 -0600 Subject: [PATCH 1/2] Refactor components by removing unused imports and optimizing state management; add configuration for Azure Static Web Apps --- .../historico/HistorialCambiosModal.tsx | 1 - src/components/planes/AddAsignaturaButton.tsx | 3 - src/components/planes/DownloadPlanPDF.tsx | 1 - src/components/planes/academic-sections.tsx | 16 +- src/hooks/useSupabaseUpdateWithHistory.ts | 4 +- src/routes/_authenticated.tsx | 2 - src/routes/_authenticated/asignaturas.tsx | 175 +++++++++--------- src/routes/_authenticated/plan/$planId.tsx | 2 +- src/routes/_authenticated/usuarios.tsx | 95 +--------- swa-cli.config.json | 12 ++ 10 files changed, 106 insertions(+), 205 deletions(-) create mode 100644 swa-cli.config.json diff --git a/src/components/historico/HistorialCambiosModal.tsx b/src/components/historico/HistorialCambiosModal.tsx index 225e432..55592f7 100644 --- a/src/components/historico/HistorialCambiosModal.tsx +++ b/src/components/historico/HistorialCambiosModal.tsx @@ -2,7 +2,6 @@ import { useQuery } from "@tanstack/react-query" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { supabase } from "@/auth/supabase" -import ReactMarkdown from "react-markdown" import { useSupabaseAuth } from "@/auth/supabase" export function HistorialCambiosModal({ diff --git a/src/components/planes/AddAsignaturaButton.tsx b/src/components/planes/AddAsignaturaButton.tsx index c2b4e62..1c8d3af 100644 --- a/src/components/planes/AddAsignaturaButton.tsx +++ b/src/components/planes/AddAsignaturaButton.tsx @@ -6,15 +6,12 @@ import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { AuroraButton } from "@/components/effect/aurora-button" import confetti from "canvas-confetti" -import { useQueryClient } from "@tanstack/react-query" import { supabase, useSupabaseAuth } from "@/auth/supabase" import { Field } from "./Field" import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs" -import { asignaturaKeys } from "./planQueries" import { useRouter } from "@tanstack/react-router" export function AddAsignaturaButton({ planId, onAdded }: { planId: string; onAdded?: () => void }) { - const qc = useQueryClient() const router = useRouter() const supabaseAuth = useSupabaseAuth() const [open, setOpen] = useState(false) diff --git a/src/components/planes/DownloadPlanPDF.tsx b/src/components/planes/DownloadPlanPDF.tsx index 5784e71..583a71e 100644 --- a/src/components/planes/DownloadPlanPDF.tsx +++ b/src/components/planes/DownloadPlanPDF.tsx @@ -33,7 +33,6 @@ export function DownloadPlanPDF({ plan }: { plan: PlanLike }) { const sectionGap = 10 // Espacio entre recuadros de sección const bodyFontSize = 10.5 const headingFontSize = 12 - const subHeadingFontSize = 10 const bulletGlifo = "\u21D2" // Flecha doble (⇒) para el glifo const bulletIndent = 6 // Sangría para el texto de la lista diff --git a/src/components/planes/academic-sections.tsx b/src/components/planes/academic-sections.tsx index 1cc54c4..b4242e6 100644 --- a/src/components/planes/academic-sections.tsx +++ b/src/components/planes/academic-sections.tsx @@ -72,7 +72,7 @@ const rgba = (rgb: [number, number, number], a: number) => `rgba(${rgb[0]},${rgb /* ===================================================== Expandable text ===================================================== */ -function ExpandableText({ text, mono = false }: { text?: string | string[] | null; mono?: boolean }) { +function ExpandableText({ text }: { text?: string | string[] | null; mono?: boolean }) { const [open, setOpen] = useState(false) if (!text || (Array.isArray(text) && text.length === 0)) { return @@ -127,16 +127,6 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st const [editing, setEditing] = useState(null) const [draft, setDraft] = useState("") - const plan_format={ - "objetivo_general": "...", - "sistema_evaluacion": "...", - "perfil_ingreso": "...", - "perfil_egreso": "...", - "competencias_genericas": "...", - "competencias_especificas": "...", - "indicadores_desempeno": "...", - "pertinencia": "..." -} // --- mutation con actualización optimista --- const updateField = useMutation({ @@ -313,12 +303,12 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st - setOpenHistorial(false)} planId={planId} onRestore={async (key, value) => { - updateField.mutate({ key, value }) + updateField.mutate({ key: key as keyof PlanTextFields, value }) }} /> diff --git a/src/hooks/useSupabaseUpdateWithHistory.ts b/src/hooks/useSupabaseUpdateWithHistory.ts index a712668..b8e60f7 100644 --- a/src/hooks/useSupabaseUpdateWithHistory.ts +++ b/src/hooks/useSupabaseUpdateWithHistory.ts @@ -1,4 +1,4 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useMutation } from "@tanstack/react-query"; import { supabase } from "@/auth/supabase"; /** @@ -11,8 +11,6 @@ export function useSupabaseUpdateWithHistory>( tableName: string, idKey: keyof T = "id" as keyof T ) { - const qc = useQueryClient(); - // Generar diferencias tipo JSON Patch function generateDiff(oldData: T, newData: Partial) { const changes: any[] = []; diff --git a/src/routes/_authenticated.tsx b/src/routes/_authenticated.tsx index f004f36..f8beab2 100644 --- a/src/routes/_authenticated.tsx +++ b/src/routes/_authenticated.tsx @@ -149,8 +149,6 @@ function Layout() { function Sidebar({ onNavigate }: { onNavigate?: () => void }) { const { claims } = useSupabaseAuth() - const isAdmin = claims?.role === 'lci' || claims?.role === 'vicerrectoria' - const canSeeCarreras = ["jefe_carrera", 'vicerrectoria', 'secretario_academico', 'lci'].includes(claims?.role ?? '') diff --git a/src/routes/_authenticated/asignaturas.tsx b/src/routes/_authenticated/asignaturas.tsx index 425225c..3abc7c4 100644 --- a/src/routes/_authenticated/asignaturas.tsx +++ b/src/routes/_authenticated/asignaturas.tsx @@ -2,7 +2,7 @@ import { createFileRoute, Link, useRouter } from '@tanstack/react-router' import { useSuspenseQuery, queryOptions, useQueryClient } from '@tanstack/react-query' import { supabase } from '@/auth/supabase' import * as Icons from 'lucide-react' -import { useEffect, useMemo, useState } from 'react' +import { useMemo, useState } from 'react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select' @@ -81,7 +81,7 @@ async function fetchAsignaturas(search: SearchState): Promise { const planIds = await fetchPlanIdsByScope(search) if (planIds && planIds.length === 0) return [] console.log(AsignaturaCard); - + let query = supabase .from('asignaturas') .select(` @@ -168,25 +168,25 @@ function RouteComponent() { const [q, setQ] = useState(search.q ?? '') const [sem, setSem] = useState('todos') const [tipo, setTipo] = useState('todos') - const [groupBy, setGroupBy] = useState<'semestre' | 'ninguno'>('semestre') + const [groupBy] = useState<'semestre' | 'ninguno'>('semestre') const [flag, setFlag] = useState<'' | 'sinBibliografia' | 'sinCriterios' | 'sinContenidos'>(search.f ?? '') -const [facultad, setFacultad] = useState("todas") -const [carrera, setCarrera] = useState("todas") + const [facultad, setFacultad] = useState("todas") + const [carrera, setCarrera] = useState("todas") -/* useEffect(() => { - const timeout = setTimeout(() => { - router.navigate({ - to: '/asignaturas', - search: { ...search, q }, - replace: true, - }) - }, 400) - return () => clearTimeout(timeout) -}, [q]) */ + /* useEffect(() => { + const timeout = setTimeout(() => { + router.navigate({ + to: '/asignaturas', + search: { ...search, q }, + replace: true, + }) + }, 400) + return () => clearTimeout(timeout) + }, [q]) */ -function handleChange(e: React.ChangeEvent) { + function handleChange(e: React.ChangeEvent) { const value = e.target.value setQ(value) router.navigate({ @@ -199,30 +199,30 @@ function handleChange(e: React.ChangeEvent) { }) } -// 🟣 Lista única de facultades -const facultadesList = useMemo(() => { - const unique = new Map() - planes?.forEach((p) => { - const fac = p.carrera?.facultad - if (fac?.id && fac?.nombre) unique.set(fac.id, fac.nombre) - }) - return Array.from(unique.entries()) -}, [planes]) + // 🟣 Lista única de facultades + const facultadesList = useMemo(() => { + const unique = new Map() + planes?.forEach((p) => { + const fac = p.carrera?.facultad + if (fac?.id && fac?.nombre) unique.set(fac.id, fac.nombre) + }) + return Array.from(unique.entries()) + }, [planes]) -// 🎓 Lista de carreras según la facultad seleccionada -const carrerasList = useMemo(() => { - const unique = new Map() - planes?.forEach((p) => { - if ( - p.carrera?.id && - p.carrera?.nombre && - (!facultad || facultad === "todas" || p.carrera?.facultad?.id === facultad) - ) { - unique.set(p.carrera.id, p.carrera.nombre) - } - }) - return Array.from(unique.entries()) -}, [planes, facultad]) + // 🎓 Lista de carreras según la facultad seleccionada + const carrerasList = useMemo(() => { + const unique = new Map() + planes?.forEach((p) => { + if ( + p.carrera?.id && + p.carrera?.nombre && + (!facultad || facultad === "todas" || p.carrera?.facultad?.id === facultad) + ) { + unique.set(p.carrera.id, p.carrera.nombre) + } + }) + return Array.from(unique.entries()) + }, [planes, facultad]) // NEW: Clonado individual @@ -256,12 +256,6 @@ const carrerasList = useMemo(() => { return Array.from(s).sort((a, b) => (a === '—' ? 1 : 0) - (b === '—' ? 1 : 0) || Number(a) - Number(b)) }, [asignaturas]) - const tipos = useMemo(() => { - const s = new Set() - asignaturas.forEach(a => s.add(a.tipo ?? '—')) - return Array.from(s).sort() - }, [asignaturas]) - // Salud const salud = useMemo(() => { let sinBibliografia = 0, sinCriterios = 0, sinContenidos = 0 @@ -274,29 +268,29 @@ const carrerasList = useMemo(() => { }, [asignaturas]) const filtered = useMemo(() => { - const t = q.trim().toLowerCase() - return asignaturas.filter(a => { - const matchesQ = - !t || - [a.nombre, a.clave, a.tipo, a.objetivos, a.plan?.nombre, a.plan?.carrera?.nombre, a.plan?.carrera?.facultad?.nombre] - .filter(Boolean) - .some(v => String(v).toLowerCase().includes(t)) + const t = q.trim().toLowerCase() + return asignaturas.filter(a => { + const matchesQ = + !t || + [a.nombre, a.clave, a.tipo, a.objetivos, a.plan?.nombre, a.plan?.carrera?.nombre, a.plan?.carrera?.facultad?.nombre] + .filter(Boolean) + .some(v => String(v).toLowerCase().includes(t)) - const semOK = sem === 'todos' || String(a.semestre ?? '—') === sem - const tipoOK = tipo === 'todos' || (a.tipo ?? '—') === tipo - const carreraOK = carrera === 'todas' || a.plan?.carrera?.id === carrera - const facultadOK = facultad === 'todas' || a.plan?.carrera?.facultad?.id === facultad - const planOK = !search.planId || a.plan?.id === search.planId + const semOK = sem === 'todos' || String(a.semestre ?? '—') === sem + const tipoOK = tipo === 'todos' || (a.tipo ?? '—') === tipo + const carreraOK = carrera === 'todas' || a.plan?.carrera?.id === carrera + const facultadOK = facultad === 'todas' || a.plan?.carrera?.facultad?.id === facultad + const planOK = !search.planId || a.plan?.id === search.planId - const flagOK = - !flag || - (flag === 'sinBibliografia' && (!a.bibliografia || a.bibliografia.length === 0)) || - (flag === 'sinCriterios' && (!a.criterios_evaluacion || !a.criterios_evaluacion.trim())) || - (flag === 'sinContenidos' && (!a.contenidos || Object.keys(a.contenidos ?? {}).length === 0)) + const flagOK = + !flag || + (flag === 'sinBibliografia' && (!a.bibliografia || a.bibliografia.length === 0)) || + (flag === 'sinCriterios' && (!a.criterios_evaluacion || !a.criterios_evaluacion.trim())) || + (flag === 'sinContenidos' && (!a.contenidos || Object.keys(a.contenidos ?? {}).length === 0)) - return matchesQ && semOK && tipoOK && flagOK && carreraOK && facultadOK && planOK - }) -}, [q, sem, tipo, flag, carrera, facultad, asignaturas]) + return matchesQ && semOK && tipoOK && flagOK && carreraOK && facultadOK && planOK + }) + }, [q, sem, tipo, flag, carrera, facultad, asignaturas]) // Agrupación @@ -316,18 +310,19 @@ const carrerasList = useMemo(() => { }, [filtered, groupBy]) // Helpers - const clearFilters = () => { setQ(''); setSem('todos'); setTipo('todos'); setCarrera('todas'); setFlag('') ; setFacultad('todas') + const clearFilters = () => { + setQ(''); setSem('todos'); setTipo('todos'); setCarrera('todas'); setFlag(''); setFacultad('todas') // Actualiza la URL limpiando todos los query params - router.navigate({ - to: '/asignaturas', - search: { - q: '', - planId: '', - carreraId: '', - facultadId: '', - f: '' - }, - }) + router.navigate({ + to: '/asignaturas', + search: { + q: '', + planId: '', + carreraId: '', + facultadId: '', + f: '' + }, + }) } // NEW: util para clonar 1 asignatura @@ -363,7 +358,7 @@ const carrerasList = useMemo(() => { if (error) throw error } - + // NEW: abrir modal clon individual function openClone(a: Asignatura) { @@ -550,7 +545,12 @@ const carrerasList = useMemo(() => { value={search.planId ?? "todos"} onValueChange={(val) => { router.navigate({ - search: { ...search, planId: val === "todos" ? "" : val }, + to: '/asignaturas', + search: { + ...search, + planId: val === 'todos' ? '' : val, + }, + replace: true, }) }} > @@ -828,15 +828,14 @@ function AsignaturaCard({ a, onClone, onAddToCart }: { a: Asignatura; onClone: ( const horasT = a.horas_teoricas ?? 0 const horasP = a.horas_practicas ?? 0 const meta = tipoMeta(a.tipo) - const FacIcon = (Icons as any)[a.plan?.carrera?.facultad?.icon ?? 'Building2'] || Icons.Building2 console.log(a); - + return (
  • @@ -890,15 +889,15 @@ function AsignaturaCard({ a, onClone, onAddToCart }: { a: Asignatura; onClone: ( {a.plan.carrera && ( } - label={a.plan.carrera.nombre} - /> + label={a.plan.carrera.nombre} + /> )} {a.plan.carrera?.facultad && ( } label={a.plan.carrera.facultad.nombre} - tint={a.plan.carrera.facultad.color} - /> + tint={a.plan.carrera.facultad.color} + /> )}
    )} diff --git a/src/routes/_authenticated/plan/$planId.tsx b/src/routes/_authenticated/plan/$planId.tsx index 2dfb041..76e0e02 100644 --- a/src/routes/_authenticated/plan/$planId.tsx +++ b/src/routes/_authenticated/plan/$planId.tsx @@ -7,7 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { AcademicSections, planKeys } from "@/components/planes/academic-sections" import { GradientMesh } from "../../../components/planes/GradientMesh" -import { asignaturaExtraOptions, asignaturaKeys, asignaturasCountOptions, asignaturasPreviewOptions, planByIdOptions, type AsignaturaLite, type PlanFull } from "@/components/planes/planQueries" +import { asignaturaExtraOptions, asignaturaKeys, asignaturasPreviewOptions, planByIdOptions, type AsignaturaLite, type PlanFull } from "@/components/planes/planQueries" import { softAccentStyle } from "@/components/planes/planHelpers" import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog" import { DialogFooter, DialogHeader } from "@/components/ui/dialog" diff --git a/src/routes/_authenticated/usuarios.tsx b/src/routes/_authenticated/usuarios.tsx index 8345f17..9405dd1 100644 --- a/src/routes/_authenticated/usuarios.tsx +++ b/src/routes/_authenticated/usuarios.tsx @@ -17,13 +17,6 @@ import { toast } from "sonner" -/* -------------------- Tipos -------------------- */ - - -const SCOPED_ROLES = ["director_facultad", "secretario_academico", "jefe_carrera"] as const - - - /* -------------------- Query Keys & Fetcher -------------------- */ const usersKeys = { @@ -149,35 +142,6 @@ function RouteComponent() { carrera_id?: string | null }>({ email: "", password: "" }) - function genPassword() { - /* - Supabase requiere que las contraseñas tengan las siguientes características: - - Mínimo de 6 caracteres - - Debe contener al menos una letra minúscula - - Debe contener al menos una letra mayúscula - - Debe contener al menos un número - - Debe contener al menos un carácter especial - Para garantizar la seguridad, generaremos contraseñas de 12 caracteres en vez del mínimo de 6 - */ - - // 1. Generar una permutación de los números de 1 al 12 con el método Fisher-Yates - - const positions = Array.from({ length: 12 }, (_, i) => i); - for (let i = positions.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [positions[i], positions[j]] = [positions[j], positions[i]]; - } - - // 2. Las correspondencias son las siguientes: - // - El primer número indica la posición de la letra minúscula - // - El segundo número indica la posición de la letra mayúscula - // - El tercer número indica la posición del número - // - El cuarto número indica la posición del carácter especial - // - En las demás posiciones puede haber cualquier caracter alfanumérico - - const s = Array.from(crypto.getRandomValues(new Uint32Array(4))).map((n) => n.toString(36)).join("") - return s.slice(0, 14) - } function RolePill({ role }: { role: Role }) { const meta = ROLE_META[role] @@ -197,61 +161,6 @@ function RouteComponent() { router.invalidate() } - const upsertNombramiento = useMutation({ - mutationFn: async (opts: { - user_id: string - puesto: "director_facultad" | "secretario_academico" | "jefe_carrera" - facultad_id?: string | null - carrera_id?: string | null - }) => { - // cierra vigentes - if (opts.puesto === "jefe_carrera") { - if (!opts.carrera_id) throw new Error("Selecciona carrera") - await supabase - .from("nombramientos") - .update({ hasta: new Date().toISOString().slice(0, 10) }) - .eq("puesto", "jefe_carrera") - .eq("carrera_id", opts.carrera_id) - .is("hasta", null) - } else { - if (!opts.facultad_id) throw new Error("Selecciona facultad") - await supabase - .from("nombramientos") - .update({ hasta: new Date().toISOString().slice(0, 10) }) - .eq("puesto", opts.puesto) - .eq("facultad_id", opts.facultad_id) - .is("hasta", null) - } - const { error } = await supabase.from("nombramientos").insert({ - user_id: opts.user_id, - puesto: opts.puesto, - facultad_id: opts.facultad_id ?? null, - carrera_id: opts.carrera_id ?? null, - desde: new Date().toISOString().slice(0, 10), - hasta: null, - }) - if (error) throw error - }, - onError: (e: any) => toast.error(e?.message || "Error al registrar nombramiento"), - }) - - const toggleBan = useMutation({ - mutationFn: async (u: UserClaims) => { - throw new Error("Funcionalidad de baneo no implementada aún.") - const banned = false // cuando se tenga acceso a ese campo - // const banned = !!u.banned_until && new Date(u.banned_until) > new Date() - const payload = banned ? { banned_until: null } : { banned_until: new Date(Date.now() + 100 * 365 * 24 * 60 * 60 * 1000).toISOString() } - // const { error } = await supabase.auth.admin.updateUserById(u.id, payload as any) - // if (error) throw new Error(error.message) - return !banned - }, - onSuccess: async (isBanned) => { - toast.success(isBanned ? "Usuario baneado" : "Usuario desbaneado") - await invalidateAll() - }, - onError: (e: any) => toast.error(e?.message || "Error al cambiar estado de baneo"), - }) - const createUser = useMutation({ mutationFn: async (payload: typeof createForm) => { // Validaciones previas @@ -409,7 +318,7 @@ function RouteComponent() {
    -
    - +
    diff --git a/swa-cli.config.json b/swa-cli.config.json new file mode 100644 index 0000000..80985f6 --- /dev/null +++ b/swa-cli.config.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://aka.ms/azure/static-web-apps-cli/schema", + "configurations": { + "acad-ia": { + "appLocation": ".", + "outputLocation": "dist", + "appBuildCommand": "npm run build", + "run": "npm run dev", + "appDevserverUrl": "http://localhost:5173" + } + } +} \ No newline at end of file From e3c1a0ce2b1d771939f130b070711073fd5a012f Mon Sep 17 00:00:00 2001 From: Alejandro Rosales Date: Fri, 28 Nov 2025 10:09:28 -0600 Subject: [PATCH 2/2] Add staticwebapp.config.json for navigation fallback configuration --- staticwebapp.config.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 staticwebapp.config.json diff --git a/staticwebapp.config.json b/staticwebapp.config.json new file mode 100644 index 0000000..a7c52b1 --- /dev/null +++ b/staticwebapp.config.json @@ -0,0 +1,14 @@ +{ + "navigationFallback": { + "rewrite": "/index.html", + "exclude": [ + "/assets/*", + "/*.css", + "/*.js", + "/*.ico", + "/*.png", + "/*.jpg", + "/*.svg" + ] + } +}