feat(9-mejoras): limpieza leads + admin CRUD + calendario + CRM360 FRASA

1. Leads limpiados (207 eliminados, 18 apartados conservados)
2. Round-robin desactivado (config round_robin_habilitado=false)
3. Admin puede borrar/reasignar/mover leads (DELETE/PATCH/POST)
4. Calendario: fechaCita guardada y pasada correctamente
5. Avisos Monday eliminados (BannerModoLocal, ChipPendienteSync, vinculación)
6. Contactos filtrados por usuario asignado
7. Modal bitácora: notas persisten entre fases
8. Fases oportunidades: Agendo→Afluencia→Cotizar→Aparto
9. Toast de nota eliminado en cambios de status
10. Branding: CRM360 FRASA + logo en todas las apps
11. 7 apps construidas
This commit is contained in:
2026-09-10 07:05:40 +00:00
parent 356bf46216
commit 16cacd5f31
43 changed files with 246 additions and 190 deletions

View File

@@ -6,7 +6,7 @@
<meta name="theme-color" content="#1B4332" />
<link rel="manifest" href="/ai/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/ai/icons/icon-192.png" />
<title>FRASA Desarrollos — Asistente IA</title>
<title>CRM360 FRASA — Asistente IA</title>
</head>
<body>
<div id="root"></div>

View File

@@ -1,5 +1,5 @@
{
"name": "Frasa · Asistente IA",
"name": "CRM360 FRASA · Asistente IA",
"short_name": "Asistente IA",
"start_url": "/ai/",
"scope": "/ai/",

View File

@@ -26,7 +26,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
<div className="w-full max-w-sm rounded-xl border border-[#E5E7EB] bg-white p-8 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm">
<div className="mb-10 text-center">
<p className="text-lg font-bold tracking-wide">
FRASA<span className="text-[#F97316]">.</span>
CRM360 FRASA<span className="text-[#F97316]">.</span>
</p>
<p className="mt-2 text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
Acceso al ecosistema

View File

@@ -6,7 +6,7 @@
<meta name="theme-color" content="#1B4332" />
<link rel="manifest" href="/client/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/client/icons/icon-192.png" />
<title>FRASA Desarrollos — Alta de Clientes</title>
<title>CRM360 FRASA — Alta de Clientes</title>
</head>
<body>
<div id="root"></div>

View File

@@ -1,5 +1,5 @@
{
"name": "Frasa · Alta de clientes",
"name": "CRM360 FRASA · Alta de clientes",
"short_name": "Alta clientes",
"start_url": "/client/",
"scope": "/client/",

View File

@@ -25,7 +25,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
<div className="flex min-h-screen items-center justify-center bg-[#F8FAFC] px-4">
<div className="w-full max-w-sm rounded-xl border border-[#E5E7EB] bg-white p-8 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm">
<div className="mb-10 text-center">
<p className="text-lg font-bold tracking-widest text-[#111827] dark:text-[#E2E8F0]">FRASA<span className="text-[#F97316]">.</span></p>
<p className="text-lg font-bold tracking-widest text-[#111827] dark:text-[#E2E8F0]">CRM360 FRASA<span className="text-[#F97316]">.</span></p>
<p className="mt-2 text-xs font-medium uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
Acceso al ecosistema
</p>

View File

@@ -2,7 +2,6 @@ import { useEffect, useRef, useState, type ReactNode } from 'react'
import AppHeader from '../components/AppHeader'
import { SkeletonCard } from "../components/Skeleton"
import toast from "react-hot-toast"
import BannerModoLocal from '../components/BannerModoLocal'
import Stepper from '../components/Stepper'
import DatosGeneralesForm from '../forms/DatosGeneralesForm'
import EmpleoForm from '../forms/EmpleoForm'
@@ -349,7 +348,6 @@ export default function AltaClientePage() {
return (
<div className="min-h-screen bg-[#F8FAFC] dark:bg-[#0F172A]">
<AppHeader titulo="Alta de Clientes" />
<BannerModoLocal />
<main className="px-4 py-8 sm:px-6 sm:py-12 lg:px-8">
<div className="mx-auto max-w-4xl">
<p className="text-sm text-[#64748B] dark:text-[#94A3B8]">

View File

@@ -6,7 +6,7 @@
<meta name="theme-color" content="#1B4332" />
<link rel="manifest" href="/pay/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/pay/icons/icon-192.png" />
<title>FRASA Desarrollos — Clientes</title>
<title>CRM360 FRASA — Clientes</title>
</head>
<body>
<div id="root"></div>

View File

@@ -1,5 +1,5 @@
{
"name": "Frasa · Clientes",
"name": "CRM360 FRASA · Clientes",
"short_name": "Clientes",
"start_url": "/pay/",
"scope": "/pay/",

View File

@@ -26,7 +26,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
<div className="w-full max-w-sm rounded-xl border border-[#E5E7EB] bg-white p-8 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm">
<div className="mb-10 text-center">
<p className="text-lg font-bold tracking-widest">
FRASA<span className="text-[#F97316]">.</span>
CRM360 FRASA<span className="text-[#F97316]">.</span>
</p>
<p className="mt-2 text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
Acceso al ecosistema

View File

@@ -7,7 +7,7 @@
<link rel="manifest" href="/quote/manifest.webmanifest" />
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" href="/quote/icons/icon-192.png" />
<title>FRASA Desarrollos — Cotizador</title>
<title>CRM360 FRASA — Cotizador</title>
</head>
<body>
<div id="root"></div>

View File

@@ -1,6 +1,6 @@
{
"name": "FRASA CRM",
"short_name": "FRASA",
"name": "CRM360 FRASA",
"short_name": "CRM360",
"start_url": "/quote/",
"display": "standalone",
"background_color": "#F8FAFC",

View File

@@ -1,5 +1,5 @@
{
"name": "Frasa · Cotizador",
"name": "CRM360 FRASA · Cotizador",
"short_name": "Cotizador",
"start_url": "/quote/",
"scope": "/quote/",

View File

@@ -26,7 +26,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
<div className="w-full max-w-sm rounded-xl border border-[#E5E7EB] bg-white p-8 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm">
<div className="mb-8 text-center">
<p className="text-lg font-bold tracking-wide">
FRASA<span className="text-[#F97316]">.</span>
CRM360 FRASA<span className="text-[#F97316]">.</span>
</p>
<p className="mt-2 text-xs font-medium text-[#64748B] dark:text-[#94A3B8]">
Acceso al ecosistema

View File

@@ -5,7 +5,6 @@ import { z } from 'zod'
import QRCode from 'qrcode'
import { useUnidades } from '../hooks/useUnidades'
import { useCotizacion } from '../hooks/useCotizacion'
import BannerModoLocal from '../components/BannerModoLocal'
import DarkModeToggle from '../components/DarkModeToggle'
import { SkeletonList } from "../components/Skeleton"
import toast from "react-hot-toast"
@@ -307,7 +306,6 @@ export default function CotizadorPage() {
return (
<div className="min-h-screen bg-[#F8FAFC] dark:bg-[#0F172A]">
<BannerModoLocal />
<main className="px-4 py-8 sm:px-6 lg:px-8 lg:py-10">
{/* Encabezado de página */}
<div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">

View File

@@ -6,7 +6,7 @@
<meta name="theme-color" content="#1B4332" />
<link rel="manifest" href="/dash/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/dash/icons/icon-192.png" />
<title>FRASA Desarrollos — Tableros</title>
<title>CRM360 FRASA — Tableros</title>
</head>
<body>
<div id="root"></div>

View File

@@ -1,5 +1,5 @@
{
"name": "Frasa · Tableros",
"name": "CRM360 FRASA · Tableros",
"short_name": "Tableros",
"start_url": "/dash/",
"scope": "/dash/",

View File

@@ -26,7 +26,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
<div className="w-full max-w-sm rounded-xl border border-[#E5E7EB] bg-white p-8 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm">
<div className="mb-10 text-center">
<p className="text-lg font-bold tracking-widest">
FRASA<span className="text-[#F97316]">.</span>
CRM360 FRASA<span className="text-[#F97316]">.</span>
</p>
<p className="mt-2 text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
Acceso al ecosistema

View File

@@ -4,7 +4,6 @@ import TableroGeneral from '../components/TableroGeneral'
import type { AccionesTablero } from '../components/comunes'
import { esRolConEquipo } from '../lib/mi-tablero'
import { getMe, getSesion } from '../services/auth'
import BannerModoLocal from '../components/BannerModoLocal'
import DarkModeToggle from '../components/DarkModeToggle'
type Vista = 'mia' | 'general'
@@ -45,7 +44,6 @@ export default function DashboardPage() {
return (
<div>
<BannerModoLocal />
<header className="sticky top-0 z-10 border-b border-[#E5E7EB] bg-white/95 backdrop-blur dark:border-[#1E293B] dark:bg-[#1E293B]/95">
<div className="flex min-h-14 flex-wrap items-center justify-between gap-x-4 gap-y-2 px-4 py-2 sm:px-6 lg:px-8">
<div className="flex items-center gap-2.5">

View File

@@ -4,10 +4,12 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#1B4332" />
<link rel="icon" type="image/png" sizes="192x192" href="/favicon.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
<meta name="description" content="Página de entrada al ecosistema comercial de FRASA Desarrollos" />
<title>FRASA Desarrollos — Ecosistema comercial</title>
<meta name="description" content="CRM360 FRASA — Ecosistema comercial" />
<title>CRM360 FRASA — Ecosistema comercial</title>
</head>
<body>
<div id="root"></div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

View File

@@ -1,12 +1,14 @@
{
"name": "Frasa Desarrollos",
"short_name": "Frasa",
"name": "CRM360 FRASA",
"short_name": "CRM360",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#1B4332",
"icons": [
{ "src": "/favicon.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/logo.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
]

View File

@@ -3,7 +3,6 @@ import { MODULOS, esModuloVisible } from './data/modulos'
import { Navbar } from './components/Navbar'
import { TarjetaModulo } from './components/TarjetaModulo'
import { ConfiguracionCuenta } from './components/ConfiguracionCuenta'
import BannerModoLocal from './components/BannerModoLocal'
import { VistaContactos } from './components/VistaContactos'
import { ConfigPage } from './components/config/ConfigPage'
import { CapturaPage } from './pages/CapturaPage'
@@ -49,7 +48,7 @@ export default function App() {
return (
<div className="flex min-h-screen flex-col">
<Navbar usuario={usuario} onConfiguraciones={() => setVista('configuraciones')} />
<BannerModoLocal />
<VistaContactos onVolver={() => setVista('home')} />
</div>
)
@@ -59,7 +58,7 @@ export default function App() {
return (
<div className="flex min-h-screen flex-col">
<Navbar usuario={usuario} onConfiguraciones={() => setVista('configuraciones')} />
<BannerModoLocal />
<CapturaPage onVolver={() => setVista('home')} />
</div>
)
@@ -69,7 +68,7 @@ export default function App() {
return (
<div className="flex min-h-screen flex-col">
<Navbar usuario={usuario} onConfiguraciones={() => setVista('configuraciones')} />
<BannerModoLocal />
<ClientesMaestroPage onVolver={() => setVista('home')} />
</div>
)
@@ -79,7 +78,7 @@ export default function App() {
return (
<div className="flex min-h-screen flex-col">
<Navbar usuario={usuario} onConfiguraciones={() => setVista('configuraciones')} />
<BannerModoLocal />
<ConfigPage onVolver={() => setVista('home')} />
</div>
)
@@ -88,7 +87,6 @@ export default function App() {
return (
<div className="flex min-h-screen flex-col">
<Navbar usuario={usuario} onConfiguraciones={() => setVista('configuraciones')} />
<BannerModoLocal />
<main className="w-full flex-1 px-4 sm:px-6 lg:px-8">
<section aria-labelledby="titulo-hero" className="py-12 sm:py-16">
@@ -99,7 +97,7 @@ export default function App() {
Ecosistema comercial
</h1>
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-[#64748B] dark:text-[#94A3B8] sm:text-base">
Las herramientas del equipo comercial de Frasa Desarrollos en un solo lugar:
Las herramientas del equipo comercial de CRM360 FRASA en un solo lugar:
cotizaciones, alta de clientes, cobranza, tableros y asistente IA.
</p>
</section>
@@ -131,7 +129,7 @@ export default function App() {
<footer className="mt-16 border-t border-[#E5E7EB] dark:border-[#1E293B]">
<div className="px-6 py-8 sm:px-6 lg:px-8">
<p className="text-xs text-[#64748B] dark:text-[#94A3B8]">Consult (AS) Technology</p>
<p className="text-xs text-[#64748B] dark:text-[#94A3B8]">CRM360 FRASA &middot; Consult (AS) Technology</p>
</div>
</footer>
</div>

View File

@@ -1,6 +1,6 @@
import { useEffect, useState, type FormEvent } from 'react'
import { ErrorLogin, getSesion, login, logout, type UsuarioSesion } from '../services/auth'
import { getMe, getUsuariosMonday, guardarVinculoMonday, type UsuarioMonday } from '../services/api'
import { getMe } from '../services/api'
const labelEstilo = 'block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]'
const inputEstilo =
@@ -27,39 +27,20 @@ export function ConfiguracionCuenta() {
const [entrando, setEntrando] = useState(false)
const [errorLogin, setErrorLogin] = useState('')
// ---------- Perfil + usuarios Monday ----------
// ---------- Perfil ----------
const [perfil, setPerfil] = useState<UsuarioSesion | null>(null)
const [cargandoPerfil, setCargandoPerfil] = useState(false)
const [usuarios, setUsuarios] = useState<UsuarioMonday[] | null>(null)
const [errorUsuarios, setErrorUsuarios] = useState('')
const [seleccion, setSeleccion] = useState('')
const [guardando, setGuardando] = useState(false)
const [aviso, setAviso] = useState('')
const [errorGuardar, setErrorGuardar] = useState('')
const [errorPerfil, setErrorPerfil] = useState('')
useEffect(() => {
if (!conSesion) return
setCargandoPerfil(true)
getMe()
.then((u) => {
setPerfil(u)
setSeleccion(u.mondayUserId !== null ? String(u.mondayUserId) : '')
})
.catch(() => setErrorGuardar('No se pudo cargar tu perfil.'))
.then((u) => setPerfil(u))
.catch(() => setErrorPerfil('No se pudo cargar tu perfil.'))
.finally(() => setCargandoPerfil(false))
cargarUsuarios()
}, [conSesion])
const cargarUsuarios = () => {
setErrorUsuarios('')
getUsuariosMonday()
.then(setUsuarios)
.catch(() => {
setUsuarios(null)
setErrorUsuarios('No se pudo cargar la lista de usuarios de Monday.')
})
}
const entrar = async (e: FormEvent) => {
e.preventDefault()
setEntrando(true)
@@ -76,32 +57,12 @@ export function ConfiguracionCuenta() {
}
}
const guardar = async (mondayUserId: number | null) => {
setGuardando(true)
setAviso('')
setErrorGuardar('')
try {
const actualizado = await guardarVinculoMonday(mondayUserId)
setPerfil(actualizado)
setSeleccion(actualizado.mondayUserId !== null ? String(actualizado.mondayUserId) : '')
setAviso(mondayUserId === null ? 'Vínculo con Monday eliminado' : 'Vínculo con Monday guardado')
} catch {
setErrorGuardar('No se pudo guardar el vínculo. Intenta de nuevo.')
} finally {
setGuardando(false)
}
}
const cerrarSesion = () => {
logout()
setPerfil(null)
setUsuarios(null)
setConSesion(false)
}
const vinculado =
perfil?.mondayUserId != null ? (usuarios?.find((u) => u.id === perfil.mondayUserId) ?? null) : null
return (
<section
id="configuracion"
@@ -118,7 +79,7 @@ export function ConfiguracionCuenta() {
className="mt-6 max-w-sm"
>
<p className="text-sm leading-relaxed text-[#64748B] dark:text-[#94A3B8]">
Inicia sesión para vincular tu usuario de Monday.
Inicia sesión para acceder a las herramientas del ecosistema.
</p>
<div className="mt-6">
<label htmlFor="cuenta-username" className={labelEstilo}>
@@ -171,83 +132,10 @@ export function ConfiguracionCuenta() {
Sesión: <span className="font-medium">{perfil.nombre}</span>
</p>
<p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">Rol: {formatearRol(perfil.rol)}</p>
<p className="mt-2 text-sm text-[#64748B] dark:text-[#94A3B8]">
Vínculo Monday actual:{' '}
{perfil.mondayUserId === null ? (
<span className="text-[#94A3B8] dark:text-[#64748B]">sin vincular</span>
) : vinculado ? (
<span className="text-[#111827] dark:text-[#E2E8F0]">
{vinculado.nombre} ({vinculado.email})
</span>
) : (
<span className="text-[#111827] dark:text-[#E2E8F0]">ID {perfil.mondayUserId}</span>
)}
</p>
{errorUsuarios ? (
<div className="mt-6">
<p role="alert" className="text-sm text-red-600">
{errorUsuarios}
</p>
<button
type="button"
onClick={cargarUsuarios}
className="mt-3 rounded-lg border border-[#D1D5DB] bg-white px-4 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-2.5 text-sm font-medium text-[#374151] dark:text-[#CBD5E1] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB] dark:hover:bg-[#334155]"
>
Reintentar
</button>
</div>
) : usuarios === null ? (
<p className="mt-6 text-sm text-[#64748B] dark:text-[#94A3B8]">Cargando usuarios de Monday…</p>
) : (
<div className="mt-6">
<label htmlFor="cuenta-monday" className={labelEstilo}>
Tu usuario de Monday
</label>
<select
id="cuenta-monday"
value={seleccion}
onChange={(e) => setSeleccion(e.target.value)}
className={inputEstilo}
>
<option value="">— Sin vincular —</option>
{usuarios.map((u) => (
<option key={u.id} value={String(u.id)}>
{u.nombre} ({u.email})
</option>
))}
</select>
<div className="mt-4 flex flex-wrap gap-3">
<button
type="button"
disabled={guardando}
onClick={() => void guardar(seleccion === '' ? null : Number(seleccion))}
className="rounded-lg bg-accent px-5 py-2.5 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover disabled:opacity-50"
>
{guardando ? 'Guardando…' : 'Guardar'}
</button>
{perfil.mondayUserId !== null && (
<button
type="button"
disabled={guardando}
onClick={() => void guardar(null)}
className="rounded-lg border border-red-300 bg-white px-5 py-2.5 text-sm font-medium text-red-600 transition-all duration-100 ease-out hover:bg-red-50 disabled:opacity-50"
>
Desvincular
</button>
)}
</div>
</div>
)}
{aviso && (
<p role="status" className="mt-4 text-sm font-medium text-green-600">
{aviso}
</p>
)}
{errorGuardar && (
{errorPerfil && (
<p role="alert" className="mt-4 text-sm text-red-600">
{errorGuardar}
{errorPerfil}
</p>
)}
</>

View File

@@ -29,7 +29,7 @@ export function Navbar({ usuario, onConfiguraciones }: Props) {
<header className="sticky top-0 z-10 border-b border-[#E5E7EB] bg-white/95 backdrop-blur dark:border-[#1E293B] dark:bg-[#1E293B]/95">
<div className="flex h-14 items-center justify-between px-4 sm:px-6 lg:px-8">
<span className="text-base font-semibold tracking-tight text-[#111827] dark:text-[#E2E8F0]">
FRASA<span className="text-accent">.</span>
CRM360 FRASA<span className="text-accent">.</span>
</span>
{/* Desktop nav */}

View File

@@ -35,7 +35,7 @@ export const MODULOS: Modulo[] = [
{
id: 'chat-hermes',
nombre: 'Chat Hermes',
descripcion: 'Asistente IA con la base de conocimiento de Frasa',
descripcion: 'Asistente IA con la base de conocimiento de CRM360 FRASA',
claves: ['chat'],
url: '/ai/',
},

View File

@@ -7,7 +7,7 @@
<link rel="manifest" href="/hoy/manifest.webmanifest" />
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" href="/hoy/icons/icon-192.png" />
<title>FRASA Desarrollos — Mi día</title>
<title>CRM360 FRASA — Mi día</title>
</head>
<body>
<div id="root"></div>

View File

@@ -1,6 +1,6 @@
{
"name": "FRASA CRM",
"short_name": "FRASA",
"name": "CRM360 FRASA",
"short_name": "CRM360",
"start_url": "/hoy/",
"display": "standalone",
"background_color": "#F8FAFC",

View File

@@ -1,5 +1,5 @@
{
"name": "Frasa · Mi día",
"name": "CRM360 FRASA · Mi día",
"short_name": "Mi día",
"start_url": "/hoy/",
"scope": "/hoy/",

View File

@@ -26,7 +26,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
<div className="w-full max-w-sm">
<div className="mb-8 text-center">
<p className="text-lg font-bold tracking-widest text-[#111827] dark:text-[#E2E8F0]">
FRASA<span aria-hidden="true" className="text-[#F97316]">.</span>
CRM360 FRASA<span aria-hidden="true" className="text-[#F97316]">.</span>
</p>
<p className="mt-2 text-xs font-medium uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
Acceso al ecosistema

View File

@@ -1,10 +1,8 @@
import { useCallback, useEffect, useState, type FormEvent } from 'react'
import KpiCard from '../components/KpiCard'
import BannerModoLocal from '../components/BannerModoLocal'
import DarkModeToggle from '../components/DarkModeToggle'
import { SkeletonList } from "../components/Skeleton"
import toast from "react-hot-toast"
import ChipPendienteSync from '../components/ChipPendienteSync'
import type { AccionesSesion } from '../components/AuthGate'
import {
esActividadVencida,
@@ -338,9 +336,9 @@ function FilaOportunidad({
enviando: boolean
acciones?: string[]
}) {
const [conCita, setConCita] = useState(false)
const [fechaCita, setFechaCita] = useState('')
const fases = ['Agendo Cita', 'Afluencia', 'Cotizador'] as const
const [conCita, setConCita] = useState(!!op.fechaCita)
const [fechaCita, setFechaCita] = useState(op.fechaCita ?? '')
const fases = ['Agendo Cita', 'Afluencia', 'Cotizador', 'Aparto'] as const
return (
<li className="rounded-xl border border-[#E5E7EB] bg-white p-4 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm">
<div className="flex flex-wrap items-center justify-between gap-3">
@@ -353,7 +351,7 @@ function FilaOportunidad({
>
{op.nombre}
</button>
{op.pendienteSync === true && <ChipPendienteSync />}
</p>
<p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">
{op.telefono || 'Sin teléfono'}
@@ -469,7 +467,7 @@ function FilaApartado({
>
{apartado.nombre}
</button>
{apartado.pendienteSync === true && <ChipPendienteSync />}
{apartado.unidadCodigo ? (
<span className="text-xs font-normal text-[#94A3B8]">{apartado.unidadCodigo}</span>
) : null}
@@ -732,7 +730,7 @@ function FilaLead({
{lead.nombre}
</button>
<ScoreBadge score={calcularScore(lead)} />
{lead.pendienteSync === true && <ChipPendienteSync />}
{/* Insignia de procedencia: gris = tablero Leads, azul = Oportunidades */}
<span
className={
@@ -816,7 +814,6 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
fuente: 'lead' | 'oportunidad'
} | null>(null)
const [tabCrm, setTabCrm] = useState<TabCrm>('leads')
const [nota, setNota] = useState<string | null>(null)
const [apartadoSeleccionado, setApartadoSeleccionado] = useState<ApartadoMiDia | null>(null)
const [leadSeleccionado, setLeadSeleccionado] = useState<LeadMiDia | OportunidadMiDia | ApartadoMiDia | null>(null)
const [apartadoCancelando, setApartadoCancelando] = useState<{
@@ -993,13 +990,11 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
async function convertir(lead: LeadMiDia) {
setEnviando(true)
setAviso(null)
setNota(null)
try {
await convertirLead(lead.id)
await cargar()
setNota(`${lead.nombre} pasó a Mis oportunidades (Agendo Cita).`)
} catch (err) {
setAviso(err instanceof Error ? err.message : 'No se pudo convertir el lead')
toast.error(err instanceof Error ? err.message : 'No se pudo convertir el lead')
} finally {
setEnviando(false)
}
@@ -1017,7 +1012,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
try {
await cambiarEtapaOportunidad(op.id, 'Cotizador')
} catch {
setAviso('No se pudo actualizar la etapa en Monday; el cotizador se abrirá de todos modos.')
toast.error('No se pudo actualizar la etapa en Monday; el cotizador se abrirá de todos modos.')
}
window.location.href = urlCotizadorParaOportunidad(op)
}
@@ -1079,7 +1074,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
← Menú
</a>
<p className="shrink-0 text-xs font-semibold uppercase tracking-wider text-accent">
FRASA <span aria-hidden="true" className="text-[#F97316]">·</span> Mi día
CRM360 FRASA <span aria-hidden="true" className="text-[#F97316]">·</span> Mi día
</p>
<span aria-hidden="true" className="hidden h-4 w-px bg-[#E5E7EB] sm:block" />
<div className="flex min-w-0 items-center gap-2">
@@ -1120,7 +1115,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
</div>
</header>
<BannerModoLocal />
{offline && (
<div className="px-4 pt-4 sm:px-6 lg:px-8">
@@ -1146,11 +1141,6 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
{aviso}
</p>
)}
{nota && (
<p role="status" className="mb-6 rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm font-medium text-green-700">
{nota}
</p>
)}
{resumen?.sinMapeoMonday && (
<p className="mb-6 rounded-lg border border-[#E5E7EB] bg-white px-4 py-3 text-sm font-medium text-[#64748B] dark:text-[#94A3B8] shadow-sm dark:bg-[#1E293B] dark:border-[#1E293B]">
Tu usuario aún no está vinculado a Monday; las acciones siguen funcionando
@@ -1457,7 +1447,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
<div>
<p className="flex items-center gap-2 text-sm font-medium">
{c.nombre}
{c.pendienteSync === true && <ChipPendienteSync />}
</p>
<p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">
{c.telefono || 'Sin teléfono'}