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 } from "@/auth/supabase" import { toast } from "sonner" import ReactMarkdown from 'react-markdown' /* ===================================================== 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 } 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` ) .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() 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-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 }, ], [] ) return ( <>
{sections.map((s) => { const text = plan[s.key] ?? null return (
{s.key !== "prompt" && ()}
) })}
{/* Diálogo de edición */} { if (!o) setEditing(null) }}> {editing ? `Editar: ${sections.find((x) => x.key === editing.key)?.title}` : ""}