Files
FRASA-CAS/apps/landing/src/components/desarrollos/comun.tsx

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)
}