feat(crm): fecha de alta en cards y bitacora, click en kanban abre bitacora, tab Descartados con reasignacion para admin, tareas al calendario, estatus No asistio y edicion de unidades del inventario

This commit is contained in:
2026-09-25 18:28:14 +00:00
parent 7f663208a0
commit 2400ecf6ff
15 changed files with 527 additions and 12 deletions

View File

@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import {
actualizarUnidad,
crearUnidades,
eliminarUnidad,
estatusDe,
@@ -147,6 +148,76 @@ export function TabInventario({ desarrollo, onRefrescar }: Props) {
const modelos = desarrollo.modelos ?? []
// Edición de unidad existente (precios, m², ubicación, modelo y estatus)
const [editando, setEditando] = useState<UnidadDesarrollo | null>(null)
const [formEdicion, setFormEdicion] = useState({
precioLista: '',
precioAcabados: '',
supConstruccion: '',
supExcedente: '',
supEstacionamiento: '',
manzana: '',
lote: '',
nivel: '',
modeloId: '',
status: '',
})
const abrirEdicion = (unidad: UnidadDesarrollo) => {
setEditando(unidad)
setFormEdicion({
precioLista: String(unidad.precioSinAcabados ?? unidad.precioLista ?? ''),
precioAcabados: String(unidad.precioConAcabados ?? ''),
supConstruccion: String(unidad.supConstruccion ?? ''),
supExcedente: String(unidad.supExcedente ?? ''),
supEstacionamiento: String(unidad.supEstacionamiento ?? ''),
manzana: (unidad.extras?.['manzana'] as string | undefined) ?? '',
lote: (unidad.extras?.['lote'] as string | undefined) ?? '',
nivel: unidad.nivel ?? '',
modeloId: modelos.find((m) => m.nombre === unidad.modelo)?.id ?? '',
status: estatusDe(unidad) ?? '',
})
setError('')
setExito('')
}
const cambiarEdicion = (campo: keyof typeof formEdicion) => (valor: string) =>
setFormEdicion((prev) => ({ ...prev, [campo]: valor }))
const guardarEdicion = async () => {
if (!editando) return
setError('')
setExito('')
const lista = aNumero(formEdicion.precioLista)
if (lista === null) {
setError('El precio de lista es obligatorio.')
return
}
setEnviando(true)
try {
await actualizarUnidad(desarrollo.id, editando.codigo, {
precioLista: lista,
...(aNumero(formEdicion.precioAcabados) !== null ? { precioAcabados: aNumero(formEdicion.precioAcabados)! } : {}),
...(aNumero(formEdicion.supConstruccion) !== null ? { supConstruccion: aNumero(formEdicion.supConstruccion)! } : {}),
...(aNumero(formEdicion.supExcedente) !== null ? { supExcedente: aNumero(formEdicion.supExcedente)! } : {}),
...(aNumero(formEdicion.supEstacionamiento) !== null ? { supEstacionamiento: aNumero(formEdicion.supEstacionamiento)! } : {}),
manzana: formEdicion.manzana.trim(),
lote: formEdicion.lote.trim(),
nivel: formEdicion.nivel.trim(),
...(formEdicion.modeloId !== '' ? { modeloId: formEdicion.modeloId } : {}),
...(formEdicion.status !== '' ? { status: formEdicion.status } : {}),
})
setExito(`Unidad ${editando.codigo} actualizada.`)
setEditando(null)
await cargarUnidades()
await onRefrescar()
} catch (err) {
setError(err instanceof Error && err.message ? err.message : 'No se pudo actualizar la unidad.')
} finally {
setEnviando(false)
}
}
const guardarGenerador = async () => {
setError('')
setExito('')
@@ -711,6 +782,14 @@ export function TabInventario({ desarrollo, onRefrescar }: Props) {
</div>
<div className="flex shrink-0 items-center gap-2">
<BadgeEstatus estatus={estatusDe(unidad)} />
<button
type="button"
onClick={() => abrirEdicion(unidad)}
aria-label={`Editar unidad ${unidad.codigo}`}
className={BOTON_SECUNDARIO}
>
Editar
</button>
<button
type="button"
onClick={() => void borrarUnidad(unidad)}
@@ -756,6 +835,113 @@ export function TabInventario({ desarrollo, onRefrescar }: Props) {
</div>
)}
</section>
{/* Modal de edición de unidad */}
{editando && (
<div className="fixed inset-0 z-20 flex items-center justify-center bg-black/50 px-4" role="dialog" aria-modal="true">
<div className="w-full max-w-lg rounded-2xl border border-[#E5E7EB] bg-white p-6 shadow-xl dark:border-[#1E293B] dark:bg-[#1E293B]">
<h3 className="text-base font-semibold text-[#111827] dark:text-[#E2E8F0]">
Editar unidad <span className="font-mono">{editando.codigo}</span>
</h3>
<div className="mt-4 grid gap-3 sm:grid-cols-2">
<label className="block">
<span className="mb-1 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]">Precio de lista *</span>
<input
type="number"
min="0"
step="1000"
value={formEdicion.precioLista}
onChange={(e) => cambiarEdicion('precioLista')(e.target.value)}
className={INPUT}
/>
</label>
<label className="block">
<span className="mb-1 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]">Precio con acabados</span>
<input
type="number"
min="0"
step="1000"
value={formEdicion.precioAcabados}
onChange={(e) => cambiarEdicion('precioAcabados')(e.target.value)}
className={INPUT}
/>
</label>
<label className="block">
<span className="mb-1 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]">Sup. construcción (m²)</span>
<input
type="number"
min="0"
step="0.01"
value={formEdicion.supConstruccion}
onChange={(e) => cambiarEdicion('supConstruccion')(e.target.value)}
className={INPUT}
/>
</label>
<label className="block">
<span className="mb-1 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]">Sup. excedente (m²)</span>
<input
type="number"
min="0"
step="0.01"
value={formEdicion.supExcedente}
onChange={(e) => cambiarEdicion('supExcedente')(e.target.value)}
className={INPUT}
/>
</label>
<label className="block">
<span className="mb-1 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]">Sup. estacionamiento (m²)</span>
<input
type="number"
min="0"
step="0.01"
value={formEdicion.supEstacionamiento}
onChange={(e) => cambiarEdicion('supEstacionamiento')(e.target.value)}
className={INPUT}
/>
</label>
<label className="block">
<span className="mb-1 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]">Modelo</span>
<select value={formEdicion.modeloId} onChange={(e) => cambiarEdicion('modeloId')(e.target.value)} className={INPUT}>
<option value="">Sin modelo</option>
{modelos.map((m) => (
<option key={m.id} value={m.id}>{m.nombre}</option>
))}
</select>
</label>
<label className="block">
<span className="mb-1 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]">Manzana</span>
<input value={formEdicion.manzana} onChange={(e) => cambiarEdicion('manzana')(e.target.value)} className={INPUT} />
</label>
<label className="block">
<span className="mb-1 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]">Lote</span>
<input value={formEdicion.lote} onChange={(e) => cambiarEdicion('lote')(e.target.value)} className={INPUT} />
</label>
<label className="block">
<span className="mb-1 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]">Nivel</span>
<input value={formEdicion.nivel} onChange={(e) => cambiarEdicion('nivel')(e.target.value)} className={INPUT} />
</label>
<label className="block">
<span className="mb-1 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]">Estatus</span>
<select value={formEdicion.status} onChange={(e) => cambiarEdicion('status')(e.target.value)} className={INPUT}>
<option value="">Sin cambio</option>
{ESTATUS_UNIDAD.map((estatus) => (
<option key={estatus} value={estatus}>{estatus}</option>
))}
</select>
</label>
</div>
{error && <div className="mt-3"><MensajeError>{error}</MensajeError></div>}
<div className="mt-5 flex justify-end gap-2">
<button type="button" onClick={() => setEditando(null)} disabled={enviando} className={BOTON_SECUNDARIO}>
Cancelar
</button>
<button type="button" onClick={() => void guardarEdicion()} disabled={enviando} className={BOTON_PRIMARIO}>
{enviando ? 'Guardando…' : 'Guardar cambios'}
</button>
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -382,3 +382,29 @@ export async function eliminarUnidad(desarrolloId: string, codigo: string): Prom
method: 'DELETE',
})
}
/** Cambios editables de una unidad (PATCH /desarrollos/:id/unidades/:codigo) */
export interface CambiosUnidad {
manzana?: string
lote?: string
nivel?: string
modeloId?: string
precioLista?: number
precioAcabados?: number
supConstruccion?: number
supExcedente?: number
supEstacionamiento?: number
status?: string
}
/** PATCH /api/desarrollos/:id/unidades/:codigo — edición de precios, m², modelo y estatus */
export async function actualizarUnidad(
desarrolloId: string,
codigo: string,
cambios: CambiosUnidad,
): Promise<UnidadDesarrollo> {
return pedir<UnidadDesarrollo>(
`${rutaDesarrollo(desarrolloId)}/unidades/${encodeURIComponent(codigo)}`,
initJson('PATCH', cambios),
)
}