Compare commits
1 Commits
8bb8399ec5
...
feature/Hi
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e884f20c5 |
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>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { Textarea } from "@/components/ui/textarea"
|
|||||||
import { supabase,useSupabaseAuth } from "@/auth/supabase"
|
import { supabase,useSupabaseAuth } from "@/auth/supabase"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
|
import { HistorialCambiosModal } from "../historico/HistorialCambiosModal"
|
||||||
|
|
||||||
|
|
||||||
/* =====================================================
|
/* =====================================================
|
||||||
@@ -27,6 +28,7 @@ export type PlanTextFields = {
|
|||||||
indicadores_desempeno?: string | string[] | null
|
indicadores_desempeno?: string | string[] | null
|
||||||
pertinencia?: string | string[] | null
|
pertinencia?: string | string[] | null
|
||||||
prompt?: string | null
|
prompt?: string | null
|
||||||
|
historico?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchPlanText(planId: string): Promise<PlanTextFields> {
|
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 }) {
|
export function AcademicSections({ planId, color }: { planId: string; color?: string | null }) {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const auth = useSupabaseAuth()
|
const auth = useSupabaseAuth()
|
||||||
|
const [openHistorial, setOpenHistorial] = useState(false)
|
||||||
if(!planId) return <div>Cargando…</div>
|
if(!planId) return <div>Cargando…</div>
|
||||||
const { data: plan } = useSuspenseQuery(planTextOptions(planId))
|
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-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-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-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 }
|
||||||
],
|
],
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
@@ -163,38 +167,48 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
|
|||||||
<>
|
<>
|
||||||
<div className="grid gap-5 md:grid-cols-2">
|
<div className="grid gap-5 md:grid-cols-2">
|
||||||
{sections.map((s) => {
|
{sections.map((s) => {
|
||||||
const text = plan[s.key] ?? null
|
const text = plan[s.key] ?? null
|
||||||
return (
|
return (
|
||||||
<SectionPanel key={s.id} id={s.id} title={s.title} icon={s.icon} color={color}>
|
<SectionPanel key={s.id} id={s.id} title={s.title} icon={s.icon} color={color}>
|
||||||
<ExpandableText text={text} mono={s.mono} />
|
{s.key === "historico" ? (
|
||||||
<div className="mt-4 flex flex-wrap gap-2">
|
<Button variant="outline" size="sm" onClick={() => setOpenHistorial(true)}>
|
||||||
<Button
|
Ver historial
|
||||||
variant="outline"
|
</Button>
|
||||||
size="sm"
|
) : (
|
||||||
disabled={!text || (Array.isArray(text) && text.length === 0)}
|
<>
|
||||||
onClick={() => {
|
<ExpandableText text={text} mono={s.mono} />
|
||||||
const toCopy = Array.isArray(text) ? text.join("\n") : (text ?? "")
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
if (toCopy) navigator.clipboard.writeText(toCopy)
|
<Button
|
||||||
}}
|
variant="outline"
|
||||||
>
|
|
||||||
Copiar
|
|
||||||
</Button>
|
|
||||||
{s.key !== "prompt" &&
|
|
||||||
(<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
size="sm"
|
||||||
|
disabled={!text || (Array.isArray(text) && text.length === 0)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const current = Array.isArray(text) ? text.join("\n") : (text ?? "")
|
const toCopy = Array.isArray(text) ? text.join("\n") : (text ?? "")
|
||||||
setEditing({ key: s.key, title: s.title })
|
if (toCopy) navigator.clipboard.writeText(toCopy)
|
||||||
setDraft(current)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Editar
|
Copiar
|
||||||
</Button>)}
|
</Button>
|
||||||
</div>
|
{s.key !== "prompt" && (
|
||||||
</SectionPanel>
|
<Button
|
||||||
)
|
variant="ghost"
|
||||||
})}
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
const current = Array.isArray(text) ? text.join("\n") : (text ?? "")
|
||||||
|
setEditing({ key: s.key, title: s.title })
|
||||||
|
setDraft(current)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Editar
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SectionPanel>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Diálogo de edición */}
|
{/* Diálogo de edición */}
|
||||||
@@ -257,7 +271,14 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
<HistorialCambiosModal
|
||||||
|
open={openHistorial}
|
||||||
|
onClose={() => setOpenHistorial(false)}
|
||||||
|
planId={planId}
|
||||||
|
onRestore={async (key, value) => {
|
||||||
|
updateField.mutate({ key, value })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user