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: file.titulo_archivo, 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((path: string) => { setSelectedFiles(prev => prev.includes(path) ? prev.filter(p => p !== path) : [...prev, path]); }, []); const replaceSelection = useCallback((path: string) => { setSelectedFiles([path]); }, []); const rangeSelect = useCallback((start: number, end: number) => { const [s, e] = start < end ? [start, end] : [end, start]; const paths = dbFiles.slice(s, e + 1).map(f => f.s3_file_path); setSelectedFiles(prev => Array.from(new Set([...prev, ...paths]))); }, [dbFiles]); const handleCardClick = useCallback((e: React.MouseEvent, index: number, file: { s3_file_path: string }) => { const path = file.s3_file_path; if (e.shiftKey && lastSelectedIndex !== null) { rangeSelect(lastSelectedIndex, index); } else if (e.metaKey || e.ctrlKey) { toggleSelected(path); setLastSelectedIndex(index); } else { if (isSelected(path) && selectedFiles.length === 1) { // si ya es el único seleccionado, des-selecciona setSelectedFiles([]); setLastSelectedIndex(null); } else { replaceSelection(path); 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 res = await postAPI("/api/generar/plan", { carreraId, prompt: prompt, insert: true, files: selectedFiles, uuid: auth.user?.id, }) 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)