From db5465032e4eb061078b27c36cf814e300c723d4 Mon Sep 17 00:00:00 2001 From: "Roberto.silva" Date: Wed, 4 Feb 2026 16:05:05 -0600 Subject: [PATCH 1/2] =?UTF-8?q?Guardado=20autom=C3=A1tico=20fix=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/planes/$planId/_detalle/index.tsx | 60 +++++++++++++++++++- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/src/routes/planes/$planId/_detalle/index.tsx b/src/routes/planes/$planId/_detalle/index.tsx index d4582e4..1a8e457 100644 --- a/src/routes/planes/$planId/_detalle/index.tsx +++ b/src/routes/planes/$planId/_detalle/index.tsx @@ -112,15 +112,68 @@ function DatosGeneralesPage() { }, [data]) // 3. Manejadores de acciones (Ahora como funciones locales) - const handleEdit = (campo: DatosGeneralesField) => { - setEditingId(campo.id) - setEditValue(campo.value) + const handleEdit = (nuevoCampo: DatosGeneralesField) => { + // 1. SI YA ESTÁBAMOS EDITANDO OTRO CAMPO, GUARDAMOS EL ANTERIOR PRIMERO + if (editingId && editingId !== nuevoCampo.id) { + const campoAnterior = campos.find((c) => c.id === editingId) + if (campoAnterior && editValue !== campoAnterior.value) { + // Solo guardamos si el valor realmente cambió + ejecutarGuardadoSilencioso(campoAnterior, editValue) + } + } + + // 2. ABRIMOS EL NUEVO CAMPO + setEditingId(nuevoCampo.id) + setEditValue(nuevoCampo.value) } const handleCancel = () => { setEditingId(null) setEditValue('') } + // Función auxiliar para procesar los datos (fuera o dentro del componente) + const prepararDatosActualizados = ( + data: any, + campo: DatosGeneralesField, + valor: string, + ) => { + const currentValue = data.datos[campo.clave] + let newValue: any + + if ( + typeof currentValue === 'object' && + currentValue !== null && + 'description' in currentValue + ) { + newValue = { ...currentValue, description: valor } + } else { + newValue = valor + } + + return { + ...data.datos, + [campo.clave]: newValue, + } + } + + const ejecutarGuardadoSilencioso = ( + campo: DatosGeneralesField, + valor: string, + ) => { + if (!data?.datos) return + + const datosActualizados = prepararDatosActualizados(data, campo, valor) + + updatePlan.mutate({ + planId, + patch: { datos: datosActualizados }, + }) + + // Actualizar UI localmente + setCampos((prev) => + prev.map((c) => (c.id === campo.id ? { ...c, value: valor } : c)), + ) + } const handleSave = (campo: DatosGeneralesField) => { if (!data?.datos) return @@ -161,6 +214,7 @@ function DatosGeneralesPage() { prev.map((c) => (c.id === campo.id ? { ...c, value: editValue } : c)), ) + ejecutarGuardadoSilencioso(campo, editValue) setEditingId(null) setEditValue('') } -- 2.49.1 From a6a94fa42b310204920a70d87edfd8df591d1d7f Mon Sep 17 00:00:00 2001 From: "Roberto.silva" Date: Thu, 5 Feb 2026 14:09:55 -0600 Subject: [PATCH 2/2] =?UTF-8?q?WIP:=20Guardado=20autom=C3=A1tico=20fix=20#?= =?UTF-8?q?53=20fix=20#68?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/planes/$planId/_detalle.tsx | 138 ++++++++++++------------- 1 file changed, 68 insertions(+), 70 deletions(-) diff --git a/src/routes/planes/$planId/_detalle.tsx b/src/routes/planes/$planId/_detalle.tsx index 94f1c29..2f6b37c 100644 --- a/src/routes/planes/$planId/_detalle.tsx +++ b/src/routes/planes/$planId/_detalle.tsx @@ -5,12 +5,10 @@ import { Clock, Hash, CalendarDays, - Save, } from 'lucide-react' import { useState, useEffect, forwardRef } from 'react' import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' import { DropdownMenu, DropdownMenuContent, @@ -20,7 +18,7 @@ import { import { NotFoundPage } from '@/components/ui/NotFoundPage' import { Skeleton } from '@/components/ui/skeleton' import { plans_get } from '@/data/api/plans.api' -import { usePlan } from '@/data/hooks/usePlans' +import { usePlan, useUpdatePlanFields } from '@/data/hooks/usePlans' import { qk } from '@/data/query/keys' export const Route = createFileRoute('/planes/$planId/_detalle')({ @@ -56,6 +54,7 @@ export const Route = createFileRoute('/planes/$planId/_detalle')({ function RouteComponent() { const { planId } = Route.useParams() const { data, isLoading } = usePlan(planId) + const { mutate } = useUpdatePlanFields() // Estados locales para manejar la edición "en vivo" antes de persistir const [nombrePlan, setNombrePlan] = useState('') @@ -77,32 +76,37 @@ function RouteComponent() { 'Especialidad', ] - const handleKeyDown = (e: React.KeyboardEvent) => { + const persistChange = (patch: any) => { + mutate({ planId, patch }) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { - e.preventDefault() // Evita el salto de línea - e.currentTarget.blur() // Quita el foco, lo que dispara el onBlur y "guarda" en el estado + e.preventDefault() + e.currentTarget.blur() // Esto disparará el onBlur automáticamente } } - const handleSave = () => { - console.log('Guardando en DB...', { nombrePlan, nivelPlan }) - // Aquí iría tu mutation - setIsDirty(false) + const handleBlurNombre = (e: React.FocusEvent) => { + const nuevoNombre = e.currentTarget.textContent || '' + setNombrePlan(nuevoNombre) + + // Solo guardamos si el valor es realmente distinto al de la base de datos + if (nuevoNombre !== data?.nombre) { + persistChange({ nombre: nuevoNombre }) + } + } + + const handleSelectNivel = (n: string) => { + setNivelPlan(n) + // Guardamos inmediatamente al seleccionar + if (n !== data?.nivel) { + persistChange({ nivel: n }) + } } return (
- {/* Botón Flotante de Guardar */} - {isDirty && ( -
- -
- )} {/* 1. Header Superior */}
@@ -116,62 +120,54 @@ function RouteComponent() {
- {/* Header del Plan */} + {/* 2. Header del Plan */} {isLoading ? ( /* ===== SKELETON ===== */ -
-
- {Array.from({ length: 6 }).map((_, i) => ( - - ))} -
+
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))}
) : ( - <> -
-
-

- {nivelPlan} en - - setNombrePlan(e.currentTarget.textContent || '') +
+
+

+ {nivelPlan} en + { + const nuevoNombre = e.currentTarget.textContent || '' + setNombrePlan(nuevoNombre) + if (nuevoNombre !== data?.nombre) { + mutate({ planId, patch: { nombre: nuevoNombre } }) } - className="cursor-text border-b border-transparent decoration-transparent transition-colors outline-none select-text hover:border-slate-300 focus:border-teal-500" - style={{ - WebkitTextDecoration: 'none', - textDecoration: 'none', - }} // Doble seguridad contra subrayados - > - {nombrePlan} - -

-

- {data?.carreras?.facultades?.nombre}{' '} - {data?.carreras?.nombre_corto} -

-
- -
- {/* - {data?.estados_plan?.etiqueta} - */} - - {data?.estados_plan?.etiqueta} - -
+ {nombrePlan} + +

+

+ {data?.carreras?.facultades?.nombre}{' '} + {data?.carreras?.nombre_corto} +

- + +
+ + {data?.estados_plan?.etiqueta} + +
+
)} - {/* 3. Cards de Información con Context Menu */} + {/* 3. Cards de Información */}
@@ -189,7 +185,9 @@ function RouteComponent() { key={n} onClick={() => { setNivelPlan(n) - setIsDirty(true) + if (n !== data?.nivel) { + mutate({ planId, patch: { nivel: n } }) + } }} > {n} @@ -211,7 +209,7 @@ function RouteComponent() { } label="Creación" - value={data?.creado_en?.split('T')[0]} // Cortamos la fecha para que no sea tan larga + value={data?.creado_en?.split('T')[0]} />
-- 2.49.1