diff --git a/src/components/planes/CreatePlanDialog.tsx b/src/components/planes/CreatePlanDialog.tsx index c7df82a..c25b7e2 100644 --- a/src/components/planes/CreatePlanDialog.tsx +++ b/src/components/planes/CreatePlanDialog.tsx @@ -1,15 +1,24 @@ -import { useRouter } from "@tanstack/react-router" -import { useSupabaseAuth } from "@/auth/supabase" -import { useState, useEffect, useCallback } from "react" -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog" -import { Label } from "@/components/ui/label" -import { Textarea } from "@/components/ui/textarea" -import { Input } from "@/components/ui/input" -import { CarreraCombobox, FacultadCombobox } from "@/components/users/procedencia-combobox" -import { Button } from "@/components/ui/button" -import { postAPI } from "@/lib/api" -import { supabase } from "@/auth/supabase" -import { DetailDialog } from "@/components/archivos/DetailDialog" +import { useRouter } from "@tanstack/react-router"; +import { useSupabaseAuth } from "@/auth/supabase"; +import { useState, useEffect, useCallback } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Input } from "@/components/ui/input"; +import { + CarreraCombobox, + FacultadCombobox, +} from "@/components/users/procedencia-combobox"; +import { Button } from "@/components/ui/button"; +import { postAPI } from "@/lib/api"; +import { supabase } from "@/auth/supabase"; +import { DetailDialog } from "@/components/archivos/DetailDialog"; import type { RefRow } from "@/types/RefRow"; // ———————————————————————————————————————————————————————————————— @@ -50,42 +59,51 @@ function extIcon(ext: string) { // ———————————————————————————————————————————————————————————————— // Componente principal // ———————————————————————————————————————————————————————————————— -export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (v: boolean) => void }) { - const router = useRouter() - const auth = useSupabaseAuth() - const role = auth.claims?.role +export function CreatePlanDialog({ + open, + onOpenChange, +}: { + open: boolean; + onOpenChange: (v: boolean) => void; +}) { + const router = useRouter(); + const auth = useSupabaseAuth(); + const role = auth.claims?.role; - const [saving, setSaving] = useState(false) - const [err, setErr] = useState(null) + const [saving, setSaving] = useState(false); + const [err, setErr] = useState(null); - const [facultadId, setFacultadId] = useState(auth.claims?.facultad_id ?? "") - const [carreraId, setCarreraId] = useState(auth.claims?.carrera_id ?? "") - const [nivel, setNivel] = useState("") + const [facultadId, setFacultadId] = useState(auth.claims?.facultad_id ?? ""); + const [carreraId, setCarreraId] = useState(auth.claims?.carrera_id ?? ""); + const [nivel, setNivel] = useState(""); const [prompt, setPrompt] = useState( "Genera un plan de estudios claro y realista: " - ) + ); - const [dbFiles, setDbFiles] = useState<{ - id: string; - titulo: string; - s3_file_path: string; - fecha_subida?: string; - tags?: string[]; - }[]>([]) - const [selectedFiles, setSelectedFiles] = useState([]) - const [lastSelectedIndex, setLastSelectedIndex] = useState(null) - const [searchTerm, setSearchTerm] = useState("") - const [currentPage, setCurrentPage] = useState(1) - const itemsPerPage = 10 - const debouncedSearchTerm = useDebounce(searchTerm, 300) + const [dbFiles, setDbFiles] = useState< + { + id: string; + titulo: string; + s3_file_path: string; + fecha_subida?: string; + tags?: string[]; + }[] + >([]); + const [selectedFiles, setSelectedFiles] = useState([]); + const [lastSelectedIndex, setLastSelectedIndex] = useState( + null + ); + const [searchTerm, setSearchTerm] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 10; + const debouncedSearchTerm = useDebounce(searchTerm, 300); const totalPages = Math.ceil(dbFiles.length / itemsPerPage); const [previewRow, setPreviewRow] = useState(null); - const lockFacultad = role === "secretario_academico" || role === "jefe_carrera" - const lockCarrera = role === "jefe_carrera" - - + const lockFacultad = + role === "secretario_academico" || role === "jefe_carrera"; + const lockCarrera = role === "jefe_carrera"; useEffect(() => { async function fetchDbFiles() { @@ -94,20 +112,25 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen .from("documentos") .select("documentos_id, titulo_archivo, fecha_subida, tags") .ilike("titulo_archivo", `%${debouncedSearchTerm}%`) - .range((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage - 1); + .range( + (currentPage - 1) * itemsPerPage, + currentPage * itemsPerPage - 1 + ); if (error) { console.error("Error fetching files from database:", error); return; } - setDbFiles((data || []).map((file: any) => ({ - id: file.documentos_id, - titulo: file.titulo_archivo, - s3_file_path: `prueba-referencias/documento_${file.documentos_id}.pdf`, - fecha_subida: file.fecha_subida, - tags: file.tags || [], - }))); + setDbFiles( + (data || []).map((file: any) => ({ + id: file.documentos_id, + titulo: file.titulo_archivo, + s3_file_path: `prueba-referencias/documento_${file.documentos_id}.pdf`, + fecha_subida: file.fecha_subida, + tags: file.tags || [], + })) + ); } catch (err) { console.error("Unexpected error fetching files:", err); } @@ -116,41 +139,59 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen if (open) fetchDbFiles(); }, [open, debouncedSearchTerm, currentPage]); - const isSelected = useCallback((path: string) => selectedFiles.includes(path), [selectedFiles]); + const isSelected = useCallback( + (path: string) => selectedFiles.includes(path), + [selectedFiles] + ); const toggleSelected = useCallback((id: string) => { - setSelectedFiles(prev => prev.includes(id) ? prev.filter(p => p !== id) : [...prev, id]); + setSelectedFiles((prev) => + prev.includes(id) ? prev.filter((p) => p !== id) : [...prev, id] + ); }, []); const replaceSelection = useCallback((id: string) => { setSelectedFiles([id]); }, []); - const rangeSelect = useCallback((start: number, end: number) => { - const [s, e] = start < end ? [start, end] : [end, start]; - const ids = dbFiles.slice(s, e + 1).map(f => f.id); - setSelectedFiles(prev => Array.from(new Set([...prev, ...ids]))); - }, [dbFiles]); + const rangeSelect = useCallback( + (start: number, end: number) => { + const [s, e] = start < end ? [start, end] : [end, start]; + const ids = dbFiles.slice(s, e + 1).map((f) => f.id); + setSelectedFiles((prev) => Array.from(new Set([...prev, ...ids]))); + }, + [dbFiles] + ); - const handleCardClick = useCallback((e: React.MouseEvent, index: number, file: { id: string }) => { - const id = file.id; + const handleCardClick = useCallback( + (e: React.MouseEvent, index: number, file: { id: string }) => { + const id = file.id; - if (e.shiftKey && lastSelectedIndex !== null) { - rangeSelect(lastSelectedIndex, index); - } else if (e.metaKey || e.ctrlKey) { - toggleSelected(id); - setLastSelectedIndex(index); - } else { - if (isSelected(id) && selectedFiles.length === 1) { - // si ya es el único seleccionado, des-selecciona - setSelectedFiles([]); - setLastSelectedIndex(null); - } else { - replaceSelection(id); + if (e.shiftKey && lastSelectedIndex !== null) { + rangeSelect(lastSelectedIndex, index); + } else if (e.metaKey || e.ctrlKey) { + toggleSelected(id); setLastSelectedIndex(index); + } else { + if (isSelected(id) && selectedFiles.length === 1) { + // si ya es el único seleccionado, des-selecciona + setSelectedFiles([]); + setLastSelectedIndex(null); + } else { + replaceSelection(id); + setLastSelectedIndex(index); + } } - } - }, [isSelected, lastSelectedIndex, rangeSelect, replaceSelection, selectedFiles.length, toggleSelected]); + }, + [ + isSelected, + lastSelectedIndex, + rangeSelect, + replaceSelection, + selectedFiles.length, + toggleSelected, + ] + ); const clearSelection = () => { setSelectedFiles([]); @@ -158,30 +199,51 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen }; async function crearConIA() { - setErr(null) - if (!carreraId) { setErr("Selecciona una carrera."); return } - setSaving(true) + setErr(null); + if (!carreraId) { + setErr("Selecciona una carrera."); + return; + } + setSaving(true); try { - const res = await postAPI("/api/generar/plan", { - carreraId, - prompt: prompt, - insert: true, - files: selectedFiles, - created_by: auth.user?.id, - }) - const newId = (res as any)?.id || (res as any)?.plan?.id || (res as any)?.data?.id + + const { + data: { session }, + } = await supabase.auth.getSession(); + const token = session?.access_token; + + const { data, error } = await supabase.functions.invoke( + "crear-plan-estudios", + { + headers: { Authorization: `Bearer ${token}` }, + body: { + carreraId, + prompt_usuario: prompt, + insert: true, + archivos_a_usar: [], + }, + } + ); + if (error) throw error; + + const res = JSON.parse(data as string); + + const newId = + (res as any)?.id || (res as any)?.plan?.id || (res as any)?.data?.id; if (newId) { - onOpenChange(false) - router.invalidate() - router.navigate({ to: "/plan/$planId", params: { planId: newId } }) + onOpenChange(false); + router.invalidate(); + router.navigate({ to: "/plan/$planId", params: { planId: newId } }); } else { - onOpenChange(false) - router.invalidate() + onOpenChange(false); + router.invalidate(); } } catch (e: any) { - setErr(typeof e?.message === "string" ? e.message : "Error al generar el plan.") + setErr( + typeof e?.message === "string" ? e.message : "Error al generar el plan." + ); } finally { - setSaving(false) + setSaving(false); } } @@ -192,7 +254,9 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen - Nuevo plan de estudios (IA) + + Nuevo plan de estudios (IA) +
@@ -215,7 +279,10 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen { setFacultadId(id); setCarreraId("") }} + onChange={(id) => { + setFacultadId(id); + setCarreraId(""); + }} disabled={lockFacultad} placeholder="Elige una facultad…" /> @@ -228,7 +295,11 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen value={carreraId} onChange={setCarreraId} disabled={!facultadId || lockCarrera} - placeholder={facultadId ? "Elige una carrera…" : "Selecciona una facultad primero"} + placeholder={ + facultadId + ? "Elige una carrera…" + : "Selecciona una facultad primero" + } />
@@ -246,11 +317,19 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen
{selectedFiles.length > 0 ? ( - {selectedFiles.length} seleccionado{selectedFiles.length > 1 ? 's' : ''} - + {selectedFiles.length} seleccionado + {selectedFiles.length > 1 ? "s" : ""} + ) : ( - Tip: ⇧ para seleccionar rango, ⌘/Ctrl para múltiples. + + Tip: ⇧ para seleccionar rango, ⌘/Ctrl para múltiples. + )}
@@ -258,12 +337,15 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen {/* Grid de archivos con selección tipo file manager */}
-
+
{dbFiles.map((file, index) => { const ext = fileExt(file.titulo); const selected = isSelected(file.id); console.log(file); - + return (
{ - if (e.key === 'Enter' || e.key === ' ') { + if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleCardClick(e as any, index, file); } @@ -296,31 +378,51 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen className={[ "group relative rounded-2xl border bg-white p-4 text-left shadow-sm transition", "hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500", - selected ? "border-blue-500 ring-2 ring-blue-500 shadow-md" : "border-neutral-200 hover:border-neutral-300", - ].join(' ')} + selected + ? "border-blue-500 ring-2 ring-blue-500 shadow-md" + : "border-neutral-200 hover:border-neutral-300", + ].join(" ")} > {/* Outline animado tipo file manager */} - +
- {extIcon(ext)} + + {extIcon(ext)} +
-

{file.titulo}

+

+ {file.titulo} +

{file.fecha_subida ? ( -

{new Date(file.fecha_subida).toLocaleDateString()}

+

+ {new Date(file.fecha_subida).toLocaleDateString()} +

) : ( -

Fecha desconocida

+

+ Fecha desconocida +

)} - + {file.tags && file.tags.length > 0 && (
{file.tags.map((tag, i) => ( - #{tag} + + #{tag} + ))}
)} @@ -347,50 +449,69 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen fecha_subida: file.fecha_subida ?? null, tags: file.tags ?? null, instrucciones: "", - }) + }); }} - >Previsualizar + > + Previsualizar +
{/* Footer compacto */}
{ext.toUpperCase()} - {selected ? Seleccionado : Click para seleccionar} + {selected ? ( + Seleccionado + ) : ( + + Click para seleccionar + + )}
- ) + ); })} {dbFiles.length === 0 && ( -

No se encontraron archivos.

+

+ No se encontraron archivos. +

)}
{/* Paginación mejorada */} {dbFiles.length > itemsPerPage && (
-
Página {currentPage} de {totalPages}
+
+ Página {currentPage} de {totalPages} +
+ > + Anterior + { - const v = parseInt(e.target.value || '1', 10); - if (!isNaN(v)) setCurrentPage(Math.min(Math.max(v, 1), totalPages)); + const v = parseInt(e.target.value || "1", 10); + if (!isNaN(v)) + setCurrentPage(Math.min(Math.max(v, 1), totalPages)); }} /> + onClick={() => + setCurrentPage((p) => Math.min(p + 1, totalPages)) + } + > + Siguiente +
)} @@ -400,19 +521,26 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen {err &&
{err}
} - - + {previewRow && ( - setPreviewRow(null)} - /> + setPreviewRow(null)} /> )}
- ) + ); } diff --git a/src/components/planes/academic-sections.tsx b/src/components/planes/academic-sections.tsx index ed47920..d88082f 100644 --- a/src/components/planes/academic-sections.tsx +++ b/src/components/planes/academic-sections.tsx @@ -1,16 +1,26 @@ -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" +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" - +import AIChatModal from "../ai/AIChatModal"; /* ===================================================== Query keys & fetcher @@ -18,33 +28,29 @@ import AIChatModal from "../ai/AIChatModal" 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 -} + 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( - `objetivo_general, sistema_evaluacion, perfil_ingreso, perfil_egreso, - competencias_genericas, competencias_especificas, indicadores_desempeno, - pertinencia, prompt` - ) + .select(`*`) .eq("id", planId) - .single() - if (error) throw error - return (data ?? {}) as PlanTextFields + .single(); + if (error) throw error; + return (data ?? {}) as PlanTextFields; } export const planTextOptions = (planId: string) => @@ -52,283 +58,503 @@ export const planTextOptions = (planId: string) => 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] + 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})` +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) +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 + return ; } - const content = Array.isArray(text) ? text.join("\n• ") : text - const rendered = Array.isArray(text) ? `• ${content}` : content + 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) +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)) +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("") + 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 + 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 } + 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 😓") + if (ctx?.prev) qc.setQueryData(planKeys.byId(planId), ctx.prev); + toast.error((e as any)?.message || "No se pudo guardar 😓"); }, onSuccess: () => { - toast.success("Guardado ✅") + toast.success("Guardado ✅"); }, onSettled: async () => { - await qc.invalidateQueries({ queryKey: planKeys.byId(planId) }) + await qc.invalidateQueries({ queryKey: planKeys.byId(planId) }); }, - }) + }); const sections = useMemo( () => [ - { id: "sec-obj", title: "Objetivo general", icon: Icons.Target, key: "objetivo_general" as const, mono: false }, - { id: "sec-eval", title: "Sistema de evaluación", icon: Icons.CheckCircle2, key: "sistema_evaluacion" as const, mono: false }, - { id: "sec-ing", title: "Perfil de ingreso", icon: Icons.UserPlus, key: "perfil_ingreso" as const, mono: false }, - { id: "sec-egr", title: "Perfil de egreso", icon: Icons.GraduationCap, key: "perfil_egreso" as const, mono: false }, - { id: "sec-cg", title: "Competencias genéricas", icon: Icons.Sparkles, key: "competencias_genericas" as const, mono: false }, - { id: "sec-ce", title: "Competencias específicas", icon: Icons.Cog, key: "competencias_especificas" as const, mono: false }, - { id: "sec-ind", title: "Indicadores de desempeño", icon: Icons.LineChart, key: "indicadores_desempeno" as const, mono: false }, - { id: "sec-per", title: "Pertinencia", icon: Icons.Lightbulb, key: "pertinencia" as const, mono: false }, - { 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 } + { + 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) - + ); + const [iaContext, setIaContext] = useState<{ + key: keyof PlanTextFields; + title: string; + content: string; + } | null>(null); return ( <>
{sections.map((s) => { - const text = plan[s.key] ?? null - return ( - - {s.key === "historico" ? ( - <> - - - - ) : ( - <> - -
+ const text = String(plan[s.key]) ?? null; + return ( + + {s.key === "historico" ? ( + <> - {s.key !== "prompt" && ( - + + ) : ( + <> + +
+ - )} -
- - )} -
- ) - })} - + Copiar + + {s.key !== "prompt" && ( + + )} +
+ + )} +
+ ); + })}
- {/* Diálogo de edición */} - { if (!o) setEditing(null) }}> - - - - {editing ? `Editar: ${sections.find((x) => x.key === editing.key)?.title}` : ""} - - + {/* Diálogo de edición */} + { + if (!o) setEditing(null); + }} + > + + + + {editing + ? `Editar: ${sections.find((x) => x.key === editing.key)?.title}` + : ""} + + -