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"; // ———————————————————————————————————————————————————————————————— // Utils // ———————————————————————————————————————————————————————————————— function useDebounce(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const handler = setTimeout(() => setDebouncedValue(value), delay); return () => clearTimeout(handler); }, [value, delay]); return debouncedValue; } function fileExt(title?: string) { const m = (title || "").match(/\.([a-z0-9]{2,5})$/i); return m ? m[1].toLowerCase() : "file"; } function extIcon(ext: string) { // Usa íconos de lucide si ya los tienes en tu stack; si no, fallback a emojis const map: Record = { pdf: "📄", doc: "📝", docx: "📝", xls: "📊", xlsx: "📊", csv: "📑", ppt: "🖼️", pptx: "🖼️", txt: "📃", md: "🗒️", json: "{ }", }; return map[ext] || "📁"; } // ———————————————————————————————————————————————————————————————— // Componente principal // ———————————————————————————————————————————————————————————————— 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 [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 totalPages = Math.ceil(dbFiles.length / itemsPerPage); const [previewRow, setPreviewRow] = useState(null); const lockFacultad = role === "secretario_academico" || role === "jefe_carrera"; const lockCarrera = role === "jefe_carrera"; useEffect(() => { async function fetchDbFiles() { try { const { data, error } = await supabase .from("documentos") .select("documentos_id, titulo_archivo, fecha_subida, tags") .ilike("titulo_archivo", `%${debouncedSearchTerm}%`) .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 || [], })) ); } catch (err) { console.error("Unexpected error fetching files:", err); } } if (open) fetchDbFiles(); }, [open, debouncedSearchTerm, currentPage]); 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] ); }, []); 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 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); setLastSelectedIndex(index); } } }, [ isSelected, lastSelectedIndex, rangeSelect, replaceSelection, selectedFiles.length, toggleSelected, ] ); const clearSelection = () => { setSelectedFiles([]); setLastSelectedIndex(null); }; async function crearConIA() { setErr(null); if (!carreraId) { setErr("Selecciona una carrera."); return; } setSaving(true); try { 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 } }); } else { onOpenChange(false); router.invalidate(); } } catch (e: any) { setErr( typeof e?.message === "string" ? e.message : "Error al generar el plan." ); } finally { setSaving(false); } } // ———————————————————————————————————————————————————————————————— // Render // ———————————————————————————————————————————————————————————————— return ( Nuevo plan de estudios (IA)