From d546460ba493fdd4500986cb867dc334fbf08910 Mon Sep 17 00:00:00 2001 From: Consult AS Technology Date: Wed, 9 Sep 2026 10:26:36 +0000 Subject: [PATCH] feat(8-mejoras): cancelar+motivo, brocker, etapas, calendario, modal cliente MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Cancelar con motivo: Mis apartados y Escrituración piden motivo (modal reutilizado) 2. Origen BROCKER agregado en alta afluencia y captura rápida 3. Etapas oportunidades reordenadas: Agendo Cita → Afluencia → Cotizador 4. Cotizador: mensualidades diferibles (6, 12, 14, 18, 24 meses) 5. Calendario semanal por vendedor en Mi día 6. Hora en citas (datetime-local en vez de solo fecha) 7. Click en nombre → modal DetalleLeadModal (info + notas + historial) --- .../src/controllers/apartados.controller.ts | 8 +- apps/alta-clientes/src/data/constantes.ts | 2 +- apps/cotizador/src/hooks/useCotizacion.ts | 18 +- apps/cotizador/src/pages/CotizadorPage.tsx | 29 ++ .../mi-dia/src/components/CalendarioCitas.tsx | 284 ++++++++++++++++++ .../src/components/DetalleLeadModal.tsx | 205 +++++++++++++ apps/mi-dia/src/lib/mi-dia.test.ts | 2 +- apps/mi-dia/src/lib/mi-dia.ts | 4 +- apps/mi-dia/src/pages/MiDiaPage.tsx | 223 ++++++++++++-- apps/mi-dia/src/services/mi-dia.ts | 15 +- apps/mi-dia/src/services/recordatorios.ts | 44 +++ 11 files changed, 808 insertions(+), 26 deletions(-) create mode 100644 apps/mi-dia/src/components/CalendarioCitas.tsx create mode 100644 apps/mi-dia/src/components/DetalleLeadModal.tsx create mode 100644 apps/mi-dia/src/services/recordatorios.ts diff --git a/api-gateway/src/controllers/apartados.controller.ts b/api-gateway/src/controllers/apartados.controller.ts index 1dc7554..381132d 100644 --- a/api-gateway/src/controllers/apartados.controller.ts +++ b/api-gateway/src/controllers/apartados.controller.ts @@ -39,6 +39,9 @@ export const FASES_APARTADO_PERMITIDAS = [ const cambiarEstadoSchema = z.object({ estado: z.enum(FASES_APARTADO_PERMITIDAS), + motivo: z.string().optional(), + subcategoria: z.string().optional(), + comentario: z.string().optional(), }) type MondayApartados = Pick< @@ -84,7 +87,10 @@ export async function cambiarEstadoApartadoEndpoint(req: Request, res: Response) } } res.json({ itemId, estado: body.estado }) - auditoria.registrar(req.usuarioToken?.sub ?? 'sistema', 'CAMBIAR_ESTADO', 'apartado', itemId, body.estado) + const detalle = body.motivo + ? `${body.estado} — motivo: ${body.motivo}${body.subcategoria ? ` / ${body.subcategoria}` : ''}${body.comentario ? ` (${body.comentario})` : ''}` + : body.estado + auditoria.registrar(req.usuarioToken?.sub ?? 'sistema', 'CAMBIAR_ESTADO', 'apartado', itemId, detalle) } // ---------- Precarga de documentos del expediente (desde "Mis apartados") ---------- diff --git a/apps/alta-clientes/src/data/constantes.ts b/apps/alta-clientes/src/data/constantes.ts index e802c7b..6e9581c 100644 --- a/apps/alta-clientes/src/data/constantes.ts +++ b/apps/alta-clientes/src/data/constantes.ts @@ -95,7 +95,7 @@ export const TAMANO_MAX_DOCUMENTO = 10 * 1024 * 1024 // 10 MB // Catálogos de Monday (labels exactos). Origen: vocabulario FRASA sembrado en // los tableros Leads y Oportunidades el 2026-09-04 (reemplaza al set anterior) -export const ORIGENES_LEAD = ['PASE', 'PROSPECCION ASESOR', 'ACTIVACION', 'FACEBOOK FRASA', 'REFERIDO', 'OTRO'] as const +export const ORIGENES_LEAD = ['PASE', 'PROSPECCION ASESOR', 'ACTIVACION', 'FACEBOOK FRASA', 'REFERIDO', 'BROCKER', 'OTRO'] as const export const DESARROLLOS_LEAD = ['ATELIER', 'DIAGONAL', 'ESTANZA', 'SENDA', 'SOLANA'] as const // Label de la columna status 'Califica' del tablero Leads (captura de afluencia). diff --git a/apps/cotizador/src/hooks/useCotizacion.ts b/apps/cotizador/src/hooks/useCotizacion.ts index 8cb704b..8718cc0 100644 --- a/apps/cotizador/src/hooks/useCotizacion.ts +++ b/apps/cotizador/src/hooks/useCotizacion.ts @@ -38,6 +38,10 @@ interface UseCotizacionResult { propuestaIndex: number setPropuestaIndex: (i: number) => void precioLista: number + /** Número de mensualidades elegido por el vendedor; null = usa el calculado */ + numMensualidades: number | null + setNumMensualidades: (n: number | null) => void + /** Número efectivo de mensualidades (la elegida o la calculada) */ numMeses: number | null propuestas: PropuestaResultado[] /** Restaura el estado completo desde una cotización guardada (historial) */ @@ -66,6 +70,7 @@ export function useCotizacion(): UseCotizacionResult { const [enganchePct, setEnganchePct] = useState(null) const [descuentos, setDescuentos] = useState(descuentosDefault(DESARROLLO_DEFAULT)) const [propuestaIndex, setPropuestaIndex] = useState(0) + const [numMensualidades, setNumMensualidades] = useState(null) const params = obtenerDesarrollo(desarrollo) @@ -75,6 +80,7 @@ export function useCotizacion(): UseCotizacionResult { setAcabados(false) setDescuentos(descuentosDefault(id)) setPropuestaIndex(0) + setNumMensualidades(null) } const setDescuento = (clave: keyof DescuentosPropuestas, valor: number) => { @@ -103,13 +109,17 @@ export function useCotizacion(): UseCotizacionResult { setEnganchePct(null) setDescuentos(descuentosDefault(desarrollo)) setPropuestaIndex(0) + setNumMensualidades(null) } - const numMeses = useMemo( + const numMesesCalculado = useMemo( () => (unidad ? resolverNumMensualidades(unidad, params) : null), [unidad, params], ) + /** Número efectivo de mensualidades: la elegida por el vendedor, o la calculada */ + const numMeses = numMensualidades ?? numMesesCalculado + const precioLista = useMemo( () => (unidad ? obtenerPrecioLista(unidad, acabados) : 0), [unidad, acabados], @@ -125,8 +135,8 @@ export function useCotizacion(): UseCotizacionResult { const propuestas = useMemo(() => { if (!unidad) return [] - return calcularPropuestas({ unidad, acabados, apartado: apartadoEfectivo, descuentos, desarrollo, enganchePct: engancheEfectivo }) - }, [unidad, acabados, apartadoEfectivo, descuentos, desarrollo, engancheEfectivo]) + return calcularPropuestas({ unidad, acabados, apartado: apartadoEfectivo, descuentos, desarrollo, enganchePct: engancheEfectivo, numMeses: numMeses ?? undefined }) + }, [unidad, acabados, apartadoEfectivo, descuentos, desarrollo, engancheEfectivo, numMeses]) return { desarrollo, @@ -148,6 +158,8 @@ export function useCotizacion(): UseCotizacionResult { propuestaIndex, setPropuestaIndex, precioLista, + numMensualidades, + setNumMensualidades, numMeses, propuestas, restaurar, diff --git a/apps/cotizador/src/pages/CotizadorPage.tsx b/apps/cotizador/src/pages/CotizadorPage.tsx index f2ca598..88c8270 100644 --- a/apps/cotizador/src/pages/CotizadorPage.tsx +++ b/apps/cotizador/src/pages/CotizadorPage.tsx @@ -71,6 +71,8 @@ export default function CotizadorPage() { propuestaIndex, setPropuestaIndex, precioLista, + numMensualidades, + setNumMensualidades, numMeses, propuestas, restaurar, @@ -465,6 +467,33 @@ export default function CotizadorPage() { )} +
+ + + {numMeses !== null && ( +

+ Se usarán {numMeses} mensualidades para el cálculo +

+ )} +
+ { setValue('nombre', lead.nombre, { shouldValidate: true }) diff --git a/apps/mi-dia/src/components/CalendarioCitas.tsx b/apps/mi-dia/src/components/CalendarioCitas.tsx new file mode 100644 index 0000000..aadf183 --- /dev/null +++ b/apps/mi-dia/src/components/CalendarioCitas.tsx @@ -0,0 +1,284 @@ +import { useMemo, useState } from 'react' +import type { ActividadMiDia, OportunidadMiDia } from '../lib/mi-dia' +import { formatoFechaCorta } from '../lib/mi-dia' + +/** Evento unificado para el calendario */ +interface EventoCalendario { + id: string + nombre: string + fecha: string // 'YYYY-MM-DD' + hora?: string // 'HH:MM' + tipo: 'actividad' | 'oportunidad' + detalle?: string + estado?: string + idOriginal: string +} + +interface Props { + actividades: ActividadMiDia[] + oportunidades: OportunidadMiDia[] +} + +const NOMBRE_DIAS = ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'] +const NOMBRE_MESES = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'] + +function obtenerSemanaBase(fecha: Date): Date { + const d = new Date(fecha) + const diaSemana = d.getDay() + // Lunes como inicio de semana + d.setDate(d.getDate() - ((diaSemana + 6) % 7)) + d.setHours(0, 0, 0, 0) + return d +} + +function formatoDia(fecha: Date): string { + return `${fecha.getDate()}` +} + +function esMismaFecha(a: Date, b: Date): boolean { + return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate() +} + +function parsearFechaCita(fechaCita: string): { fecha: string; hora?: string } { + if (fechaCita.includes('T')) { + const [fecha, horaCompleta] = fechaCita.split('T') + const hora = horaCompleta?.slice(0, 5) + return { fecha, hora } + } + return { fecha: fechaCita } +} + +export default function CalendarioCitas({ actividades, oportunidades }: Props) { + const hoy = useMemo(() => { + const d = new Date() + d.setHours(0, 0, 0, 0) + return d + }, []) + + const [semanaInicio, setSemanaInicio] = useState(() => obtenerSemanaBase(hoy)) + + const eventos = useMemo(() => { + const lista: EventoCalendario[] = [] + + // Agregar actividades + for (const a of actividades) { + if (a.fecha === '') continue + lista.push({ + id: `act-${a.id}`, + nombre: a.nombre, + fecha: a.fecha, + tipo: 'actividad', + detalle: a.tipo, + estado: a.estado, + idOriginal: a.id, + }) + } + + // Agregar oportunidades con fechaCita + for (const op of oportunidades) { + if (!op.fechaCita) continue + const { fecha, hora } = parsearFechaCita(op.fechaCita) + lista.push({ + id: `op-${op.id}`, + nombre: op.nombre, + fecha, + hora, + tipo: 'oportunidad', + detalle: op.etapa, + estado: op.etapa, + idOriginal: op.id, + }) + } + + return lista + }, [actividades, oportunidades]) + + // Generar los 7 días de la semana + const diasSemana = useMemo(() => { + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(semanaInicio) + d.setDate(d.getDate() + i) + return d + }) + }, [semanaInicio]) + + // Eventos agrupados por día + const eventosPorDia = useMemo(() => { + const mapa = new Map() + for (const ev of eventos) { + const clave = ev.fecha + if (!mapa.has(clave)) mapa.set(clave, []) + mapa.get(clave)!.push(ev) + } + return mapa + }, [eventos]) + + const semanaAnterior = () => { + const d = new Date(semanaInicio) + d.setDate(d.getDate() - 7) + setSemanaInicio(d) + } + + const semanaSiguiente = () => { + const d = new Date(semanaInicio) + d.setDate(d.getDate() + 7) + setSemanaInicio(d) + } + + const irAHoy = () => { + setSemanaInicio(obtenerSemanaBase(hoy)) + } + + const [eventoSeleccionado, setEventoSeleccionado] = useState(null) + + const semanaFin = new Date(semanaInicio) + semanaFin.setDate(semanaFin.getDate() + 6) + + const tituloSemana = `${formatoDia(semanaInicio)} ${NOMBRE_MESES[semanaInicio.getMonth()]} – ${formatoDia(semanaFin)} ${NOMBRE_MESES[semanaFin.getMonth()]} ${semanaFin.getFullYear()}` + + return ( +
+ {/* Controles de navegación */} +
+
+ + + +
+

+ {tituloSemana} +

+
+ + {/* Grid del calendario */} +
+ {/* Encabezados de días */} + {diasSemana.map((dia, idx) => { + const esHoy = esMismaFecha(dia, hoy) + return ( +
+

{NOMBRE_DIAS[dia.getDay()]}

+

{formatoDia(dia)}

+
+ ) + })} + + {/* Celdas de eventos */} + {diasSemana.map((dia, idx) => { + const clave = `${dia.getFullYear()}-${String(dia.getMonth() + 1).padStart(2, '0')}-${String(dia.getDate()).padStart(2, '0')}` + const eventosDia = eventosPorDia.get(clave) ?? [] + const esHoy = esMismaFecha(dia, hoy) + + return ( +
+ {eventosDia.length === 0 ? ( +

—

+ ) : ( +
+ {eventosDia.map((ev) => ( + + ))} +
+ )} +
+ ) + })} +
+ + {/* Leyenda */} +
+ + Actividades + + + Oportunidades + + {eventos.length} evento{eventos.length !== 1 ? 's' : ''} esta semana +
+ + {/* Modal de detalle */} + {eventoSeleccionado && ( +
+
+
+

+ {eventoSeleccionado.tipo === 'oportunidad' ? 'Oportunidad' : 'Actividad'} +

+ +
+

+ {eventoSeleccionado.nombre} +

+
+

+ 📅 {formatoFechaCorta(eventoSeleccionado.fecha)} + {eventoSeleccionado.hora && ` a las ${eventoSeleccionado.hora}`} +

+ {eventoSeleccionado.detalle && ( +

+ 📋 {eventoSeleccionado.detalle} +

+ )} + {eventoSeleccionado.estado && ( +

+ + {eventoSeleccionado.estado} + +

+ )} +
+
+
+ )} +
+ ) +} diff --git a/apps/mi-dia/src/components/DetalleLeadModal.tsx b/apps/mi-dia/src/components/DetalleLeadModal.tsx new file mode 100644 index 0000000..bc89c10 --- /dev/null +++ b/apps/mi-dia/src/components/DetalleLeadModal.tsx @@ -0,0 +1,205 @@ +import { useCallback, useEffect, useState, type FormEvent } from 'react' +import type { LeadMiDia, OportunidadMiDia, ApartadoMiDia } from '../lib/mi-dia' +import { formatoFechaCorta } from '../lib/mi-dia' +import TimelineLead, { type EventoLead } from './TimelineLead' +import { listarRecordatorios, crearRecordatorio, type Recordatorio } from '../services/recordatorios' + +type LeadItem = LeadMiDia | OportunidadMiDia | ApartadoMiDia + +function esLead(item: LeadItem): item is LeadMiDia { + return 'email' in item && 'origen' in item && !('modeloInteres' in item) +} + +function esOportunidad(item: LeadItem): item is OportunidadMiDia { + return 'modeloInteres' in item +} + +export default function DetalleLeadModal({ + item, + onCerrar, +}: { + item: LeadItem + onCerrar: () => void +}) { + const esOp = esOportunidad(item) + const esLd = esLead(item) + + // --- Historial --- + const eventos: EventoLead[] = item.ultimoCambio + ? [{ + fechaISO: item.ultimoCambio.fechaISO, + usuario: item.ultimoCambio.usuario, + detalle: item.ultimoCambio.detalle, + fuente: esLd ? item.fuente : esOp ? undefined : undefined, + }] + : [] + + // --- Notas --- + const [notas, setNotas] = useState([]) + const [cargandoNotas, setCargandoNotas] = useState(true) + const [notaError, setNotaError] = useState('') + const [nuevaNota, setNuevaNota] = useState('') + const [enviandoNota, setEnviandoNota] = useState(false) + + const cargarNotas = useCallback(async () => { + setCargandoNotas(true) + setNotaError('') + try { + const todas = await listarRecordatorios() + setNotas(todas.filter((r) => r.leadId === item.id)) + } catch { + setNotaError('No se pudieron cargar las notas.') + } finally { + setCargandoNotas(false) + } + }, [item.id]) + + useEffect(() => { + void cargarNotas() + }, [cargarNotas]) + + async function agregarNota(e: FormEvent) { + e.preventDefault() + const texto = nuevaNota.trim() + if (texto === '') return + setEnviandoNota(true) + setNotaError('') + try { + await crearRecordatorio({ + titulo: texto, + leadId: item.id, + fechaRecordar: new Date().toISOString(), + }) + setNuevaNota('') + await cargarNotas() + } catch { + setNotaError('No se pudo guardar la nota.') + } finally { + setEnviandoNota(false) + } + } + + // --- Cerrar con Escape --- + useEffect(() => { + function onKey(e: KeyboardEvent) { + if (e.key === 'Escape') onCerrar() + } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, [onCerrar]) + + return ( +
+
+ {/* Header */} +
+

Detalle del lead

+ +
+ + {/* Body scrollable */} +
+ {/* Sección Información */} +
+

Información

+
+ + {esLd && item.telefono && } + {esOp && item.telefono && } + {esLd && item.email && } + {esOp && item.email && } + {esLd && item.desarrollo && } + {esOp && item.desarrollo && } + {esOp && item.modeloInteres && } + {esOp && item.unidadCodigo && } + {esLd && item.origen && } + + {esOp && item.fechaCita && ( + + )} + {!esLd && !esOp && (item as ApartadoMiDia).unidadCodigo && ( + + )} + {!esLd && !esOp && (item as ApartadoMiDia).fechaLimite && ( + + )} + {item.ultimoCambio && ( +
+

+ Último cambio: {item.ultimoCambio.detalle} · {item.ultimoCambio.usuario ?? '—'} ·{' '} + {formatoFechaCorta(item.ultimoCambio.fechaISO.slice(0, 10))} +

+
+ )} +
+
+ + {/* Sección Notas */} +
+

Notas

+
+ setNuevaNota(e.target.value)} + placeholder="Escribe una nota…" + disabled={enviandoNota} + className="flex-1 rounded-lg border border-[#D1D5DB] bg-white px-3 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-2 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20 disabled:opacity-50" + /> + +
+ {notaError && ( +

{notaError}

+ )} + {cargandoNotas ? ( +

Cargando notas…

+ ) : notas.length === 0 ? ( +

Sin notas registradas.

+ ) : ( +
    + {[...notas].reverse().map((n) => ( +
  • +

    {n.titulo}

    + {n.descripcion && ( +

    {n.descripcion}

    + )} +

    + {formatoFechaCorta(n.createdAt.slice(0, 10))} +

    +
  • + ))} +
+ )} +
+ + {/* Sección Historial */} +
+

Historial

+ +
+
+
+
+ ) +} + +function InfoCampo({ etiqueta, valor }: { etiqueta: string; valor: string }) { + return ( +
+

{etiqueta}

+

{valor || '—'}

+
+ ) +} diff --git a/apps/mi-dia/src/lib/mi-dia.test.ts b/apps/mi-dia/src/lib/mi-dia.test.ts index 0c6d37e..969b229 100644 --- a/apps/mi-dia/src/lib/mi-dia.test.ts +++ b/apps/mi-dia/src/lib/mi-dia.test.ts @@ -15,7 +15,7 @@ import { describe('catálogos', () => { it('orígenes = vocabulario FRASA sembrado en Monday (Leads y Oportunidades)', () => { - expect([...ORIGENES_LEAD]).toEqual(['PASE', 'PROSPECCION ASESOR', 'ACTIVACION', 'FACEBOOK FRASA', 'REFERIDO', 'OTRO']) + expect([...ORIGENES_LEAD]).toEqual(['PASE', 'PROSPECCION ASESOR', 'ACTIVACION', 'FACEBOOK FRASA', 'REFERIDO', 'BROCKER', 'OTRO']) }) it('desarrollos = labels reales de la columna Desarrollo del board Leads', () => { diff --git a/apps/mi-dia/src/lib/mi-dia.ts b/apps/mi-dia/src/lib/mi-dia.ts index 472e5fb..8c4ca9a 100644 --- a/apps/mi-dia/src/lib/mi-dia.ts +++ b/apps/mi-dia/src/lib/mi-dia.ts @@ -68,6 +68,8 @@ export interface OportunidadMiDia { unidadCodigo: string updatedAt: string ultimoCambio: { fechaISO: string; usuario: string | null; detalle: string } | null + /** Fecha y hora de la cita agendada (ISO local, ej. '2026-09-09T14:30') */ + fechaCita?: string /** true cuando el item se creó en modo local y aún no se sincroniza a Monday */ pendienteSync?: boolean } @@ -112,7 +114,7 @@ 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', 'OTRO'] as const +export const ORIGENES_LEAD = ['PASE', 'PROSPECCION ASESOR', 'ACTIVACION', 'FACEBOOK FRASA', 'REFERIDO', 'BROCKER', 'OTRO'] 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 diff --git a/apps/mi-dia/src/pages/MiDiaPage.tsx b/apps/mi-dia/src/pages/MiDiaPage.tsx index 924a55d..7ad09ca 100644 --- a/apps/mi-dia/src/pages/MiDiaPage.tsx +++ b/apps/mi-dia/src/pages/MiDiaPage.tsx @@ -13,6 +13,7 @@ import { hoyLocal, DESARROLLOS_LEAD, ETIQUETA_FASE_APARTADO, + ETIQUETA_FASE_ESCRITURACION, ETIQUETA_FASE_OPORTUNIDAD, FASES_APARTADO, MOTIVOS_NO_COMPRA, @@ -55,6 +56,8 @@ import DashboardVendedor from '../components/DashboardVendedor' import ScoreBadge, { calcularScore } from '../components/ScoreBadge' import KanbanView from '../components/KanbanView' import TimelineLead, { type EventoLead } from '../components/TimelineLead' +import CalendarioCitas from '../components/CalendarioCitas' +import DetalleLeadModal from '../components/DetalleLeadModal' function TituloSeccion({ children }: { children: string }) { return

{children}

@@ -111,6 +114,109 @@ const CLASE_INPUT = const CLASE_LABEL = 'mb-2 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]' +/** Modal genérico de descarte/cancelación con motivo + subcategoría + comentario */ +function ModalDescarteGenerico({ + item, + titulo, + onCancelar, + onConfirmar, + enviando, +}: { + item: { id: string; nombre: string } + titulo: string + onCancelar: () => void + onConfirmar: (motivo: MotivoNoCompra, subcategoria: string | undefined, comentario: string) => void + enviando: boolean +}) { + const [motivo, setMotivo] = useState(MOTIVOS_NO_COMPRA[0]) + const [subcategoria, setSubcategoria] = useState('') + const [comentario, setComentario] = useState('') + const subcategorias = SUBCATEGORIAS_NO_COMPRA[motivo] + return ( +
+
+

{titulo}

+

{item.nombre}

+
{ + e.preventDefault() + onConfirmar(motivo, subcategoria === '' ? undefined : subcategoria, comentario.trim()) + }} + > +
+ + +
+
+ + +
+
+ +