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:
@@ -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>
|
||||
|
||||
BIN
apps/landing/public/apple-touch-icon.png
Normal file
BIN
apps/landing/public/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
apps/landing/public/favicon.png
Normal file
BIN
apps/landing/public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
apps/landing/public/logo.png
Normal file
BIN
apps/landing/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 478 KiB |
@@ -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" }
|
||||
]
|
||||
|
||||
@@ -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 · Consult (AS) Technology</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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/',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user