From d59c8c0d7e0e9f3ef89160eb47e51d217376f77f Mon Sep 17 00:00:00 2001 From: Consult AS Technology Date: Thu, 3 Sep 2026 04:28:56 +0000 Subject: [PATCH] style: rediseno de layout profesional en las 7 apps Headers sticky con marca y acciones, contenedores max-w-[1280px], grids de contenido con rails laterales sticky (cotizador, mi-dia, detalle de cobranza), stepper vertical en el wizard de alta, toolbar+ tabla unificadas en cobranza, tabs como segmented control en dashboards, chat con barra de entrada fija y preguntas sugeridas, landing con navbar + hero + grid de modulos + footer. --- apps/agente-ia/src/pages/ChatPage.tsx | 115 +-- .../src/components/AltaAfluenciaForm.tsx | 2 +- .../src/components/AppHeader.tsx | 21 + .../alta-clientes/src/components/AuthGate.tsx | 2 +- .../src/components/ChecklistExpediente.tsx | 2 +- .../src/components/SectionCard.tsx | 6 +- apps/alta-clientes/src/components/Stepper.tsx | 109 ++- .../src/forms/CoacreditadoForm.tsx | 4 +- .../src/forms/DocumentosForm.tsx | 4 +- apps/alta-clientes/src/forms/FirmasForm.tsx | 6 +- apps/alta-clientes/src/forms/Navegacion.tsx | 2 +- .../src/forms/ReferenciasForm.tsx | 2 +- .../src/forms/ViviendaCreditoForm.tsx | 2 +- .../src/pages/AltaClientePage.tsx | 595 ++++++++------- apps/cobranza/src/components/KpiCard.tsx | 19 +- apps/cobranza/src/components/Layout.tsx | 12 +- apps/cobranza/src/pages/DashboardPage.tsx | 19 +- apps/cobranza/src/pages/HoyPage.tsx | 4 +- apps/cobranza/src/pages/NuevoPlanPage.tsx | 4 +- apps/cobranza/src/pages/PlanDetallePage.tsx | 151 ++-- apps/cotizador/src/components/AuthGate.tsx | 26 +- .../src/components/ComparadorPropuestas.tsx | 2 +- .../src/components/ComparadorUnidades.tsx | 2 +- apps/cotizador/src/pages/CotizadorPage.tsx | 490 +++++++------ apps/dashboards/src/components/AuthGate.tsx | 2 +- apps/dashboards/src/components/MiTablero.tsx | 79 +- .../src/components/Perfilamiento.tsx | 6 +- .../src/components/SeccionEquipo.tsx | 2 +- .../src/components/TableroGeneral.tsx | 220 +++--- apps/dashboards/src/components/comunes.tsx | 25 + apps/dashboards/src/pages/DashboardPage.tsx | 69 +- apps/landing/src/App.tsx | 37 +- .../src/components/ConfiguracionCuenta.tsx | 10 +- apps/landing/src/components/Navbar.tsx | 46 ++ apps/landing/src/components/TarjetaModulo.tsx | 25 +- apps/landing/src/services/auth.ts | 7 + apps/mi-dia/src/components/AuthGate.tsx | 46 +- apps/mi-dia/src/main.tsx | 2 +- apps/mi-dia/src/pages/MiDiaPage.tsx | 680 ++++++++++-------- 39 files changed, 1622 insertions(+), 1235 deletions(-) create mode 100644 apps/alta-clientes/src/components/AppHeader.tsx create mode 100644 apps/landing/src/components/Navbar.tsx diff --git a/apps/agente-ia/src/pages/ChatPage.tsx b/apps/agente-ia/src/pages/ChatPage.tsx index b72c191..e6b1b39 100644 --- a/apps/agente-ia/src/pages/ChatPage.tsx +++ b/apps/agente-ia/src/pages/ChatPage.tsx @@ -7,6 +7,13 @@ import type { Mensaje } from '../types' let contadorLocal = 0 const idLocal = () => `local-${++contadorLocal}` +const PREGUNTAS_SUGERIDAS = [ + '¿Qué incluye la propuesta del cotizador DIAGONAL?', + '¿Cuáles son los pasos del proceso de venta?', + '¿Qué políticas de Frasa debo conocer?', + '¿Qué modelos hay en el catálogo DIAGONAL?', +] + export default function ChatPage() { const [sessionId, setSessionId] = useState(null) const [mensajes, setMensajes] = useState([]) @@ -70,8 +77,8 @@ export default function ChatPage() { return (
-
-
+
+
FRASA · @@ -80,14 +87,14 @@ export default function ChatPage() {
-
+
{sinConexion && (

Sin conexión al API del asistente. Revisa que el servicio esté disponible e inténtalo de @@ -101,60 +108,76 @@ export default function ChatPage() { )} {mensajes.length === 0 && !escribiendo ? ( -

-

- Asistente interno -

-

- Pregunta sobre el catálogo DIAGONAL, las propuestas del cotizador, el proceso de venta - o las políticas de Frasa. -

+
+
+

+ Asistente interno +

+

+ Pregunta sobre el catálogo DIAGONAL, las propuestas del cotizador, el proceso de + venta o las políticas de Frasa. +

+
+ {PREGUNTAS_SUGERIDAS.map((pregunta) => ( + + ))} +
+
) : (
{mensajes.map((m) => ( ))} - {escribiendo && ( -
-

- Escribiendo… -

-
- )}
)}
-
-
{ - e.preventDefault() - void enviar() - }} - > - - setEntrada(e.target.value)} - placeholder="Escribe tu pregunta…" - autoComplete="off" - className="flex-1 rounded-lg border border-[#D1D5DB] bg-white px-4 py-3 text-sm placeholder:text-[#94A3B8] transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20" - /> - -
+ + setEntrada(e.target.value)} + placeholder="Escribe tu pregunta…" + autoComplete="off" + className="flex-1 rounded-lg border border-[#D1D5DB] bg-white px-4 py-3 text-sm placeholder:text-[#94A3B8] transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20" + /> + + +
) diff --git a/apps/alta-clientes/src/components/AltaAfluenciaForm.tsx b/apps/alta-clientes/src/components/AltaAfluenciaForm.tsx index 8197095..f681c55 100644 --- a/apps/alta-clientes/src/components/AltaAfluenciaForm.tsx +++ b/apps/alta-clientes/src/components/AltaAfluenciaForm.tsx @@ -173,7 +173,7 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Pro } return ( -
void enviar(e)} noValidate className="max-w-2xl"> + void enviar(e)} noValidate>
diff --git a/apps/cotizador/src/components/ComparadorPropuestas.tsx b/apps/cotizador/src/components/ComparadorPropuestas.tsx index 35c66db..2598bc8 100644 --- a/apps/cotizador/src/components/ComparadorPropuestas.tsx +++ b/apps/cotizador/src/components/ComparadorPropuestas.tsx @@ -72,7 +72,7 @@ export function ComparadorPropuestas({ precioLista, apartado, propuestas, titulo } return ( -
+

+

-
-
-

- FRASA DESARROLLOS.{' '} - — Cotizador -

-
-

- {formatoFechaLarga(new Date())} -

-

- Vigencia: hasta el {formatoFechaLarga(calcularVigencia())} -

-
+
+ {/* Encabezado de página */} +
+

Cotizador

+

{formatoFechaLarga(new Date())}

+

+ Vigencia: hasta el {formatoFechaLarga(calcularVigencia())} +

-
-
- {/* Panel de configuración */} - - - {/* Panel de propuestas */} -
+ {/* Secciones de ancho completo */} +
- - {!unidad ? ( -
-

- Selecciona una unidad para ver las propuestas comerciales. -

-
- ) : ( -
-
- {propuestas.map((p, i) => ( - setDescuento(params.propuestas[i].claveDescuento, v)} - /> - ))} -
- p.titulo)} - /> -
-
- - -
- - -
- {aviso &&

{aviso}

} - {!puedeGenerarPdf && !guardando && ( -

- Selecciona una unidad para guardar y generar el PDF. -

- )} -
- )} - -
+

-
+
{sesion.usuario.nombre} · {sesion.usuario.rol} diff --git a/apps/dashboards/src/components/MiTablero.tsx b/apps/dashboards/src/components/MiTablero.tsx index 40dbe12..a25adce 100644 --- a/apps/dashboards/src/components/MiTablero.tsx +++ b/apps/dashboards/src/components/MiTablero.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from 'react' import { Cell, Funnel, FunnelChart, LabelList, ResponsiveContainer, Tooltip } from 'recharts' import KpiCard from './KpiCard' -import { EstadoVacio, TituloSeccion } from './comunes' +import { EstadoVacio, TarjetaGrafica, type AccionesTablero } from './comunes' import { funnelMisLeads, type ResumenMiDia } from '../lib/mi-tablero' import { fetchResumenMiDia } from '../services/mi-dia' @@ -13,7 +13,7 @@ const COLORES_FUNNEL = ['#94A3B8', '#64748B', '#3B82F6', '#2563EB'] * apartados por vencer) y un funnel simple de sus leads por estado. * Fuente: GET /api/mi-dia/resumen (filtra por su mondayUserId en el API). */ -export default function MiTablero() { +export default function MiTablero({ alCambiarAcciones }: { alCambiarAcciones: (acciones: AccionesTablero) => void }) { const [resumen, setResumen] = useState(null) const [error, setError] = useState(null) const [cargando, setCargando] = useState(true) @@ -34,28 +34,22 @@ export default function MiTablero() { void cargar() }, [cargar]) + // Expone "Actualizar" al header sticky de DashboardPage + useEffect(() => { + alCambiarAcciones({ actualizar: () => void cargar(), cargando }) + }, [alCambiarAcciones, cargar, cargando]) + const funnel = resumen ? funnelMisLeads(resumen.misLeads) : [] return ( -
-
-
-

FRASA Desarrollos

-

Mi tablero

- {resumen && ( -

- Actualizado: {new Date(resumen.generadoEn).toLocaleString('es-MX')} -

- )} -
- +
+
+

Mi tablero

+ {resumen && ( +

+ Actualizado: {new Date(resumen.generadoEn).toLocaleString('es-MX')} +

+ )}
{error && ( @@ -75,7 +69,7 @@ export default function MiTablero() { {resumen && ( <> -
+
-
- Mis leads por estado - {funnel.length === 0 ? ( - - ) : ( -
- - - - - {funnel.map((etapa, i) => ( - - ))} - - - - - -
- )} +
+ + {funnel.length === 0 ? ( + + ) : ( +
+ + + + + {funnel.map((etapa, i) => ( + + ))} + + + + + +
+ )} +
)} diff --git a/apps/dashboards/src/components/Perfilamiento.tsx b/apps/dashboards/src/components/Perfilamiento.tsx index c03f38b..0ba81e2 100644 --- a/apps/dashboards/src/components/Perfilamiento.tsx +++ b/apps/dashboards/src/components/Perfilamiento.tsx @@ -20,7 +20,7 @@ const DIMENSIONES: { titulo: string; registro: (perfil: ResumenReportes['leads'] function TarjetaDimension({ titulo, datos }: { titulo: string; datos: Record }) { const rebanadas = rebanadasDe(datos) return ( -
+

{titulo}

{rebanadas.length === 0 ? ( @@ -47,9 +47,9 @@ function TarjetaDimension({ titulo, datos }: { titulo: string; datos: Record +
Perfilamiento de la afluencia -
+
{DIMENSIONES.map((d) => ( ))} diff --git a/apps/dashboards/src/components/SeccionEquipo.tsx b/apps/dashboards/src/components/SeccionEquipo.tsx index b5f5aa9..092cfd6 100644 --- a/apps/dashboards/src/components/SeccionEquipo.tsx +++ b/apps/dashboards/src/components/SeccionEquipo.tsx @@ -30,7 +30,7 @@ export default function SeccionEquipo() { }, [cargar]) return ( -
+
Equipo: leads por vendedor {error && (

diff --git a/apps/dashboards/src/components/TableroGeneral.tsx b/apps/dashboards/src/components/TableroGeneral.tsx index 72ae1d0..a8f367e 100644 --- a/apps/dashboards/src/components/TableroGeneral.tsx +++ b/apps/dashboards/src/components/TableroGeneral.tsx @@ -18,7 +18,7 @@ import { import KpiCard from './KpiCard' import Perfilamiento from './Perfilamiento' import SeccionEquipo from './SeccionEquipo' -import { EstadoVacio, TituloSeccion } from './comunes' +import { EstadoVacio, TarjetaGrafica, type AccionesTablero } from './comunes' import { datosCobranza, datosFunnel, @@ -47,7 +47,13 @@ const COLORES_COBRANZA = ['#2563EB', '#F59E0B'] * pipeline, funnel y cobranza). Con `mostrarEquipo` (coordinador/corporativo/ * admin) agrega la sección Equipo con leads por vendedor. */ -export default function TableroGeneral({ mostrarEquipo }: { mostrarEquipo: boolean }) { +export default function TableroGeneral({ + mostrarEquipo, + alCambiarAcciones, +}: { + mostrarEquipo: boolean + alCambiarAcciones: (acciones: AccionesTablero) => void +}) { const [resumen, setResumen] = useState(null) const [error, setError] = useState(null) const [cargando, setCargando] = useState(true) @@ -68,6 +74,11 @@ export default function TableroGeneral({ mostrarEquipo }: { mostrarEquipo: boole void cargar() }, [cargar]) + // Expone "Actualizar" al header sticky de DashboardPage + useEffect(() => { + alCambiarAcciones({ actualizar: () => void cargar(), cargando }) + }, [alCambiarAcciones, cargar, cargando]) + const unidades = resumen ? unidadesApiladas(resumen) : [] const funnel = resumen ? datosFunnel(resumen) : [] const cobranza = resumen ? datosCobranza(resumen) : [] @@ -76,25 +87,14 @@ export default function TableroGeneral({ mostrarEquipo }: { mostrarEquipo: boole const sinCobranza = resumen !== null && resumen.cobranza.planesAlCorriente + resumen.cobranza.planesConVencidos === 0 return ( -

-
-
-

FRASA Desarrollos

-

Tablero comercial

- {resumen && ( -

- Actualizado: {new Date(resumen.generadoEn).toLocaleString('es-MX')} -

- )} -
- +
+
+

Tablero comercial

+ {resumen && ( +

+ Actualizado: {new Date(resumen.generadoEn).toLocaleString('es-MX')} +

+ )}
{error && ( @@ -151,97 +151,101 @@ export default function TableroGeneral({ mostrarEquipo }: { mostrarEquipo: boole />
-
- Unidades por desarrollo y status - {unidades.length === 0 ? ( - - ) : ( -
- - - - - - - - {Object.keys(COLORES_STATUS).map((status) => ( - - ))} - - -
- )} -
- -
- Pipeline de ventas por etapa - {sinPipeline ? ( - - ) : ( -
- - - - - - - - - -
- )} -
- -
-
- Embudo: leads → oportunidades → ganado - {funnel.every((e) => e.valor === 0) ? ( - - ) : ( -
- - - - - {funnel.map((etapa, i) => ( - +
+
+ + {unidades.length === 0 ? ( + + ) : ( +
+ + + + + + + + {Object.keys(COLORES_STATUS).map((status) => ( + ))} - - - - - -
- )} + + +
+ )} + +
+ +
+ + {sinPipeline ? ( + + ) : ( +
+ + + + + + + + + +
+ )} +
+
+ +
+ + {funnel.every((e) => e.valor === 0) ? ( + + ) : ( +
+ + + + + {funnel.map((etapa, i) => ( + + ))} + + + + + +
+ )} +
- Cobranza: al corriente vs vencidos - {sinCobranza ? ( - - ) : ( -
- - - `${name}: ${value}`} - fontSize={11} - > - {cobranza.map((rebanada, i) => ( - - ))} - - - - -
- )} + + {sinCobranza ? ( + + ) : ( +
+ + + `${name}: ${value}`} + fontSize={11} + > + {cobranza.map((rebanada, i) => ( + + ))} + + + + +
+ )} +
diff --git a/apps/dashboards/src/components/comunes.tsx b/apps/dashboards/src/components/comunes.tsx index 5f8752e..4ddfe4d 100644 --- a/apps/dashboards/src/components/comunes.tsx +++ b/apps/dashboards/src/components/comunes.tsx @@ -1,5 +1,14 @@ // Piezas visuales compartidas por las vistas del tablero (NovaSpark) +import type { ReactNode } from 'react' + +/** + * Acciones que cada vista del tablero expone al header de DashboardPage: + * el botón "Actualizar" vive en el header sticky y dispara la recarga de la + * vista activa (sin cambiar fuentes de datos ni consultas). + */ +export type AccionesTablero = { actualizar: () => void; cargando: boolean } + export function EstadoVacio({ mensaje }: { mensaje: string }) { return (
@@ -11,3 +20,19 @@ export function EstadoVacio({ mensaje }: { mensaje: string }) { export function TituloSeccion({ children }: { children: string }) { return

{children}

} + +/** Tarjeta contenedora de gráficas: título interno + padding consistente. */ +export function TarjetaGrafica({ + titulo, + children, +}: { + titulo: string + children: ReactNode +}) { + return ( +
+

{titulo}

+ {children} +
+ ) +} diff --git a/apps/dashboards/src/pages/DashboardPage.tsx b/apps/dashboards/src/pages/DashboardPage.tsx index 4c5f16e..89e3a64 100644 --- a/apps/dashboards/src/pages/DashboardPage.tsx +++ b/apps/dashboards/src/pages/DashboardPage.tsx @@ -1,6 +1,7 @@ import { useState } from 'react' import MiTablero from '../components/MiTablero' import TableroGeneral from '../components/TableroGeneral' +import type { AccionesTablero } from '../components/comunes' import { esRolConEquipo } from '../lib/mi-tablero' import { getSesion } from '../services/auth' @@ -8,42 +9,70 @@ type Vista = 'mia' | 'general' const VISTAS: { id: Vista; etiqueta: string }[] = [ { id: 'mia', etiqueta: 'Mi tablero' }, - { id: 'general', etiqueta: 'General' }, + { id: 'general', etiqueta: 'Tablero general' }, ] /** * Tableros con vista por rol: el vendedor entra a "Mi tablero" (sus KPIs y el * funnel de sus leads); coordinador/corporativo/admin entran a "General" con - * la sección Equipo. El toggle permite cambiar de vista en cualquier momento. + * la sección Equipo. El header sticky concentra la marca, el segmented control + * de vistas y el botón "Actualizar" de la vista activa. */ export default function DashboardPage() { const rol = getSesion()?.usuario.rol ?? 'vendedor' const conEquipo = esRolConEquipo(rol) const [vista, setVista] = useState(conEquipo ? 'general' : 'mia') + const [acciones, setAcciones] = useState(null) return (
-
-
- {VISTAS.map((v) => ( - + ))} +
+ - ))} +
-
- {vista === 'mia' ? : } + + {vista === 'mia' ? ( + + ) : ( + + )}
) } diff --git a/apps/landing/src/App.tsx b/apps/landing/src/App.tsx index 444bf7e..4e5ad7c 100644 --- a/apps/landing/src/App.tsx +++ b/apps/landing/src/App.tsx @@ -1,33 +1,44 @@ import { MODULOS } from './data/modulos' +import { Navbar } from './components/Navbar' import { TarjetaModulo } from './components/TarjetaModulo' import { ConfiguracionCuenta } from './components/ConfiguracionCuenta' export default function App() { return (
-
-

- FRASA Desarrollos -

-

- Ecosistema comercial -

-
+ + +
+
+

+ Ecosistema comercial +

+

+ Las herramientas del equipo comercial de Frasa Desarrollos en un solo lugar: + cotizaciones, alta de clientes, cobranza, tableros y asistente IA. +

+
-
- +
+ +
-
-

Consult (AS) Technology

+
+
+

Consult (AS) Technology

+
) diff --git a/apps/landing/src/components/ConfiguracionCuenta.tsx b/apps/landing/src/components/ConfiguracionCuenta.tsx index fd43730..9dfc78e 100644 --- a/apps/landing/src/components/ConfiguracionCuenta.tsx +++ b/apps/landing/src/components/ConfiguracionCuenta.tsx @@ -95,7 +95,11 @@ export function ConfiguracionCuenta() { perfil?.mondayUserId != null ? (usuarios?.find((u) => u.id === perfil.mondayUserId) ?? null) : null return ( -
+

Configuración de cuenta

@@ -103,7 +107,7 @@ export function ConfiguracionCuenta() { {!conSesion ? (
void entrar(e)} - className="mt-8 max-w-sm rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm" + className="mt-6 max-w-sm" >

Inicia sesión para vincular tu usuario de Monday. @@ -150,7 +154,7 @@ export function ConfiguracionCuenta() {

) : ( -
+
{cargandoPerfil &&

Cargando tu perfil…

} {perfil && ( diff --git a/apps/landing/src/components/Navbar.tsx b/apps/landing/src/components/Navbar.tsx new file mode 100644 index 0000000..20ca161 --- /dev/null +++ b/apps/landing/src/components/Navbar.tsx @@ -0,0 +1,46 @@ +import { useEffect, useState } from 'react' +import { getSesion } from '../services/auth' + +/** Barra superior: marca a la izquierda, estado de sesión y acceso a configuración a la derecha */ +export function Navbar() { + const [conSesion, setConSesion] = useState(() => getSesion() !== null) + + useEffect(() => { + const actualizar = () => setConSesion(getSesion() !== null) + window.addEventListener('frasa:sesion', actualizar) + window.addEventListener('storage', actualizar) + return () => { + window.removeEventListener('frasa:sesion', actualizar) + window.removeEventListener('storage', actualizar) + } + }, []) + + return ( +
+
+ + FRASA. + +
+ + + + Configuración + +
+
+
+ ) +} diff --git a/apps/landing/src/components/TarjetaModulo.tsx b/apps/landing/src/components/TarjetaModulo.tsx index f97aba2..deda967 100644 --- a/apps/landing/src/components/TarjetaModulo.tsx +++ b/apps/landing/src/components/TarjetaModulo.tsx @@ -4,17 +4,34 @@ interface Props { modulo: Modulo } +// Iniciales del "icono" de cada tarjeta; fallback: primeras 2 letras del nombre +const INICIALES: Record = { + cotizador: 'CO', + 'alta-clientes': 'AC', + cobranza: 'CB', + 'agente-ia': 'IA', + tableros: 'TB', + 'mi-dia': 'MD', +} + /** Tarjeta de acceso a un módulo del ecosistema */ export function TarjetaModulo({ modulo }: Props) { + const iniciales = INICIALES[modulo.id] ?? modulo.nombre.slice(0, 2).toUpperCase() return (
  • -

    {modulo.nombre}

    -

    {modulo.descripcion}

    - + +

    {modulo.nombre}

    +

    {modulo.descripcion}

    + Entrar
    diff --git a/apps/landing/src/services/auth.ts b/apps/landing/src/services/auth.ts index 56435da..04a4148 100644 --- a/apps/landing/src/services/auth.ts +++ b/apps/landing/src/services/auth.ts @@ -40,6 +40,12 @@ export function getToken(): string | null { export function logout(): void { localStorage.removeItem(STORAGE_KEY) + notificarCambioSesion() +} + +/** Avisa a la UI (p. ej. el navbar) que cambió el estado de sesión */ +function notificarCambioSesion(): void { + if (typeof window !== 'undefined') window.dispatchEvent(new Event('frasa:sesion')) } /** POST /auth/login; guarda token + usuario en localStorage */ @@ -57,6 +63,7 @@ export async function login(username: string, password: string): Promise void + onLogout: () => void +} + /** * Gate de autenticación: sin sesión solo se ve el login; con sesión se ve la - * app con una barra discreta "nombre · rol", "Contraseña" (modal de cambio) - * y botón "Salir". + * app. La página recibe las acciones de sesión como render prop para + * integrarlas en su propio header. */ -export default function AuthGate({ children }: { children: ReactNode }) { +export default function AuthGate({ children }: { children: (acciones: AccionesSesion) => ReactNode }) { const [sesion, setSesion] = useState(() => getSesion()) const [modalPassword, setModalPassword] = useState(false) @@ -18,33 +24,13 @@ export default function AuthGate({ children }: { children: ReactNode }) { return (
    -
    -
    - - {sesion.usuario.nombre} · {sesion.usuario.rol} - -
    - - -
    -
    -
    - {children} + {children({ + onCambiarPassword: () => setModalPassword(true), + onLogout: () => { + logout() + setSesion(null) + }, + })} {modalPassword && setModalPassword(false)} />}
    ) diff --git a/apps/mi-dia/src/main.tsx b/apps/mi-dia/src/main.tsx index 0805168..4edcd3a 100644 --- a/apps/mi-dia/src/main.tsx +++ b/apps/mi-dia/src/main.tsx @@ -11,7 +11,7 @@ import MiDiaPage from './pages/MiDiaPage' createRoot(document.getElementById('root')!).render( - + {(accionesSesion) => } , ) diff --git a/apps/mi-dia/src/pages/MiDiaPage.tsx b/apps/mi-dia/src/pages/MiDiaPage.tsx index d31ca05..a3e2ead 100644 --- a/apps/mi-dia/src/pages/MiDiaPage.tsx +++ b/apps/mi-dia/src/pages/MiDiaPage.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useState, type FormEvent } from 'react' import KpiCard from '../components/KpiCard' +import type { AccionesSesion } from '../components/AuthGate' import { esActividadVencida, esRolConEquipo, @@ -179,7 +180,7 @@ function FilaLead({ ) } -export default function MiDiaPage() { +export default function MiDiaPage({ accionesSesion }: { accionesSesion: AccionesSesion }) { const nombreVendedor = getSesion()?.usuario.nombre ?? '' const rol = getSesion()?.usuario.rol ?? 'vendedor' const [resumen, setResumen] = useState(null) @@ -258,340 +259,387 @@ export default function MiDiaPage() { } const hoy = hoyLocal() + const iniciales = nombreVendedor + .split(' ') + .filter((p) => p !== '') + .map((p) => p.charAt(0)) + .slice(0, 2) + .join('') + .toUpperCase() return ( -
    -
    -
    -

    - FRASA Mi día -

    -

    {nombreVendedor}

    + <> +
    +
    +
    +

    + FRASA Mi día +

    +
    +
    + + + +
    - -
    + - {error && ( -

    - Sin datos del API — {error} -

    - )} - {aviso && ( -

    - {aviso} -

    - )} - {resumen?.sinMapeoMonday && ( -

    - Tu usuario aún no está vinculado a Monday; las acciones siguen funcionando -

    - )} +
    +
    +

    {nombreVendedor}

    +

    {formatoFechaCorta(hoy)}

    +
    - {!resumen && cargando && } + {error && ( +

    + Sin datos del API — {error} +

    + )} + {aviso && ( +

    + {aviso} +

    + )} + {resumen?.sinMapeoMonday && ( +

    + Tu usuario aún no está vinculado a Monday; las acciones siguen funcionando +

    + )} - {resumen && ( - <> -
    - - - - -
    + {!resumen && cargando && } -
    - Mis actividades - {resumen.misActividadesHoy.length === 0 ? ( - - ) : ( -
      - {resumen.misActividadesHoy.map((a) => ( -
    • -

      - {a.nombre} - {a.tipo ? {a.tipo} : null} -

      - - {formatoFechaCorta(a.fecha)} - -
    • - ))} -
    - )} -
    - - {resumen.apartadosPorVencer.length > 0 && ( -
    - Apartados por vencer -
      - {resumen.apartadosPorVencer.map((a) => ( -
    • -

      - {a.nombre} - {a.unidadCodigo ? {a.unidadCodigo} : null} -

      - - {textoDiasRestantes(a.diasRestantes)} · {formatoFechaCorta(a.fechaLimite)} - -
    • - ))} -
    + {resumen && ( + <> +
    + + + +
    - )} -
    - Mis leads - {resumen.misLeads.length === 0 ? ( - - ) : ( -
      - {resumen.misLeads.map((lead) => ( - void ejecutar(() => cambiarEstadoLead(lead.id, estado, fecha), 'No se pudo actualizar el lead')} - onDescartar={() => setDescartando(lead)} - /> - ))} -
    - )} -
    +
    +
    +
    + Mis leads + {resumen.misLeads.length === 0 ? ( + + ) : ( +
      + {resumen.misLeads.map((lead) => ( + void ejecutar(() => cambiarEstadoLead(lead.id, estado, fecha), 'No se pudo actualizar el lead')} + onDescartar={() => setDescartando(lead)} + /> + ))} +
    + )} +
    -
    - Mis capturas de afluencia - {resumen.misCapturas.length === 0 ? ( - - ) : ( -
      - {resumen.misCapturas.map((c) => ( -
    • + Mis capturas de afluencia + {resumen.misCapturas.length === 0 ? ( + + ) : ( +
        + {resumen.misCapturas.map((c) => ( +
      • +
        +

        {c.nombre}

        +

        + {c.telefono || 'Sin teléfono'} + {c.origen ? ` · ${c.origen}` : ''} +

        +
        +
        + + {c.estado || '—'} + + + {formatoFechaCorta(c.capturadoEn.slice(0, 10))} + +
        +
      • + ))} +
      + )} +
    + +
    + Mis clientes + {resumen.misClientes.length === 0 ? ( + + ) : ( +
      + {resumen.misClientes.map((c) => ( +
    • +
      +

      {c.nombre}

      +

      + {[c.proyecto, c.unidadCodigo].filter((p) => p !== '').join(' · ') || 'Alta sin proyecto'} +

      +
      +
      + + {c.completado ? 'Cliente' : 'En captura'} + + + {formatoFechaCorta(c.capturadoEn.slice(0, 10))} + +
      +
    • + ))} +
    + )} +
    +
    + +
  • - ))} - - )} -
    - -
    - Mis clientes - {resumen.misClientes.length === 0 ? ( - - ) : ( -
      - {resumen.misClientes.map((c) => ( -
    • -

      {c.nombre}

      -

      - {[c.proyecto, c.unidadCodigo].filter((p) => p !== '').join(' · ') || 'Alta sin proyecto'} -

      + + setCapTelefono(e.target.value)} + className={CLASE_INPUT} + />
      -
      - + + setCapEmail(e.target.value)} + className={CLASE_INPUT} + /> +
      +
      + + +
      +
      + + +
      +
      + + setCapNotas(e.target.value)} + className={CLASE_INPUT} + /> +
      + {capMensaje && ( +

      - {c.completado ? 'Cliente' : 'En captura'} - - - {formatoFechaCorta(c.capturadoEn.slice(0, 10))} - + {capMensaje.texto} +

      + )} +
      +
      -
    • - ))} -
    - )} -
    + +
    + +
    -
    - Captura rápida de lead -
    -
    - - setCapNombre(e.target.value)} - required - className={CLASE_INPUT} - /> -
    -
    - - setCapTelefono(e.target.value)} - className={CLASE_INPUT} - /> -
    -
    - - setCapEmail(e.target.value)} - className={CLASE_INPUT} - /> -
    -
    - - -
    -
    - - -
    -
    - - setCapNotas(e.target.value)} - className={CLASE_INPUT} - /> -
    - {capMensaje && ( -

    - {capMensaje.texto} + {esRolConEquipo(rol) && resumen.equipo && ( +

    + Equipo +

    + Leads nuevos sin asignar: {resumen.equipo.leadsNuevosSinAsignar}

    - )} -
    - -
    - -
    - - {esRolConEquipo(rol) && resumen.equipo && ( -
    - Equipo -

    - Leads nuevos sin asignar: {resumen.equipo.leadsNuevosSinAsignar} -

    - {resumen.equipo.porVendedor.length === 0 ? ( - - ) : ( -
      - {resumen.equipo.porVendedor.map((v) => ( -
    • -

      {v.nombre ?? `Monday #${v.mondayUserId}`}

      - - {v.leadsAsignados} lead(s) - -
    • - ))} -
    - )} -
    - )} - - )} + {resumen.equipo.porVendedor.length === 0 ? ( + + ) : ( +
      + {resumen.equipo.porVendedor.map((v) => ( +
    • +

      {v.nombre ?? `Monday #${v.mondayUserId}`}

      + + {v.leadsAsignados} lead(s) + +
    • + ))} +
    + )} +
    + )} + + )} +
    {descartando && ( )} -
    + ) }