feat(mejoras): 25 mejoras UX en todos los módulos + purga Monday

Backend: mi-dia siempre local, vocabulario canónico, AplicacionPago con usuario
Mi día: modal español real, cita desde modal, score falso eliminado
Cotizador: barra sticky, persistencia local con folio
Alta Clientes: redirect eliminado, foto conectada, asesor desde sesión, refs dinámicas
Cobranza: contrato Mora arreglado, link a Plan, abono próximo pago, WhatsApp
Landing: contactos locales, ModalContacto eliminado, ModoTrabajo eliminado
Dashboards: MiTablero unificado, funnel con descartados, drill-down, rango fechas
Chat FRASA: corpus 5 desarrollos, 6 herramientas, historial conversaciones

69 archivos, 7 apps construidas
This commit is contained in:
2026-09-11 06:52:37 +00:00
parent 075d47c304
commit 64b01246d6
69 changed files with 1600 additions and 1133 deletions

View File

@@ -53,9 +53,9 @@ describe('DashboardVendedor', () => {
await waitFor(() => {
expect(screen.getByText('Ventas del mes')).toBeTruthy()
})
expect(screen.getByText('Actividades de hoy')).toBeTruthy()
expect(screen.getByText('Leads nuevos hoy')).toBeTruthy()
expect(screen.getByText('Leads totales')).toBeTruthy()
expect(screen.getByText('Funnel conversión')).toBeTruthy()
expect(screen.getByText('Funnel de conversión')).toBeTruthy()
expect(screen.getByText('Leads por día (últimos 30 días)')).toBeTruthy()
expect(screen.getByText('Funnel de conversión')).toBeTruthy()
expect(screen.getByText('Rendimiento por origen')).toBeTruthy()

View File

@@ -76,16 +76,14 @@ export default function DashboardVendedor() {
const mesAnterior = mesActual === 0 ? 11 : mesActual - 1
const anioAnterior = mesActual === 0 ? anioActual - 1 : anioActual
// leadsPorDia es el único histórico mensual del tablero (últimos 30 días);
// se agrupa por mes parseando 'YYYY-MM-DD' sin Date (evita corrimientos UTC)
let leadsMesActual = 0
let leadsMesAnterior = 0
for (const d of tablero.leadsPorDia) {
const fecha = new Date(d.fecha)
if (fecha.getMonth() === mesActual && fecha.getFullYear() === anioActual) {
leadsMesActual += d.cantidad
}
if (fecha.getMonth() === mesAnterior && fecha.getFullYear() === anioAnterior) {
leadsMesAnterior += d.cantidad
}
const [anio, mes] = d.fecha.split('-').map(Number)
if (anio === anioActual && mes === mesActual + 1) leadsMesActual += d.cantidad
if (anio === anioAnterior && mes === mesAnterior + 1) leadsMesAnterior += d.cantidad
}
const nombresMes = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic']
@@ -100,14 +98,16 @@ export default function DashboardVendedor() {
{
metrica: 'Oportunidades',
mesActual: tablero.conversion.oportunidades,
mesAnterior: Math.round(tablero.conversion.oportunidades * 0.8), // estimado
// Sin histórico mensual: no se estima el mes anterior
mesAnterior: null,
nombreMesActual: nombresMes[mesActual],
nombreMesAnterior: nombresMes[mesAnterior],
},
{
metrica: 'Ventas',
mesActual: tablero.ventasMes.unidades,
mesAnterior: Math.round(tablero.ventasMes.unidades * 0.85), // estimado
// Sin histórico mensual: no se estima el mes anterior
mesAnterior: null,
nombreMesActual: nombresMes[mesActual],
nombreMesAnterior: nombresMes[mesAnterior],
},
@@ -211,6 +211,9 @@ export default function DashboardVendedor() {
{/* F1: Comparativa mensual */}
<div className="rounded-xl border border-[#E5E7EB] bg-white p-5 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm lg:col-span-2">
<TituloSeccion>Comparativa mensual (mes actual vs anterior)</TituloSeccion>
<p className="-mt-2 mb-3 text-xs text-[#94A3B8]">
El mes anterior solo se compara en leads (único histórico disponible); oportunidades y ventas muestran el mes en curso.
</p>
<ResponsiveContainer width="100%" height={260}>
<BarChart data={comparativaMensual} margin={{ top: 8, right: 4, left: -16, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke={gridStroke()} vertical={false} />

View File

@@ -27,14 +27,23 @@ const CLASE_ACCION_RAPIDA =
'rounded-lg border border-[#D1D5DB] bg-white px-3 py-1.5 text-xs font-medium text-accent shadow-sm transition-all duration-100 ease-out hover:bg-blue-50 dark:border-accent dark:bg-[#0F172A] dark:text-accent dark:hover:bg-accent/10'
const TABS: { id: TabId; titulo: string; icono: typeof User }[] = [
{ id: 'info', titulo: 'Información del cliente potencial', icono: User },
{ id: 'cita', titulo: 'Reserve o actualice una cita', icono: Calendar },
{ id: 'info', titulo: 'Información', icono: User },
{ id: 'cita', titulo: 'Cita', icono: Calendar },
{ id: 'tareas', titulo: 'Tareas', icono: CheckSquare },
{ id: 'notas', titulo: 'Notas', icono: FileText },
{ id: 'miembros', titulo: 'Objetos de los miembros', icono: Users },
{ id: 'miembros', titulo: 'Miembros', icono: Users },
]
export default function DetalleLeadModal({ item, onCerrar }: { item: LeadItem; onCerrar: () => void }) {
export default function DetalleLeadModal({
item,
onCerrar,
onGuardarCita,
}: {
item: LeadItem
onCerrar: () => void
/** Guarda la cita de una oportunidad (fechaHora 'YYYY-MM-DDTHH:mm'); la página maneja el error */
onGuardarCita?: (fechaHora: string) => Promise<void> | void
}) {
const esOp = esOportunidad(item)
const esLd = esLead(item)
const vendedor = getSesion()?.usuario.nombre ?? ''
@@ -49,6 +58,23 @@ export default function DetalleLeadModal({ item, onCerrar }: { item: LeadItem; o
const [busquedaNota, setBusquedaNota] = useState('')
// --- Tareas ---
const [completandoTarea, setCompletandoTarea] = useState<string | null>(null)
// --- Cita ---
const [fechaCita, setFechaCita] = useState('')
const [guardandoCita, setGuardandoCita] = useState(false)
/** Guarda la cita: el cambio de etapa lo ejecuta la página (onGuardarCita) */
async function guardarCita(e: FormEvent) {
e.preventDefault()
if (!onGuardarCita || fechaCita === '') return
setGuardandoCita(true)
try {
await onGuardarCita(fechaCita)
} catch {
// el error ya se muestra en la página (toast/aviso del caller)
} finally {
setGuardandoCita(false)
}
}
const cargarNotas = useCallback(async () => {
setCargandoNotas(true)
@@ -102,8 +128,8 @@ export default function DetalleLeadModal({ item, onCerrar }: { item: LeadItem; o
{/* Header */}
<div className="absolute top-0 left-0 right-0 flex items-center justify-between border-b border-[#E5E7EB] dark:border-[#334155] px-6 py-3 bg-white dark:bg-[#1E293B] z-10">
<div>
<h2 className="text-lg font-semibold text-[#111827] dark:text-[#E2E8F0]">Editar "{item.nombre}"</h2>
<p className="text-xs text-[#64748B] dark:text-[#94A3B8]">Añadir y editar oportunidad Detalles, Actividades, notas y Cita.</p>
<h2 className="text-lg font-semibold text-[#111827] dark:text-[#E2E8F0]">Bitácora de {item.nombre}</h2>
<p className="text-xs text-[#64748B] dark:text-[#94A3B8]">Seguimiento del cliente: información, citas, tareas y notas</p>
</div>
<button type="button" onClick={onCerrar} className="rounded-lg p-2 text-[#64748B] hover:bg-[#F1F5F9] dark:text-[#94A3B8] dark:hover:bg-[#334155]"><X className="h-5 w-5" /></button>
</div>
@@ -168,14 +194,37 @@ export default function DetalleLeadModal({ item, onCerrar }: { item: LeadItem; o
{tab === 'cita' && (
<div className="space-y-4">
<h3 className="text-sm font-semibold text-[#111827] dark:text-[#E2E8F0]">Reserve o actualice una cita</h3>
<h3 className="text-sm font-semibold text-[#111827] dark:text-[#E2E8F0]">Cita</h3>
{esOp && item.fechaCita ? (
<div className="rounded-lg border border-[#E5E7EB] dark:border-[#334155] bg-[#F8FAFC] dark:bg-[#1E293B] p-4">
<p className="text-sm font-medium text-[#111827] dark:text-[#E2E8F0]">Cita programada</p>
<p className="text-xs text-[#64748B] dark:text-[#94A3B8] mt-1">{item.fechaCita.replace('T', ' a las ')}</p>
</div>
) : esOp ? (
<form onSubmit={guardarCita} className="space-y-3">
<div>
<label htmlFor="cita-fecha" className="mb-1 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]">
Fecha y hora de la cita
</label>
<input
id="cita-fecha"
type="datetime-local"
value={fechaCita}
onChange={(e) => setFechaCita(e.target.value)}
disabled={guardandoCita}
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2 text-sm dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20 disabled:opacity-50"
/>
</div>
<button
type="submit"
disabled={guardandoCita || fechaCita === ''}
className="rounded-lg bg-accent px-4 py-2 text-sm font-medium text-white shadow-sm transition-colors hover:bg-accent-hover disabled:opacity-50"
>
{guardandoCita ? 'Guardando…' : 'Guardar cita'}
</button>
</form>
) : (
<p className="text-sm text-[#94A3B8]">No hay cita programada. Use los botones de fase en la lista para agendar una.</p>
<p className="text-sm text-[#94A3B8]">No hay cita programada. Las citas se agendan desde Mis oportunidades.</p>
)}
</div>
)}
@@ -267,7 +316,7 @@ export default function DetalleLeadModal({ item, onCerrar }: { item: LeadItem; o
{tab === 'miembros' && (
<div className="space-y-4">
<h3 className="text-sm font-semibold text-[#111827] dark:text-[#E2E8F0]">Objetos de los miembros</h3>
<h3 className="text-sm font-semibold text-[#111827] dark:text-[#E2E8F0]">Miembros</h3>
<p className="text-sm text-[#94A3B8]">Información de los miembros del equipo asignados a este lead.</p>
<div className="rounded-lg border border-[#E5E7EB] dark:border-[#334155] bg-[#F8FAFC] dark:bg-[#1E293B] p-4">
<p className="text-xs text-[#94A3B8]">Vendedor: {(item as any).vendedor ?? '—'}</p>

View File

@@ -1,5 +1,4 @@
import { useState, useCallback } from 'react'
import ScoreBadge from './ScoreBadge'
/** Columna genérica del kanban */
interface ColumnaDef {
@@ -77,7 +76,6 @@ export default function KanbanView<T extends { id: string; nombre: string; estad
<>
<div className="flex items-start justify-between gap-2">
<p className="text-sm font-medium text-[#111827] dark:text-[#E2E8F0]">{item.nombre}</p>
{'score' in item && <ScoreBadge score={Number((item as Record<string, unknown>).score) || 0} />}
</div>
{item.telefono && <p className="mt-1 text-xs text-[#94A3B8]">{item.telefono}</p>}
{item.unidadCodigo && <p className="mt-0.5 text-xs text-[#94A3B8]">{item.unidadCodigo}</p>}

View File

@@ -1,69 +0,0 @@
/** Badge circular con score 0-100 para lead scoring. */
interface ScoreBadgeProps {
score: number
size?: 'sm' | 'md'
}
function colorForScore(score: number): string {
if (score > 70) return 'bg-green-500 text-white'
if (score > 40) return 'bg-yellow-400 text-yellow-900'
return 'bg-red-500 text-white'
}
/**
* Calcula el score de un lead de forma local usando:
* etapa (20-90) + recencia + teléfono + email + origen.
*
* Para oportunidades usa la `etapa`; para leads usa `estado`.
*/
export function calcularScore(lead: {
estado?: string
etapa?: string
telefono?: string
email?: string
origen?: string
updatedAt?: string
}): number {
const etapa = lead.etapa ?? lead.estado ?? ''
const normalizada = etapa.toUpperCase()
// Base por etapa: 20-90
let base = 20
if (normalizada.includes('AGENDO') || normalizada.includes('CITA')) base = 60
else if (normalizada.includes('INTERES')) base = 50
else if (normalizada.includes('CONTACTAD')) base = 40
else if (normalizada.includes('COTIZ')) base = 70
else if (normalizada.includes('APARTO') || normalizada.includes('APARTAD')) base = 90
else if (normalizada.includes('DESCART') || normalizada.includes('NO ASIST')) base = 5
// Recencia
let recencia = 0
if (lead.updatedAt) {
const diffMs = Date.now() - new Date(lead.updatedAt).getTime()
const diffHoras = diffMs / (1000 * 60 * 60)
if (diffHoras < 24) recencia = 10
else if (diffHoras < 168) recencia = 5
else if (diffHoras > 720) recencia = -20 // >30 días
}
// Señales extra
const tel = lead.telefono && lead.telefono.trim() !== '' ? 5 : 0
const em = lead.email && lead.email.trim() !== '' ? 5 : 0
const ori = lead.origen && lead.origen.trim() !== '' ? 5 : 0
return Math.max(0, Math.min(100, base + recencia + tel + em + ori))
}
export default function ScoreBadge({ score, size = 'sm' }: ScoreBadgeProps) {
const dim = size === 'sm' ? 'h-6 w-6 text-[10px]' : 'h-8 w-8 text-xs'
return (
<span
className={`inline-flex items-center justify-center rounded-full font-bold ${dim} ${colorForScore(score)}`}
title={`Score: ${score}`}
aria-label={`Score: ${score}`}
>
{score}
</span>
)
}

View File

@@ -112,50 +112,57 @@ export interface ResumenMiDia {
}
}
// Labels de la columna 'Origen' (board Leads) / 'Origen del Lead' (Oportunidades),
// vocabulario FRASA verificado y sembrado en Monday el 2026-09-04
export const ORIGENES_LEAD = ['PASE', 'PROSPECCION ASESOR', 'ACTIVACION', 'FACEBOOK FRASA', 'REFERIDO', 'BROCKER', 'OTRO'] as const
/**
* VOCABULARIO canónico FRASA: única fuente de verdad de fases, estados,
* etapas, motivos, subcategorías, orígenes y desarrollos. Las constantes
* individuales (FASES_OPORTUNIDAD, MOTIVOS_NO_COMPRA…) se derivan de aquí.
* Espejo del backend en api-gateway/src/config/vocabulario.ts (mantener
* sincronizados).
*/
export const VOCABULARIO = {
// Estados de la columna status del board Leads (ESTADOS_LEAD_PERMITIDOS del
// API + NUEVO/DESCARTADO)
FASES_LEAD: ['NUEVO', 'CONTACTADO', 'Intento de contacto', 'INTERESADO', 'AGENDA CITA', 'CALIFICADO', 'DESCARTADO'],
// Etapas del funnel de Oportunidades (labels REALES del board; el API las valida tal cual)
FASES_OPORTUNIDAD: ['Agendo Cita', 'Afluencia', 'Cotizador', 'Aparto', 'No Asistio', 'Abandono'],
// Fases del pipeline Enkontrol del tablero Apartado (labels REALES del board)
FASES_APARTADO: ['Pago Apartado', 'Alta Enkontrol', 'Entrego Documentacion', 'Cita Clientes', 'Contrato Enkontrol', 'Firma Cliente', 'Cancelacion Apartado'],
// Fases de escrituración (pipeline posterior a 'Firma Cliente')
FASES_ESCRITURACION: ['Venta', 'Seguimiento', 'Integracion', 'X Autorizar', 'Autorizado', 'X Escriturar', 'Escriturado', 'Titulado', 'Cancelo'],
// Categorías del dropdown Motivo No Compra (vocabulario CRM FRASA)
MOTIVOS_NO_COMPRA: ['Precio', 'Producto', 'Ubicacion', 'Financiamiento', 'Competencia', 'Cliente', 'Timing'],
// Subcategorías por categoría (catálogo del tablero Motivos No Compra,
// sembrado en Frasa-CAS el 2026-09-04; 'Mesualidad' y 'Cambo de Planes'
// conservan las faltas de ortografía originales del tablero del cliente)
SUBCATEGORIAS: {
Precio: ['Precio Total', 'Enganche', 'Mesualidad'],
Producto: ['Distribucion', 'Tamaño', 'Recamaras', 'Baños', 'Amenidades', 'Acabados', 'Estacionamiento', 'Seguridad', 'Inventario'],
Ubicacion: ['Zona', 'Accesos', 'Distancia', 'Servicios', 'Seguridad', 'Escuela', 'Trabajo'],
Financiamiento: ['No Califica', 'Credito Insuficiente', 'Tasa de interes', 'Mensualidad Alta', 'Enganche Alto', 'Forma de Pago', 'Infonavit Insuficiente', 'No tiene liquidez'],
Competencia: ['Precio', 'Producto', 'Ubicacion', 'Promocion', 'Financiamiento'],
Cliente: ['Cambo de Planes', 'Decision Familiar', 'No estaba Listo', 'Emergencia Personal'],
Timing: ['Compra Posterior', 'Esperando Credito', 'Esperando Venta de Propiedad', 'Esperando Proyecto'],
},
// Labels de la columna 'Origen' (board Leads) / 'Origen del Lead'
// (Oportunidades), verificados y sembrados en Monday el 2026-09-04
ORIGENES: ['PASE', 'PROSPECCION ASESOR', 'ACTIVACION', 'FACEBOOK FRASA', 'REFERIDO', 'BROCKER', 'OTRO'],
// Labels reales de la columna status 'Desarrollo' del board Leads (verificados en Monday)
DESARROLLOS: ['ATELIER', 'DIAGONAL', 'ESTANZA', 'SENDA', 'SOLANA'],
} as const
// Labels reales de la columna status 'Desarrollo' del board Leads (verificados en Monday)
export const DESARROLLOS_LEAD = ['ATELIER', 'DIAGONAL', 'ESTANZA', 'SENDA', 'SOLANA'] as const
// Constantes derivadas (nombres estables que consume el resto de la app)
export const ORIGENES_LEAD = VOCABULARIO.ORIGENES
// Estados de la columna status del board Leads (vocabulario real del API:
// ESTADOS_LEAD_PERMITIDOS + NUEVO/DESCARTADO); alimenta el filtro por estado
export const ESTADOS_LEAD = [
'NUEVO',
'CONTACTADO',
'Intento de contacto',
'INTERESADO',
'AGENDA CITA',
'CALIFICADO',
'DESCARTADO',
] as const
export const DESARROLLOS_LEAD = VOCABULARIO.DESARROLLOS
// Categorías del dropdown Motivo No Compra (vocabulario CRM FRASA, igual que el API)
export const MOTIVOS_NO_COMPRA = [
'Precio',
'Producto',
'Ubicacion',
'Financiamiento',
'Competencia',
'Cliente',
'Timing',
] as const
// Alimenta el filtro por estado de Mis leads
export const ESTADOS_LEAD = VOCABULARIO.FASES_LEAD
export const MOTIVOS_NO_COMPRA = VOCABULARIO.MOTIVOS_NO_COMPRA
export type MotivoNoCompra = (typeof MOTIVOS_NO_COMPRA)[number]
// Subcategorías por categoría del tablero Motivos No Compra (catálogo del
// workspace de referencia CRM FRASA, sembrado en Frasa-CAS el 2026-09-04).
// Espejo de SUBCATEGORIAS_NO_COMPRA del API (mantener sincronizados).
export const SUBCATEGORIAS_NO_COMPRA: Record<MotivoNoCompra, string[]> = {
Precio: ['Precio Total', 'Enganche', 'Mesualidad'],
Producto: ['Distribucion', 'Tamaño', 'Recamaras', 'Baños', 'Amenidades', 'Acabados', 'Estacionamiento', 'Seguridad', 'Inventario'],
Ubicacion: ['Zona', 'Accesos', 'Distancia', 'Servicios', 'Seguridad', 'Escuela', 'Trabajo'],
Financiamiento: ['No Califica', 'Credito Insuficiente', 'Tasa de interes', 'Mensualidad Alta', 'Enganche Alto', 'Forma de Pago', 'Infonavit Insuficiente', 'No tiene liquidez'],
Competencia: ['Precio', 'Producto', 'Ubicacion', 'Promocion', 'Financiamiento'],
Cliente: ['Cambo de Planes', 'Decision Familiar', 'No estaba Listo', 'Emergencia Personal'],
Timing: ['Compra Posterior', 'Esperando Credito', 'Esperando Venta de Propiedad', 'Esperando Proyecto'],
}
export const SUBCATEGORIAS_NO_COMPRA: Record<MotivoNoCompra, readonly string[]> = VOCABULARIO.SUBCATEGORIAS
// LADAs soportadas en la captura de teléfonos (México default, USA opcional)
export const LADAS = [
@@ -193,20 +200,11 @@ export function urlWhatsApp(nombre: string, telefono: string, vendedor: string):
return `https://wa.me/${telefonoParaEnlace(telefono)}?text=${texto}`
}
// Fases del funnel de Oportunidades (labels REALES del board; el API las valida tal cual)
export const FASES_OPORTUNIDAD = ['Agendo Cita', 'Afluencia', 'Cotizador', 'Aparto', 'No Asistio', 'Abandono'] as const
// Fases del funnel de Oportunidades y del pipeline Enkontrol (del VOCABULARIO)
export const FASES_OPORTUNIDAD = VOCABULARIO.FASES_OPORTUNIDAD
export type FaseOportunidad = (typeof FASES_OPORTUNIDAD)[number]
// Fases del pipeline Enkontrol del tablero Apartado (labels REALES del board)
export const FASES_APARTADO = [
'Pago Apartado',
'Alta Enkontrol',
'Entrego Documentacion',
'Cita Clientes',
'Contrato Enkontrol',
'Firma Cliente',
'Cancelacion Apartado',
] as const
export const FASES_APARTADO = VOCABULARIO.FASES_APARTADO
export type FaseApartado = (typeof FASES_APARTADO)[number]
// Texto bonito de los botones (el valor que se manda al API es el label real)
@@ -220,17 +218,7 @@ export const ETIQUETA_FASE_APARTADO: Record<FaseApartado, string> = {
'Cancelacion Apartado': 'Cancelación',
}
export const FASES_ESCRITURACION = [
'Venta',
'Seguimiento',
'Integracion',
'X Autorizar',
'Autorizado',
'X Escriturar',
'Escriturado',
'Titulado',
'Cancelo',
] as const
export const FASES_ESCRITURACION = VOCABULARIO.FASES_ESCRITURACION
export type FaseEscrituracion = (typeof FASES_ESCRITURACION)[number]
export const ETIQUETA_FASE_ESCRITURACION: Record<FaseEscrituracion, string> = {

View File

@@ -51,7 +51,6 @@ import { exportarLeads, exportarOportunidades, exportarApartados } from '../serv
import { FiltrosAvanzados, aplicarFiltros, FILTROS_VACIOS, type FiltrosCRM } from '../components/FiltrosAvanzados'
import { getSesion } from '../services/auth'
import DashboardVendedor from '../components/DashboardVendedor'
import ScoreBadge, { calcularScore } from '../components/ScoreBadge'
import KanbanView from '../components/KanbanView'
import CalendarioCitas from '../components/CalendarioCitas'
import DetalleLeadModal from '../components/DetalleLeadModal'
@@ -618,7 +617,6 @@ function FilaLead({
>
{lead.nombre}
</button>
<ScoreBadge score={calcularScore(lead)} />
{/* Insignia de procedencia: gris = tablero Leads, azul = Oportunidades */}
<span
@@ -1677,6 +1675,17 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
<DetalleLeadModal
item={leadSeleccionado}
onCerrar={() => setLeadSeleccionado(null)}
onGuardarCita={
'modeloInteres' in leadSeleccionado
? (fechaHora) => {
const op = leadSeleccionado
return ejecutar(
() => cambiarEtapaOportunidad(op.id, 'Agendo Cita', fechaHora),
'No se pudo guardar la cita',
)
}
: undefined
}
/>
)}
</>