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

@@ -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'}