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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user