@@ -5,12 +5,10 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
Hash,
|
Hash,
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
Save,
|
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useState, useEffect, forwardRef } from 'react'
|
import { useState, useEffect, forwardRef } from 'react'
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -20,7 +18,7 @@ import {
|
|||||||
import { NotFoundPage } from '@/components/ui/NotFoundPage'
|
import { NotFoundPage } from '@/components/ui/NotFoundPage'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { plans_get } from '@/data/api/plans.api'
|
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'
|
import { qk } from '@/data/query/keys'
|
||||||
|
|
||||||
export const Route = createFileRoute('/planes/$planId/_detalle')({
|
export const Route = createFileRoute('/planes/$planId/_detalle')({
|
||||||
@@ -56,6 +54,7 @@ export const Route = createFileRoute('/planes/$planId/_detalle')({
|
|||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { planId } = Route.useParams()
|
const { planId } = Route.useParams()
|
||||||
const { data, isLoading } = usePlan(planId)
|
const { data, isLoading } = usePlan(planId)
|
||||||
|
const { mutate } = useUpdatePlanFields()
|
||||||
|
|
||||||
// Estados locales para manejar la edición "en vivo" antes de persistir
|
// Estados locales para manejar la edición "en vivo" antes de persistir
|
||||||
const [nombrePlan, setNombrePlan] = useState('')
|
const [nombrePlan, setNombrePlan] = useState('')
|
||||||
@@ -77,32 +76,37 @@ function RouteComponent() {
|
|||||||
'Especialidad',
|
'Especialidad',
|
||||||
]
|
]
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const persistChange = (patch: any) => {
|
||||||
|
mutate({ planId, patch })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault() // Evita el salto de línea
|
e.preventDefault()
|
||||||
e.currentTarget.blur() // Quita el foco, lo que dispara el onBlur y "guarda" en el estado
|
e.currentTarget.blur() // Esto disparará el onBlur automáticamente
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleBlurNombre = (e: React.FocusEvent<HTMLSpanElement>) => {
|
||||||
console.log('Guardando en DB...', { nombrePlan, nivelPlan })
|
const nuevoNombre = e.currentTarget.textContent || ''
|
||||||
// Aquí iría tu mutation
|
setNombrePlan(nuevoNombre)
|
||||||
setIsDirty(false)
|
|
||||||
|
// 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 (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
{/* Botón Flotante de Guardar */}
|
|
||||||
{isDirty && (
|
|
||||||
<div className="animate-in fade-in slide-in-from-bottom-4 fixed right-8 bottom-8 z-50 duration-300">
|
|
||||||
<Button
|
|
||||||
onClick={handleSave}
|
|
||||||
className="gap-2 rounded-full bg-teal-600 px-6 shadow-xl hover:bg-teal-700"
|
|
||||||
>
|
|
||||||
<Save size={16} /> Guardar cambios del Plan
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* 1. Header Superior */}
|
{/* 1. Header Superior */}
|
||||||
<div className="sticky top-0 z-20 border-b bg-white/50 shadow-sm backdrop-blur-sm">
|
<div className="sticky top-0 z-20 border-b bg-white/50 shadow-sm backdrop-blur-sm">
|
||||||
<div className="px-6 py-2">
|
<div className="px-6 py-2">
|
||||||
@@ -116,18 +120,15 @@ function RouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx-auto max-w-400 space-y-8 p-8">
|
<div className="mx-auto max-w-400 space-y-8 p-8">
|
||||||
{/* Header del Plan */}
|
{/* 2. Header del Plan */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
/* ===== SKELETON ===== */
|
/* ===== SKELETON ===== */
|
||||||
<div className="mx-auto max-w-400 p-8">
|
|
||||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||||
{Array.from({ length: 6 }).map((_, i) => (
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
<DatosGeneralesSkeleton key={i} />
|
<DatosGeneralesSkeleton key={i} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
|
||||||
<div className="flex flex-col items-start justify-between gap-4 md:flex-row">
|
<div className="flex flex-col items-start justify-between gap-4 md:flex-row">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="flex items-baseline gap-2 text-3xl font-bold tracking-tight text-slate-900">
|
<h1 className="flex items-baseline gap-2 text-3xl font-bold tracking-tight text-slate-900">
|
||||||
@@ -137,16 +138,17 @@ function RouteComponent() {
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
contentEditable
|
contentEditable
|
||||||
suppressContentEditableWarning
|
suppressContentEditableWarning
|
||||||
spellCheck={false} // Quita el subrayado rojo de error ortográfico
|
spellCheck={false}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onBlur={(e) =>
|
onBlur={(e) => {
|
||||||
setNombrePlan(e.currentTarget.textContent || '')
|
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={{
|
className="cursor-text border-b border-transparent transition-colors outline-none select-text hover:border-slate-300 focus:border-teal-500"
|
||||||
WebkitTextDecoration: 'none',
|
style={{ textDecoration: 'none' }}
|
||||||
textDecoration: 'none',
|
|
||||||
}} // Doble seguridad contra subrayados
|
|
||||||
>
|
>
|
||||||
{nombrePlan}
|
{nombrePlan}
|
||||||
</span>
|
</span>
|
||||||
@@ -158,20 +160,14 @@ function RouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{/* <Badge className="gap-1 border-teal-200 bg-teal-50 px-3 text-teal-700 hover:bg-teal-100">
|
<Badge className="gap-1 border-teal-200 bg-teal-50 px-3 text-teal-700 hover:bg-teal-100">
|
||||||
<CheckCircle2 size={12} /> {data?.estados_plan?.etiqueta}
|
|
||||||
</Badge> */}
|
|
||||||
<Badge
|
|
||||||
className={`gap-1 border-teal-200 bg-teal-50 px-3 text-teal-700 hover:bg-teal-100`}
|
|
||||||
>
|
|
||||||
{data?.estados_plan?.etiqueta}
|
{data?.estados_plan?.etiqueta}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 3. Cards de Información con Context Menu */}
|
{/* 3. Cards de Información */}
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -189,7 +185,9 @@ function RouteComponent() {
|
|||||||
key={n}
|
key={n}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setNivelPlan(n)
|
setNivelPlan(n)
|
||||||
setIsDirty(true)
|
if (n !== data?.nivel) {
|
||||||
|
mutate({ planId, patch: { nivel: n } })
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{n}
|
{n}
|
||||||
@@ -211,7 +209,7 @@ function RouteComponent() {
|
|||||||
<InfoCard
|
<InfoCard
|
||||||
icon={<CalendarDays className="text-slate-400" />}
|
icon={<CalendarDays className="text-slate-400" />}
|
||||||
label="Creación"
|
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]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -112,15 +112,68 @@ function DatosGeneralesPage() {
|
|||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
// 3. Manejadores de acciones (Ahora como funciones locales)
|
// 3. Manejadores de acciones (Ahora como funciones locales)
|
||||||
const handleEdit = (campo: DatosGeneralesField) => {
|
const handleEdit = (nuevoCampo: DatosGeneralesField) => {
|
||||||
setEditingId(campo.id)
|
// 1. SI YA ESTÁBAMOS EDITANDO OTRO CAMPO, GUARDAMOS EL ANTERIOR PRIMERO
|
||||||
setEditValue(campo.value)
|
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 = () => {
|
const handleCancel = () => {
|
||||||
setEditingId(null)
|
setEditingId(null)
|
||||||
setEditValue('')
|
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) => {
|
const handleSave = (campo: DatosGeneralesField) => {
|
||||||
if (!data?.datos) return
|
if (!data?.datos) return
|
||||||
@@ -161,6 +214,7 @@ function DatosGeneralesPage() {
|
|||||||
prev.map((c) => (c.id === campo.id ? { ...c, value: editValue } : c)),
|
prev.map((c) => (c.id === campo.id ? { ...c, value: editValue } : c)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ejecutarGuardadoSilencioso(campo, editValue)
|
||||||
setEditingId(null)
|
setEditingId(null)
|
||||||
setEditValue('')
|
setEditValue('')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user