import * as Icons from "lucide-react"; import { useMemo, useState } from "react"; import { useSuspenseQuery, useMutation, useQueryClient, queryOptions, } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog"; import { Textarea } from "@/components/ui/textarea"; import { supabase, useSupabaseAuth } from "@/auth/supabase"; import { toast } from "sonner"; import ReactMarkdown from "react-markdown"; import { HistorialCambiosModal } from "../historico/HistorialCambiosModal"; // @ts-ignore import AIChatModal from "../ai/AIChatModal"; /* ===================================================== Query keys & fetcher ===================================================== */ export const planKeys = { root: ["plan"] as const, byId: (id: string) => [...planKeys.root, id] as const, }; export type PlanTextFields = { objetivo_general?: string | string[] | null; sistema_evaluacion?: string | string[] | null; perfil_ingreso?: string | string[] | null; perfil_egreso?: string | string[] | null; competencias_genericas?: string | string[] | null; competencias_especificas?: string | string[] | null; indicadores_desempeno?: string | string[] | null; pertinencia?: string | string[] | null; prompt?: string | null; historico?: string | null; }; async function fetchPlanText(planId: string): Promise { const { data, error } = await supabase .from("plan_estudios") .select(`*`) .eq("id", planId) .single(); if (error) throw error; return (data ?? {}) as PlanTextFields; } export const planTextOptions = (planId: string) => queryOptions({ queryKey: planKeys.byId(planId), queryFn: () => fetchPlanText(planId), staleTime: 60_000, }); /* ===================================================== Color helpers ===================================================== */ function hexToRgb(hex?: string | null): [number, number, number] { if (!hex) return [37, 99, 235]; const h = hex.replace("#", ""); const v = h.length === 3 ? h .split("") .map((c) => c + c) .join("") : h; const n = parseInt(v, 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; } const rgba = (rgb: [number, number, number], a: number) => `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${a})`; /* ===================================================== Expandable text ===================================================== */ function ExpandableText({ text, mono = false, }: { text?: string | string[] | null; mono?: boolean; }) { const [open, setOpen] = useState(false); if (!text || (Array.isArray(text) && text.length === 0)) { return ; } const content = Array.isArray(text) ? text.join("\n• ") : text; const rendered = Array.isArray(text) ? `• ${content}` : content; return (
{rendered} {String(rendered).length > 220 && ( )}
); } /* ===================================================== Section panel ===================================================== */ function SectionPanel({ title, icon: Icon, color, children, id, }: { title: string; icon: any; color?: string | null; children: React.ReactNode; id: string; }) { const rgb = hexToRgb(color); return (

{title}

{children}
); } /* ===================================================== AcademicSections (con React Query) ===================================================== */ export function AcademicSections({ planId, color, }: { planId: string; color?: string | null; }) { const qc = useQueryClient(); const auth = useSupabaseAuth(); const [openHistorial, setOpenHistorial] = useState(false); const [openModalIa, setopenModalIa] = useState(false); if (!planId) return
Cargando…
; const { data: plan } = useSuspenseQuery(planTextOptions(planId)); const [editing, setEditing] = useState(null); const [draft, setDraft] = useState(""); // --- mutation con actualización optimista --- const updateField = useMutation({ mutationFn: async ({ key, value, }: { key: keyof PlanTextFields; value: string | string[] | null; }) => { const payload: Record = { [key]: value }; const { error } = await supabase .from("plan_estudios") .update(payload) .eq("id", planId); if (error) throw error; return payload; }, onMutate: async ({ key, value }) => { await qc.cancelQueries({ queryKey: planKeys.byId(planId) }); const prev = qc.getQueryData(planKeys.byId(planId)); qc.setQueryData(planKeys.byId(planId), (old) => ({ ...(old ?? {}), [key]: value, })); return { prev }; }, onError: (e, _vars, ctx) => { if (ctx?.prev) qc.setQueryData(planKeys.byId(planId), ctx.prev); toast.error((e as any)?.message || "No se pudo guardar 😓"); }, onSuccess: () => { toast.success("Guardado ✅"); }, onSettled: async () => { await qc.invalidateQueries({ queryKey: planKeys.byId(planId) }); }, }); const sections = useMemo( () => [ { id: "sec-clave", title: "Clave del plan", icon: Icons.Key, key: "clave_del_plan_de_estudios" as const, mono: true, }, { id: "sec-area", title: "Área de estudio", icon: Icons.Library, key: "area_de_estudio" as const, mono: false, }, // --- Estructura Temporal --- { id: "sec-ciclos", title: "Total de ciclos", icon: Icons.CalendarRange, key: "total_de_ciclos_del_plan_de_estudios" as const, mono: false, }, { id: "sec-duracion-ciclo", title: "Duración del ciclo (semanas)", icon: Icons.CalendarDays, key: "duracion_del_ciclo_escolar" as const, mono: false, }, { id: "sec-carga", title: "Carga horaria semanal", icon: Icons.Clock, key: "carga_horaria_a_la_semana" as const, mono: false, }, // --- Perfiles y Fines --- { id: "sec-antecedente", title: "Antecedente académico", icon: Icons.BookOpen, key: "antecedente_academico" as const, mono: false, }, { id: "sec-ingreso", title: "Perfil de ingreso", icon: Icons.UserPlus, key: "perfil_de_ingreso" as const, mono: false, }, { id: "sec-fines", title: "Fines de aprendizaje", icon: Icons.Target, key: "fines_de_aprendizaje_o_formacion" as const, mono: false, }, { id: "sec-egreso", title: "Perfil de egreso", icon: Icons.UserCheck, key: "perfil_de_egreso" as const, mono: false, }, // --- Operatividad y Modelo --- { id: "sec-admin", title: "Administración y operatividad", icon: Icons.Briefcase, key: "administracion_y_operatividad_del_plan_de_estudios" as const, mono: false, }, { id: "sec-sustento", title: "Sustento teórico", icon: Icons.Book, key: "sustento_teorico_del_modelo_curricular" as const, mono: false, }, { id: "sec-justificacion", title: "Justificación curricular", icon: Icons.MessageSquareText, key: "justificacion_de_la_propuesta_curricular" as const, mono: false, }, { id: "sec-evaluacion", title: "Evaluación periódica", icon: Icons.CheckCircle2, key: "propuesta_de_evaluacion_periodica_del_plan_de_estudios" as const, mono: false, }, // --- Específicos / Opcionales --- { id: "sec-investigacion", title: "Programa de investigación", icon: Icons.Microscope, key: "programa_de_investigacion" as const, mono: false, }, { id: "sec-propedeutico", title: "Curso propedéutico", icon: Icons.School, key: "curso_propedeutico" as const, mono: false, }, // --- Meta / Sistema --- { id: "sec-prm", title: "Prompt (origen)", icon: Icons.Code2, key: "prompt" as const, mono: true, }, { id: "sec-hist", title: "Histórico de cambios", icon: Icons.History, key: "historico" as const, mono: false, }, ], [] ); const [iaContext, setIaContext] = useState<{ key: keyof PlanTextFields; title: string; content: string; } | null>(null); return ( <>
{sections.map((s) => { const text = String(plan[s.key]) ?? null; return ( {s.key === "historico" ? ( <> ) : ( <>
{s.key !== "prompt" && ( )}
)}
); })}
{/* Diálogo de edición */} { if (!o) setEditing(null); }} > {editing ? `Editar: ${sections.find((x) => x.key === editing.key)?.title}` : ""}