Compare commits
6 Commits
feature/Pd
...
feature/Hi
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e884f20c5 | |||
| 8bb8399ec5 | |||
| 6e2b3d72f1 | |||
| 0c5c3f935b | |||
| 8da08b6bf1 | |||
| 1fe8f2b6a8 |
92
src/components/historico/HistorialCambiosModal.tsx
Normal file
92
src/components/historico/HistorialCambiosModal.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { supabase } from "@/auth/supabase"
|
||||
import ReactMarkdown from "react-markdown"
|
||||
import { useSupabaseAuth } from "@/auth/supabase"
|
||||
|
||||
export function HistorialCambiosModal({
|
||||
open,
|
||||
onClose,
|
||||
planId,
|
||||
onRestore, // 🔥 recibiremos una función del padre para restaurar
|
||||
}: {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
planId: string
|
||||
onRestore: (key: string, value: string) => void
|
||||
}) {
|
||||
const auth = useSupabaseAuth()
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["historico_cambios", planId, auth.user?.id],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from("historico_cambios")
|
||||
.select("id, json_cambios, user_id, created_at")
|
||||
.eq("id_plan_estudios", planId)
|
||||
.eq("user_id", auth.user?.id) // ✅ filtro por usuario actual
|
||||
.order("created_at", { ascending: false })
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
},
|
||||
enabled: !!auth.user?.id, // ✅ solo corre si hay usuario autenticado
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Histórico de cambios</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{isLoading && <p className="text-sm text-gray-500">Cargando historial...</p>}
|
||||
{error && <p className="text-red-500 text-sm">Error al cargar: {String(error)}</p>}
|
||||
{!isLoading && !error && (!data || data.length === 0) && (
|
||||
<p className="text-gray-500 text-sm">No hay cambios registrados.</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-4 max-h-[60vh] overflow-y-auto">
|
||||
{data?.map((item) => {
|
||||
const diff = item.json_cambios?.[0]
|
||||
const key = diff?.path?.replace("/", "")
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="rounded-lg border p-3 bg-white/70 dark:bg-neutral-900/50"
|
||||
>
|
||||
<div className="flex justify-between text-xs text-neutral-500 mb-2">
|
||||
<span>Usuario: {item.user_id || "Desconocido"}</span>
|
||||
<span>{new Date(item.created_at).toLocaleString()}</span>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-700 font-mono whitespace-pre-wrap">
|
||||
<p><strong>Campo:</strong> {key}</p>
|
||||
<p><strong>Antes:</strong> {diff?.from || "—"}</p>
|
||||
<p><strong>Después:</strong> {diff?.value || "—"}</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="mt-2"
|
||||
onClick={() => onRestore(key, diff.from)}
|
||||
>
|
||||
Restaurar
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-right">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cerrar
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -104,7 +104,7 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen
|
||||
setDbFiles((data || []).map((file: any) => ({
|
||||
id: file.documentos_id,
|
||||
titulo: file.titulo_archivo,
|
||||
s3_file_path: file.titulo_archivo,
|
||||
s3_file_path: `prueba-referencias/documento_${file.documentos_id}.pdf`,
|
||||
fecha_subida: file.fecha_subida,
|
||||
tags: file.tags || [],
|
||||
})));
|
||||
@@ -118,35 +118,35 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen
|
||||
|
||||
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 toggleSelected = useCallback((id: string) => {
|
||||
setSelectedFiles(prev => prev.includes(id) ? prev.filter(p => p !== id) : [...prev, id]);
|
||||
}, []);
|
||||
|
||||
const replaceSelection = useCallback((path: string) => {
|
||||
setSelectedFiles([path]);
|
||||
const replaceSelection = useCallback((id: string) => {
|
||||
setSelectedFiles([id]);
|
||||
}, []);
|
||||
|
||||
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])));
|
||||
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: { s3_file_path: string }) => {
|
||||
const path = file.s3_file_path;
|
||||
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(path);
|
||||
toggleSelected(id);
|
||||
setLastSelectedIndex(index);
|
||||
} else {
|
||||
if (isSelected(path) && selectedFiles.length === 1) {
|
||||
if (isSelected(id) && selectedFiles.length === 1) {
|
||||
// si ya es el único seleccionado, des-selecciona
|
||||
setSelectedFiles([]);
|
||||
setLastSelectedIndex(null);
|
||||
} else {
|
||||
replaceSelection(path);
|
||||
replaceSelection(id);
|
||||
setLastSelectedIndex(index);
|
||||
}
|
||||
}
|
||||
@@ -167,7 +167,7 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen
|
||||
prompt: prompt,
|
||||
insert: true,
|
||||
files: selectedFiles,
|
||||
uuid: auth.user?.id,
|
||||
created_by: auth.user?.id,
|
||||
})
|
||||
const newId = (res as any)?.id || (res as any)?.plan?.id || (res as any)?.data?.id
|
||||
if (newId) {
|
||||
@@ -261,7 +261,7 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen
|
||||
<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.s3_file_path);
|
||||
const selected = isSelected(file.id);
|
||||
console.log(file);
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Textarea } from "@/components/ui/textarea"
|
||||
import { supabase,useSupabaseAuth } from "@/auth/supabase"
|
||||
import { toast } from "sonner"
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { HistorialCambiosModal } from "../historico/HistorialCambiosModal"
|
||||
|
||||
|
||||
/* =====================================================
|
||||
@@ -27,6 +28,7 @@ export type PlanTextFields = {
|
||||
indicadores_desempeno?: string | string[] | null
|
||||
pertinencia?: string | string[] | null
|
||||
prompt?: string | null
|
||||
historico?: string | null
|
||||
}
|
||||
|
||||
async function fetchPlanText(planId: string): Promise<PlanTextFields> {
|
||||
@@ -112,6 +114,7 @@ function SectionPanel({ title, icon: Icon, color, children, id }: { title: strin
|
||||
export function AcademicSections({ planId, color }: { planId: string; color?: string | null }) {
|
||||
const qc = useQueryClient()
|
||||
const auth = useSupabaseAuth()
|
||||
const [openHistorial, setOpenHistorial] = useState(false)
|
||||
if(!planId) return <div>Cargando…</div>
|
||||
const { data: plan } = useSuspenseQuery(planTextOptions(planId))
|
||||
|
||||
@@ -155,6 +158,7 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
|
||||
{ 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 },
|
||||
{ id: "sec-hist", title: "Histórico de cambios", icon: Icons.History, key: "historico" as const, mono: false }
|
||||
],
|
||||
[]
|
||||
)
|
||||
@@ -166,6 +170,12 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
|
||||
const text = plan[s.key] ?? null
|
||||
return (
|
||||
<SectionPanel key={s.id} id={s.id} title={s.title} icon={s.icon} color={color}>
|
||||
{s.key === "historico" ? (
|
||||
<Button variant="outline" size="sm" onClick={() => setOpenHistorial(true)}>
|
||||
Ver historial
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<ExpandableText text={text} mono={s.mono} />
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<Button
|
||||
@@ -179,8 +189,8 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
|
||||
>
|
||||
Copiar
|
||||
</Button>
|
||||
{s.key !== "prompt" &&
|
||||
(<Button
|
||||
{s.key !== "prompt" && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
@@ -190,11 +200,15 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
|
||||
}}
|
||||
>
|
||||
Editar
|
||||
</Button>)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</SectionPanel>
|
||||
)
|
||||
})}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Diálogo de edición */}
|
||||
@@ -257,7 +271,14 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<HistorialCambiosModal
|
||||
open={openHistorial}
|
||||
onClose={() => setOpenHistorial(false)}
|
||||
planId={planId}
|
||||
onRestore={async (key, value) => {
|
||||
updateField.mutate({ key, value })
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user