wip
This commit is contained in:
@@ -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<string | null>(null)
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [err, setErr] = useState<string | null>(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<string[]>([])
|
||||
const [lastSelectedIndex, setLastSelectedIndex] = useState<number | null>(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<string[]>([]);
|
||||
const [lastSelectedIndex, setLastSelectedIndex] = useState<number | null>(
|
||||
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<RefRow | null>(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
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="min-w-[65vw] max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="font-mono">Nuevo plan de estudios (IA)</DialogTitle>
|
||||
<DialogTitle className="font-mono">
|
||||
Nuevo plan de estudios (IA)
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
@@ -215,7 +279,10 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen
|
||||
<Label>Facultad</Label>
|
||||
<FacultadCombobox
|
||||
value={facultadId}
|
||||
onChange={(id) => { 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"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -246,11 +317,19 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen
|
||||
<div className="text-sm text-neutral-600">
|
||||
{selectedFiles.length > 0 ? (
|
||||
<span>
|
||||
{selectedFiles.length} seleccionado{selectedFiles.length > 1 ? 's' : ''}
|
||||
<button className="ml-3 underline hover:no-underline" onClick={clearSelection}>Limpiar</button>
|
||||
{selectedFiles.length} seleccionado
|
||||
{selectedFiles.length > 1 ? "s" : ""}
|
||||
<button
|
||||
className="ml-3 underline hover:no-underline"
|
||||
onClick={clearSelection}
|
||||
>
|
||||
Limpiar
|
||||
</button>
|
||||
</span>
|
||||
) : (
|
||||
<span>Tip: ⇧ para seleccionar rango, ⌘/Ctrl para múltiples.</span>
|
||||
<span>
|
||||
Tip: ⇧ para seleccionar rango, ⌘/Ctrl para múltiples.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -258,12 +337,15 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen
|
||||
{/* Grid de archivos con selección tipo file manager */}
|
||||
<div className="md:col-span-2 space-y-1">
|
||||
<Label>Archivos de referencia (opcional)</Label>
|
||||
<div role="grid" className="grid gap-4 xs:grid-cols-2 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
|
||||
<div
|
||||
role="grid"
|
||||
className="grid gap-4 xs:grid-cols-2 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"
|
||||
>
|
||||
{dbFiles.map((file, index) => {
|
||||
const ext = fileExt(file.titulo);
|
||||
const selected = isSelected(file.id);
|
||||
console.log(file);
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
key={file.id}
|
||||
@@ -285,10 +367,10 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen
|
||||
fecha_subida: file.fecha_subida ?? null,
|
||||
tags: file.tags ?? null,
|
||||
instrucciones: "",
|
||||
})
|
||||
});
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
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 */}
|
||||
<span className={[
|
||||
"pointer-events-none absolute inset-0 rounded-2xl ring-2 ring-blue-500",
|
||||
"opacity-0 group-aria-selected:opacity-100 group-focus:opacity-100 transition-opacity",
|
||||
].join(' ')} />
|
||||
<span
|
||||
className={[
|
||||
"pointer-events-none absolute inset-0 rounded-2xl ring-2 ring-blue-500",
|
||||
"opacity-0 group-aria-selected:opacity-100 group-focus:opacity-100 transition-opacity",
|
||||
].join(" ")}
|
||||
/>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border bg-neutral-50">
|
||||
<span className="text-lg" aria-hidden>{extIcon(ext)}</span>
|
||||
<span className="text-lg" aria-hidden>
|
||||
{extIcon(ext)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="font-semibold text-sm md:text-base truncate" title={file.titulo}>{file.titulo}</h3>
|
||||
<h3
|
||||
className="font-semibold text-sm md:text-base truncate"
|
||||
title={file.titulo}
|
||||
>
|
||||
{file.titulo}
|
||||
</h3>
|
||||
{file.fecha_subida ? (
|
||||
<p className="text-xs text-neutral-600">{new Date(file.fecha_subida).toLocaleDateString()}</p>
|
||||
<p className="text-xs text-neutral-600">
|
||||
{new Date(file.fecha_subida).toLocaleDateString()}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-neutral-500">Fecha desconocida</p>
|
||||
<p className="text-xs text-neutral-500">
|
||||
Fecha desconocida
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
{file.tags && file.tags.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{file.tags.map((tag, i) => (
|
||||
<span key={i} className="text-[10px] px-2 py-0.5 bg-neutral-100 text-neutral-700 rounded-full">#{tag}</span>
|
||||
<span
|
||||
key={i}
|
||||
className="text-[10px] px-2 py-0.5 bg-neutral-100 text-neutral-700 rounded-full"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -347,50 +449,69 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen
|
||||
fecha_subida: file.fecha_subida ?? null,
|
||||
tags: file.tags ?? null,
|
||||
instrucciones: "",
|
||||
})
|
||||
});
|
||||
}}
|
||||
>Previsualizar</Button>
|
||||
>
|
||||
Previsualizar
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Footer compacto */}
|
||||
<div className="mt-4 flex items-center justify-between text-xs text-neutral-600">
|
||||
<span className="truncate">{ext.toUpperCase()}</span>
|
||||
{selected ? <span className="font-medium">Seleccionado</span> : <span className="opacity-60">Click para seleccionar</span>}
|
||||
{selected ? (
|
||||
<span className="font-medium">Seleccionado</span>
|
||||
) : (
|
||||
<span className="opacity-60">
|
||||
Click para seleccionar
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
|
||||
{dbFiles.length === 0 && (
|
||||
<p className="text-sm text-neutral-500">No se encontraron archivos.</p>
|
||||
<p className="text-sm text-neutral-500">
|
||||
No se encontraron archivos.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Paginación mejorada */}
|
||||
{dbFiles.length > itemsPerPage && (
|
||||
<div className="mt-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="text-sm text-neutral-700">Página {currentPage} de {totalPages}</div>
|
||||
<div className="text-sm text-neutral-700">
|
||||
Página {currentPage} de {totalPages}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
|
||||
>Anterior</Button>
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Input
|
||||
className="h-8 w-16 text-center"
|
||||
value={currentPage}
|
||||
onChange={(e) => {
|
||||
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));
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() => setCurrentPage((p) => Math.min(p + 1, totalPages))}
|
||||
>Siguiente</Button>
|
||||
onClick={() =>
|
||||
setCurrentPage((p) => Math.min(p + 1, totalPages))
|
||||
}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -400,19 +521,26 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen
|
||||
{err && <div className="text-sm text-red-600">{err}</div>}
|
||||
|
||||
<DialogFooter className="flex flex-col-reverse sm:flex-row gap-2">
|
||||
<Button variant="outline" className="w-full sm:w-auto" onClick={() => onOpenChange(false)}>Cancelar</Button>
|
||||
<Button className="w-full sm:w-auto" onClick={crearConIA} disabled={saving}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full sm:w-auto"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
onClick={crearConIA}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? "Generando…" : "Generar y crear"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
{previewRow && (
|
||||
<DetailDialog
|
||||
row={previewRow}
|
||||
onClose={() => setPreviewRow(null)}
|
||||
/>
|
||||
<DetailDialog row={previewRow} onClose={() => setPreviewRow(null)} />
|
||||
)}
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user