193 lines
7.1 KiB
TypeScript
193 lines
7.1 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
import type { EstadoDesarrollo, TipoDesarrollo } from '../../services/desarrollos'
|
|
|
|
// ─── Clases compartidas (mismo lenguaje visual que el resto de la landing) ──
|
|
|
|
export const TARJETA =
|
|
'rounded-xl border border-[#E5E7EB] bg-white shadow-sm dark:border-[#1E293B] dark:bg-[#1E293B]'
|
|
|
|
export const INPUT =
|
|
'mt-1.5 w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2 text-sm text-[#111827] transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0]'
|
|
|
|
export const INPUT_ERROR = 'border-red-500 dark:border-red-500'
|
|
|
|
export const LABEL = 'block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]'
|
|
|
|
export const AYUDA = 'mt-1 text-[11px] text-[#64748B] dark:text-[#94A3B8]'
|
|
|
|
export const TEXTO_ERROR = 'mt-1 text-xs text-red-600 dark:text-red-400'
|
|
|
|
export const BOTON_PRIMARIO =
|
|
'rounded-lg bg-accent px-4 py-2 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover disabled:opacity-50'
|
|
|
|
export const BOTON_SECUNDARIO =
|
|
'rounded-lg border border-[#D1D5DB] bg-white px-4 py-2 text-sm font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB] dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#CBD5E1] dark:hover:bg-[#334155]'
|
|
|
|
export const BOTON_PELIGRO =
|
|
'rounded-lg border border-red-300 bg-white px-4 py-2 text-sm font-medium text-red-600 shadow-sm transition-all duration-100 ease-out hover:bg-red-50 disabled:opacity-50 dark:border-red-500/40 dark:bg-[#0F172A] dark:text-red-400 dark:hover:bg-red-500/10'
|
|
|
|
export const BOTON_MINIMO =
|
|
'rounded-lg border border-[#D1D5DB] bg-white px-3 py-1.5 text-xs font-medium text-[#374151] shadow-sm transition-colors hover:bg-[#F9FAFB] disabled:opacity-50 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#CBD5E1] dark:hover:bg-[#334155]'
|
|
|
|
// ─── Etiquetas de dominio ───────────────────────────────────────────
|
|
|
|
export const ETIQUETA_ESTADO: Record<EstadoDesarrollo, string> = {
|
|
ACTIVO: 'Activo',
|
|
PREVENTA: 'Preventa',
|
|
CERRADO: 'Cerrado',
|
|
}
|
|
|
|
const CLASE_ESTADO: Record<EstadoDesarrollo, string> = {
|
|
ACTIVO: 'bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400',
|
|
PREVENTA: 'bg-amber-50 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400',
|
|
CERRADO: 'bg-[#F1F5F9] text-[#475569] dark:bg-[#334155] dark:text-[#CBD5E1]',
|
|
}
|
|
|
|
export const ETIQUETA_TIPO: Record<TipoDesarrollo, string> = {
|
|
VERTICAL: 'Vertical',
|
|
HORIZONTAL: 'Horizontal',
|
|
}
|
|
|
|
/** Base sobre la que se calcula un componente del precio ('lista' | 'final') */
|
|
export const ETIQUETA_BASE = { lista: 'precio de lista', final: 'precio final' } as const
|
|
|
|
export const ETIQUETA_MODO_MENSUALIDADES = {
|
|
remanente: 'sobre el remanente',
|
|
remanenteLista: 'sobre el remanente del precio de lista',
|
|
porcentaje: 'sobre el remanente en porcentaje',
|
|
ninguna: 'sin mensualidades',
|
|
} as const
|
|
|
|
export const ETIQUETA_FUENTE_N = {
|
|
fechaEntrega: 'Fecha de entrega de la unidad',
|
|
fijo: 'N fijo (entrega inmediata / terminación de obra)',
|
|
porUnidad: 'Capturado por unidad',
|
|
} as const
|
|
|
|
/**
|
|
* Estatus posibles del inventario (los mismos que maneja el cotizador).
|
|
* Un par canónico APARTADO/VENDIDO: las variantes femeninas APARTADA/VENDIDA
|
|
* quedaron normalizadas en la base de datos; CLASE_ESTATUS las conserva para
|
|
* pintar bien cualquier fila histórica que aún las traiga.
|
|
*/
|
|
export const ESTATUS_UNIDAD = [
|
|
'DISPONIBLE',
|
|
'APARTADO',
|
|
'VENDIDO',
|
|
'ESCRITURADO',
|
|
'BLOQUEADO',
|
|
] as const
|
|
|
|
const CLASE_ESTATUS: Record<string, string> = {
|
|
DISPONIBLE: 'bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400',
|
|
APARTADO: 'bg-amber-50 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400',
|
|
APARTADA: 'bg-amber-50 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400',
|
|
VENDIDO: 'bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400',
|
|
VENDIDA: 'bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400',
|
|
ESCRITURADO: 'bg-indigo-50 text-indigo-700 dark:bg-indigo-500/10 dark:text-indigo-400',
|
|
BLOQUEADO: 'bg-[#F1F5F9] text-[#475569] dark:bg-[#334155] dark:text-[#CBD5E1]',
|
|
}
|
|
|
|
export function BadgeEstado({ estado }: { estado: EstadoDesarrollo }) {
|
|
return (
|
|
<span
|
|
className={`shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium ${CLASE_ESTADO[estado] ?? CLASE_ESTATUS.BLOQUEADO}`}
|
|
>
|
|
{ETIQUETA_ESTADO[estado] ?? estado}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
export function BadgeEstatus({ estatus }: { estatus: string | null | undefined }) {
|
|
if (!estatus) return <span className="text-xs text-[#94A3B8]">—</span>
|
|
return (
|
|
<span
|
|
className={`shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium ${CLASE_ESTATUS[estatus] ?? 'bg-[#F1F5F9] text-[#475569] dark:bg-[#334155] dark:text-[#CBD5E1]'}`}
|
|
>
|
|
{estatus}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
export function Chip({ children }: { children: ReactNode }) {
|
|
return (
|
|
<span className="rounded-full bg-[#F1F5F9] px-2.5 py-0.5 text-[11px] font-medium text-[#475569] dark:bg-[#334155] dark:text-[#CBD5E1]">
|
|
{children}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
// ─── Piezas de formulario ───────────────────────────────────────────
|
|
|
|
export function Campo({
|
|
id,
|
|
etiqueta,
|
|
requerido = false,
|
|
ayuda,
|
|
error,
|
|
className = '',
|
|
children,
|
|
}: {
|
|
id: string
|
|
etiqueta: string
|
|
requerido?: boolean
|
|
ayuda?: string
|
|
error?: string
|
|
className?: string
|
|
children: ReactNode
|
|
}) {
|
|
return (
|
|
<div className={className}>
|
|
<label htmlFor={id} className={LABEL}>
|
|
{etiqueta}
|
|
{requerido && <span aria-hidden="true"> *</span>}
|
|
</label>
|
|
{children}
|
|
{ayuda && <p className={AYUDA}>{ayuda}</p>}
|
|
{error && (
|
|
<p role="alert" className={TEXTO_ERROR}>
|
|
{error}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function MensajeError({ children }: { children: ReactNode }) {
|
|
return (
|
|
<p role="alert" className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-500/40 dark:bg-red-500/10 dark:text-red-400">
|
|
{children}
|
|
</p>
|
|
)
|
|
}
|
|
|
|
export function MensajeExito({ children }: { children: ReactNode }) {
|
|
return (
|
|
<p role="status" className="rounded-lg border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700 dark:border-green-500/40 dark:bg-green-500/10 dark:text-green-400">
|
|
{children}
|
|
</p>
|
|
)
|
|
}
|
|
|
|
/** Formato de moneda para precios de inventario */
|
|
export function moneda(valor: number | null | undefined): string {
|
|
if (valor == null) return '—'
|
|
return new Intl.NumberFormat('es-MX', {
|
|
style: 'currency',
|
|
currency: 'MXN',
|
|
maximumFractionDigits: 0,
|
|
}).format(valor)
|
|
}
|
|
|
|
/** Convierte '' | null | undefined a null y el resto a número (NaN → null) */
|
|
export function aNumero(valor: string): number | null {
|
|
const limpio = valor.trim()
|
|
if (limpio === '') return null
|
|
const n = Number(limpio)
|
|
return Number.isFinite(n) ? n : null
|
|
}
|
|
|
|
export function aTexto(valor: number | null | undefined): string {
|
|
return valor == null ? '' : String(valor)
|
|
}
|