// routes/_authenticated/archivos.tsx import { createFileRoute, useRouter } from "@tanstack/react-router" import { use, useMemo, useState } from "react" import { supabase, useSupabaseAuth } from "@/auth/supabase" import * as Icons from "lucide-react" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Label } from "@/components/ui/label" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Textarea } from "@/components/ui/textarea" import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select" import { DetailDialog } from "@/components/archivos/DetailDialog" import type { RefRow } from "@/types/RefRow" import { uuid } from "zod" export const Route = createFileRoute("/_authenticated/archivos")({ component: RouteComponent, loader: async () => { const { data, error } = await supabase .from("documentos") .select("*") .order("fecha_subida", { ascending: false }) .limit(200) if (error) throw error return (data ?? []) as RefRow[] }, }) function chipTint(ok?: boolean | null) { return ok ? "bg-emerald-50 text-emerald-700 border-emerald-200" : "bg-amber-50 text-amber-800 border-amber-200" } function RouteComponent() { const router = useRouter() const rows = Route.useLoaderData() as RefRow[] const [q, setQ] = useState("") const [estado, setEstado] = useState<"todos" | "proc" | "pend">("todos") const [scope, setScope] = useState<"todos" | "internos" | "externos">("todos") const [viewing, setViewing] = useState(null) const [uploadOpen, setUploadOpen] = useState(false) const filtered = useMemo(() => { const t = q.trim().toLowerCase() return rows.filter((r) => { if (estado === "proc" && !r.procesado) return false if (estado === "pend" && r.procesado) return false if (scope === "internos" && !r.interno) return false if (scope === "externos" && r.interno) return false if (!t) return true const hay = [r.titulo_archivo, r.descripcion, r.fuente_autoridad, r.tipo_contenido, ...(r.tags ?? [])] .filter(Boolean) .some((v) => String(v).toLowerCase().includes(t)) return hay }) }, [rows, q, estado, scope]) async function remove(id: string) { if (!confirm("¿Eliminar archivo de referencia?")) return const { error } = await supabase .from("documentos") .delete() .eq("documentos_id", id) if (error) return alert(error.message) router.invalidate() } return (
Archivos de referencia
setQ(e.target.value)} placeholder="Buscar por título, etiqueta, fuente…" className="pl-8" />
{filtered.map((r) => (

{r.titulo_archivo ?? "(Sin título)"}

{r.procesado ? "Procesado" : "Pendiente"}
{r.tipo_contenido && {r.tipo_contenido}} {r.interno != null && ( {r.interno ? "Interno" : "Externo"} )} {r.fecha_subida && ( {new Date(r.fecha_subida).toLocaleDateString()} )}
{r.descripcion && (

{r.descripcion}

)} {r.tags && r.tags.length > 0 && (
{r.tags.map((t, i) => ( #{t} ))}
)}
))}
{!filtered.length && (
No hay archivos
)}
{/* Detalle */} setViewing(null)} /> {/* Subida */} router.invalidate()} />
) } /* ========= Subida ========= */ function UploadDialog({ open, onOpenChange, onDone, }: { open: boolean; onOpenChange: (o: boolean) => void; onDone: () => void }) { const supabaseAuth = useSupabaseAuth() const [file, setFile] = useState(null) const [instrucciones, setInstrucciones] = useState("") const [tags, setTags] = useState("") const [interno, setInterno] = useState(true) const [fuente, setFuente] = useState("") const [subiendo, setSubiendo] = useState(false) async function toBase64(f: File): Promise { const buf = await f.arrayBuffer() const bytes = new Uint8Array(buf) let binary = "" for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]) return btoa(binary) } async function upload() { if (!file) { alert("Selecciona un archivo"); return } if (!instrucciones.trim()) { alert("Escribe las instrucciones"); return } setSubiendo(true) try { const fileBase64 = await toBase64(file) // Enviamos al motor (inserta en la tabla si insert=true) const res = await fetch(`${import.meta.env.VITE_BACK_ORIGIN}/api/upload/documento`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt: instrucciones, fileBase64, insert: true, uuid: supabaseAuth.user?.id ?? null, }), }) if (!res.ok) { const txt = await res.text() throw new Error(txt || "Error al subir") } // Ajustes extra (tags, interno, fuente) si el motor no los llenó // Intentamos leer el id que regrese el servicio; si no, solo invalidamos. let createdId: string | null = null try { const payload = await res.json() createdId = payload?.documentos_id || payload?.id || payload?.data?.documentos_id || null } catch { /* noop */ } if (createdId && (tags.trim() || fuente.trim() || typeof interno === "boolean")) { await supabase .from("documentos") .update({ tags: tags.trim() ? tags.split(",").map((s) => s.trim()).filter(Boolean) : undefined, fuente_autoridad: fuente.trim() || undefined, interno, }) .eq("documentos_id", createdId) } onOpenChange(false) onDone() } catch (e: any) { alert(e?.message ?? "Error al subir el documento") } finally { setSubiendo(false) } } return ( Nuevo archivo de referencia Sube un documento y escribe instrucciones para su procesamiento. Se guardará en la base y se marcará como procesado cuando termine el flujo.
setFile(e.target.files?.[0] ?? null)} /> {file && (
{file.name} · {(file.size / 1024).toFixed(1)} KB
)}