diff --git a/api-gateway/scripts/limpiar-leads.js b/api-gateway/scripts/limpiar-leads.js new file mode 100644 index 0000000..ad97a34 --- /dev/null +++ b/api-gateway/scripts/limpiar-leads.js @@ -0,0 +1,60 @@ +/** + * Elimina todos los registros de LeadCaptura donde board = 'LEADS' o + * board = 'OPORTUNIDADES'. Muestra conteo antes y después. + * + * Uso: npx tsx scripts/limpiar-leads.js + */ +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import dotenv from 'dotenv' +import { PrismaClient } from '@prisma/client' + +const raizApi = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +dotenv.config({ path: [path.resolve(raizApi, '.env'), path.resolve(raizApi, '..', '.env')] }) + +const prisma = new PrismaClient() + +async function main() { + // ── Conteo antes ── + const total = await prisma.leadCaptura.count() + const leads = await prisma.leadCaptura.count({ where: { board: 'LEADS' } }) + const oportunidades = await prisma.leadCaptura.count({ where: { board: 'OPORTUNIDADES' } }) + const otros = total - leads - oportunidades + + console.log('── Antes de limpiar ──') + console.log(` Total LeadCaptura: ${total}`) + console.log(` board = LEADS: ${leads}`) + console.log(` board = OPORTUNIDADES: ${oportunidades}`) + console.log(` Otros (APARTADO, etc.): ${otros}`) + + if (leads + oportunidades === 0) { + console.log('\nNo hay leads ni oportunidades que borrar.') + return + } + + // ── Eliminar leads y oportunidades ── + const deleted = await prisma.leadCaptura.deleteMany({ + where: { board: { in: ['LEADS', 'OPORTUNIDADES'] } }, + }) + console.log(`\nEliminados: ${deleted.count} registros`) + + // ── Conteo después ── + const totalDespues = await prisma.leadCaptura.count() + const leadsDespues = await prisma.leadCaptura.count({ where: { board: 'LEADS' } }) + const opDespues = await prisma.leadCaptura.count({ where: { board: 'OPORTUNIDADES' } }) + const otrosDespues = totalDespues - leadsDespues - opDespues + + console.log('\n── Después de limpiar ──') + console.log(` Total LeadCaptura: ${totalDespues}`) + console.log(` board = LEADS: ${leadsDespues}`) + console.log(` board = OPORTUNIDADES: ${opDespues}`) + console.log(` Otros (APARTADO, etc.): ${otrosDespues}`) + console.log('\n✅ Limpieza completada.') +} + +main() + .catch((err) => { + console.error('❌ Error:', err) + process.exit(1) + }) + .finally(() => prisma.$disconnect()) diff --git a/api-gateway/scripts/seed-config.js b/api-gateway/scripts/seed-config.js index 213c1e4..4b60383 100644 --- a/api-gateway/scripts/seed-config.js +++ b/api-gateway/scripts/seed-config.js @@ -28,6 +28,7 @@ const CONFIGS_DEFAULT = [ { clave: 'auto_logout', valor: 'true' }, { clave: 'mostrar_precios_lista', valor: 'true' }, { clave: 'max_upload_mb', valor: '10' }, + { clave: 'round_robin_habilitado', valor: 'false' }, ] // ── RolConfiguracion: módulos por rol (matriz de config/roles.ts) ────── diff --git a/api-gateway/src/controllers/contactos.controller.ts b/api-gateway/src/controllers/contactos.controller.ts index 41edfc1..a8be44f 100644 --- a/api-gateway/src/controllers/contactos.controller.ts +++ b/api-gateway/src/controllers/contactos.controller.ts @@ -13,16 +13,17 @@ import { NotFoundError } from '../utils/errors.js' * (nombre, teléfono, email, estado), lo más reciente primero, máx 100. * Monday caído (o 502) → lista local (Cliente + LeadCaptura), mismo shape. */ -export async function listarContactosEndpoint(_req: Request, res: Response): Promise { +export async function listarContactosEndpoint(req: Request, res: Response): Promise { + const usuarioId = req.usuarioToken?.sub if (!saludMonday.estaDisponible()) { - res.json(await listarContactosLocal()) + res.json(await listarContactosLocal(usuarioId)) return } try { res.json(await mondayService.getContactos()) } catch (err) { if (!esFalloMonday(err)) throw err - res.json(await listarContactosLocal()) + res.json(await listarContactosLocal(usuarioId)) } } diff --git a/api-gateway/src/controllers/leads.controller.ts b/api-gateway/src/controllers/leads.controller.ts index 1caab23..3dd07d7 100644 --- a/api-gateway/src/controllers/leads.controller.ts +++ b/api-gateway/src/controllers/leads.controller.ts @@ -798,6 +798,81 @@ export async function convertirLeadEndpoint(req: Request, res: Response): Promis } } +// ── Admin CRUD de leads ──────────────────────────────────────────────── + +/** DELETE /api/leads/:itemId — eliminar lead (solo admin) */ +export async function eliminarLeadEndpoint(req: Request, res: Response): Promise { + const itemId = String(req.params['itemId']) + const row = await prisma.leadCaptura.findUnique({ where: { leadId: itemId } }) + // Eliminar de Monday si es item remoto y Monday está disponible + if (row && row.mondayItemId && !esIdLocal(itemId) && saludMonday.estaDisponible()) { + try { + await mondayService.eliminarItem(row.mondayItemId) + } catch (err) { + if (!esFalloMonday(err)) throw err + } + } + // Eliminar la fila local (si existe) + if (row) { + await prisma.leadCaptura.delete({ where: { leadId: itemId } }).catch(() => {}) + } + res.json({ eliminado: true, itemId }) + auditoria.registrar(req.usuarioToken?.sub ?? 'sistema', 'ELIMINAR_LEAD', 'lead', itemId) +} + +const asignarLeadSchema = z.object({ + usuarioId: z.string().trim().min(1, 'usuarioId es obligatorio'), +}) + +/** PATCH /api/leads/:itemId/asignar — reasignar lead a otro vendedor (solo admin) */ +export async function asignarLeadEndpoint(req: Request, res: Response): Promise { + const itemId = String(req.params['itemId']) + const { usuarioId } = asignarLeadSchema.parse(req.body) + const usuario = await prisma.usuario.findUnique({ + where: { id: usuarioId }, + select: { mondayUserId: true, nombre: true }, + }) + if (!usuario) throw new NotFoundError('Usuario no encontrado') + // Actualizar LeadCaptura + await prisma.leadCaptura.updateMany({ + where: { leadId: itemId }, + data: { usuarioId }, + }) + // En Monday: determinar si es lead u oportunidad + const row = await prisma.leadCaptura.findUnique({ where: { leadId: itemId } }) + const board: 'leads' | 'oportunidades' = row?.board === 'OPORTUNIDADES' ? 'oportunidades' : 'leads' + if (usuario.mondayUserId != null && saludMonday.estaDisponible() && !esIdLocal(itemId)) { + try { + await automatizacionesService.asignarACapturista(itemId, row?.nombreLead ?? '', usuario.mondayUserId, board) + } catch (err) { + if (!esFalloMonday(err)) throw err + await encolarSyncSeguro('ASIGNAR_PERSONA', { board, mondayUserId: usuario.mondayUserId }, itemId) + } + } else if (usuario.mondayUserId != null) { + await encolarSyncSeguro('ASIGNAR_PERSONA', { board, mondayUserId: usuario.mondayUserId }, itemId) + } + res.json({ itemId, usuarioId, nombre: usuario.nombre }) + auditoria.registrar(req.usuarioToken?.sub ?? 'sistema', 'ASIGNAR_LEAD', 'lead', itemId, usuarioId) +} + +const moverLeadSchema = z.object({ + board: z.enum(['LEADS', 'OPORTUNIDADES']), +}) + +/** POST /api/leads/:itemId/mover — mover lead a otro tablero (solo admin) */ +export async function moverLeadEndpoint(req: Request, res: Response): Promise { + const itemId = String(req.params['itemId']) + const { board } = moverLeadSchema.parse(req.body) + const row = await prisma.leadCaptura.findUnique({ where: { leadId: itemId } }) + if (!row) throw new NotFoundError('Lead no encontrado') + await prisma.leadCaptura.update({ + where: { leadId: itemId }, + data: { board }, + }) + res.json({ itemId, board }) + auditoria.registrar(req.usuarioToken?.sub ?? 'sistema', 'MOVER_LEAD', 'lead', itemId, board) +} + /** * GET /api/leads/buscar?q=texto — autocompletado para prellenar el alta de * clientes. Busca en AMBOS tableros (Leads + Oportunidades); q mínimo 2 diff --git a/api-gateway/src/routes/index.ts b/api-gateway/src/routes/index.ts index 2c2b29f..c76b214 100644 --- a/api-gateway/src/routes/index.ts +++ b/api-gateway/src/routes/index.ts @@ -58,6 +58,9 @@ import { buscarLeadsEndpoint, convertirLeadEndpoint, descartarLeadEndpoint, + eliminarLeadEndpoint, + asignarLeadEndpoint, + moverLeadEndpoint, } from '../controllers/leads.controller.js' import { actualizarRolEndpoint, @@ -230,6 +233,10 @@ apiRouter.post('/leads/:itemId/estado', cambiarEstadoLeadEndpoint) apiRouter.post('/leads/:itemId/descartar', descartarLeadEndpoint) // Botón "Agendo" de Mis leads: convierte el lead en oportunidad (Agendo Cita) apiRouter.post('/leads/:itemId/convertir', convertirLeadEndpoint) +// Admin CRUD de leads +apiRouter.delete('/leads/:itemId', requireAdmin, eliminarLeadEndpoint) +apiRouter.patch('/leads/:itemId/asignar', requireAdmin, asignarLeadEndpoint) +apiRouter.post('/leads/:itemId/mover', requireAdmin, moverLeadEndpoint) // Alta Afluencia (forma completa de /client): vive en el tablero Oportunidades apiRouter.post('/afluencias', crearAfluenciaEndpoint) diff --git a/api-gateway/src/services/automatizaciones.service.ts b/api-gateway/src/services/automatizaciones.service.ts index 7bff527..ad52dad 100644 --- a/api-gateway/src/services/automatizaciones.service.ts +++ b/api-gateway/src/services/automatizaciones.service.ts @@ -1,4 +1,5 @@ import { config } from '../config/index.js' +import * as configService from './config.service.js' import { AppError } from '../utils/errors.js' import { actividadLocalService } from './actividad-local.service.js' import { @@ -325,6 +326,10 @@ export class AutomatizacionesService { * pasada a un tablero (rutas de los webhooks create_item). */ async runRoundRobin(soloItemId?: string, board?: TableroRoundRobin): Promise { + // Round-robin deshabilitado vía config (default false en seed) + if (soloItemId === undefined && configService.obtenerConfigSync('round_robin_habilitado') !== 'true') { + return { asignados: 0, detalle: [] } + } // Modo local: Monday caído → round-robin sobre tablas locales if (!saludMonday.estaDisponible() && soloItemId === undefined) { return this.runRoundRobinLocal() @@ -653,6 +658,10 @@ export class AutomatizacionesService { * Crea ActividadLocal de SEGUIMIENTO y actualiza LeadCaptura.usuarioId. */ async runRoundRobinLocal(): Promise { + // Round-robin deshabilitado vía config (default false en seed) + if (configService.obtenerConfigSync('round_robin_habilitado') !== 'true') { + return { asignados: 0, detalle: [] } + } // Leads locales sin asignar const leadsSinAsignar = await prisma.leadCaptura.findMany({ where: { diff --git a/api-gateway/src/services/contactos-local.service.ts b/api-gateway/src/services/contactos-local.service.ts index eb9e541..aaaaf59 100644 --- a/api-gateway/src/services/contactos-local.service.ts +++ b/api-gateway/src/services/contactos-local.service.ts @@ -17,11 +17,19 @@ export type ContactoListaLocal = ContactoMonday & { fuente?: 'local' } * Lista local: Clientes (alta de clientes) + capturas del embudo en boards * OPORTUNIDADES/APARTADO, deduplicados por teléfono (gana el Cliente: tiene la * ficha completa), más reciente primero, máx 100 (como getContactos). + * Filtra por usuario actual: clientes capturados por él + leads asignados a él. */ -export async function listarContactosLocal(): Promise { - const clientes = await prisma.cliente.findMany({ orderBy: { createdAt: 'desc' }, take: 100 }) +export async function listarContactosLocal(usuarioId?: string): Promise { + const clientes = await prisma.cliente.findMany({ + where: usuarioId ? { capturadoPorId: usuarioId } : {}, + orderBy: { createdAt: 'desc' }, + take: 100, + }) const capturas = await prisma.leadCaptura.findMany({ - where: { board: { in: ['OPORTUNIDADES', 'APARTADO'] } }, + where: { + board: { in: ['OPORTUNIDADES', 'APARTADO'] }, + ...(usuarioId ? { usuarioId } : {}), + }, orderBy: { updatedAt: 'desc' }, take: 100, }) diff --git a/api-gateway/src/services/crm-local.service.ts b/api-gateway/src/services/crm-local.service.ts index 7ffae48..043c678 100644 --- a/api-gateway/src/services/crm-local.service.ts +++ b/api-gateway/src/services/crm-local.service.ts @@ -350,10 +350,21 @@ export class CrmLocalService { await this.filaLocal(itemId, board, opciones.usuarioId, opciones.sombra ?? false) const payload: Record = { board: BOARD_PAYLOAD[board], label } if (opciones.fecha !== undefined) payload['fecha'] = opciones.fecha + // Guardar fechaCita en datosJson para que la oportunidad la muestre en el calendario + const updateData: Record = { etapa: label, pendienteSync: true } + if (opciones.fecha !== undefined && board === 'OPORTUNIDADES') { + const existente = await prisma.leadCaptura.findUnique({ where: { leadId: itemId }, select: { datosJson: true } }) + let datos: Record = {} + if (existente?.datosJson) { + try { datos = JSON.parse(existente.datosJson) as Record } catch { /* ignorar */ } + } + datos['fechaCita'] = opciones.fecha + updateData['datosJson'] = JSON.stringify(datos) + } await prisma.$transaction([ prisma.leadCaptura.update({ where: { leadId: itemId }, - data: { etapa: label, pendienteSync: true }, + data: updateData, }), prisma.syncPendiente.create({ data: { tipo: 'CAMBIO_ETAPA', payloadJson: JSON.stringify(payload), referenciaLocal: itemId }, diff --git a/api-gateway/src/services/mi-dia.service.ts b/api-gateway/src/services/mi-dia.service.ts index bcb6ae2..6eaf60a 100644 --- a/api-gateway/src/services/mi-dia.service.ts +++ b/api-gateway/src/services/mi-dia.service.ts @@ -117,6 +117,8 @@ export interface OportunidadMiDia { updatedAt: string /** Último evento del activity log del board; null si el item no tiene eventos */ ultimoCambio: { fechaISO: string; usuario: string | null; detalle: string } | null + /** Fecha y hora de la cita agendada (ej. '2026-09-10T14:30' o '2026-09-10') */ + fechaCita?: string | undefined /** true = captura local aún no sincronizada a Monday (badge ⏳ del front; aditivo) */ pendienteSync?: boolean } @@ -319,6 +321,7 @@ export async function resumenMiDia( unidadCodigo: o.unidadCodigo, updatedAt: o.updatedAt, ultimoCambio: null, + fechaCita: o.fechaCita, })) .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)) @@ -681,11 +684,13 @@ export function resumenMiDiaLocal( .filter((c) => !esEtapaTerminal(c.etapa)) .map((c) => { let unidadCodigo = '' + let fechaCita: string | undefined if (c.datosJson) { try { const datos = JSON.parse(c.datosJson) as Record if (typeof datos.unidadCodigo === 'string' && datos.unidadCodigo !== '') unidadCodigo = datos.unidadCodigo else if (typeof datos.desarrollo === 'string' && datos.desarrollo !== '') unidadCodigo = datos.desarrollo + if (typeof datos.fechaCita === 'string') fechaCita = datos.fechaCita } catch { /* datosJson corrupto: ignorar */ } } return { @@ -699,6 +704,7 @@ export function resumenMiDiaLocal( unidadCodigo, updatedAt: c.updatedAt.toISOString(), ultimoCambio: ultimoCambioLocal(c, usuario.nombre), + fechaCita, pendienteSync: c.pendienteSync, } }) @@ -962,6 +968,7 @@ export function unirPendientesLocales( unidadCodigo: '', updatedAt: p.updatedAt.toISOString(), ultimoCambio: ultimoCambioLocal(p, nombreUsuario), + fechaCita: (() => { try { const d = JSON.parse(p.datosJson ?? '{}') as Record; return typeof d.fechaCita === 'string' ? d.fechaCita : undefined } catch { return undefined } })(), pendienteSync: true, }) } diff --git a/api-gateway/src/services/monday.service.ts b/api-gateway/src/services/monday.service.ts index fd7bc48..82908f9 100644 --- a/api-gateway/src/services/monday.service.ts +++ b/api-gateway/src/services/monday.service.ts @@ -128,6 +128,7 @@ export interface OportunidadMonday { califica?: string // dropdown 'Lead Califica' rangoSalarial?: string // text 'Rango Salarial' loQuierePara?: string // dropdown 'Proposito de Compra' + fechaCita?: string | undefined // text 'Fecha' — fecha/hora de la cita agendada } /** Usuario del workspace (para la lista rotativa de vendedores) */ @@ -1116,9 +1117,11 @@ export class MondayService { califica: textoDe('Lead Califica'), rangoSalarial: textoDe('Rango Salarial'), loQuierePara: textoDe('Proposito de Compra'), + fechaCita: textoDe('Fecha') || undefined, } } + /** * Oportunidad con el mismo teléfono (dedup de la captura de afluencia, que * ahora vive en Oportunidades). Compara solo dígitos y los últimos 10 diff --git a/apps/agente-ia/index.html b/apps/agente-ia/index.html index 5d49b99..866701a 100644 --- a/apps/agente-ia/index.html +++ b/apps/agente-ia/index.html @@ -6,7 +6,7 @@ - FRASA Desarrollos — Asistente IA + CRM360 FRASA — Asistente IA
diff --git a/apps/agente-ia/public/manifest.webmanifest b/apps/agente-ia/public/manifest.webmanifest index bfe5e13..e80537e 100644 --- a/apps/agente-ia/public/manifest.webmanifest +++ b/apps/agente-ia/public/manifest.webmanifest @@ -1,5 +1,5 @@ { - "name": "Frasa · Asistente IA", + "name": "CRM360 FRASA · Asistente IA", "short_name": "Asistente IA", "start_url": "/ai/", "scope": "/ai/", diff --git a/apps/agente-ia/src/components/LoginScreen.tsx b/apps/agente-ia/src/components/LoginScreen.tsx index d2a107c..22279d5 100644 --- a/apps/agente-ia/src/components/LoginScreen.tsx +++ b/apps/agente-ia/src/components/LoginScreen.tsx @@ -26,7 +26,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes

- FRASA. + CRM360 FRASA.

Acceso al ecosistema diff --git a/apps/alta-clientes/index.html b/apps/alta-clientes/index.html index 5f80773..a6dbb1b 100644 --- a/apps/alta-clientes/index.html +++ b/apps/alta-clientes/index.html @@ -6,7 +6,7 @@ - FRASA Desarrollos — Alta de Clientes + CRM360 FRASA — Alta de Clientes

diff --git a/apps/alta-clientes/public/manifest.webmanifest b/apps/alta-clientes/public/manifest.webmanifest index 54df02e..1e125e6 100644 --- a/apps/alta-clientes/public/manifest.webmanifest +++ b/apps/alta-clientes/public/manifest.webmanifest @@ -1,5 +1,5 @@ { - "name": "Frasa · Alta de clientes", + "name": "CRM360 FRASA · Alta de clientes", "short_name": "Alta clientes", "start_url": "/client/", "scope": "/client/", diff --git a/apps/alta-clientes/src/components/LoginScreen.tsx b/apps/alta-clientes/src/components/LoginScreen.tsx index 06c5b0e..8eab505 100644 --- a/apps/alta-clientes/src/components/LoginScreen.tsx +++ b/apps/alta-clientes/src/components/LoginScreen.tsx @@ -25,7 +25,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
-

FRASA.

+

CRM360 FRASA.

Acceso al ecosistema

diff --git a/apps/alta-clientes/src/pages/AltaClientePage.tsx b/apps/alta-clientes/src/pages/AltaClientePage.tsx index 8d04bf3..b919e10 100644 --- a/apps/alta-clientes/src/pages/AltaClientePage.tsx +++ b/apps/alta-clientes/src/pages/AltaClientePage.tsx @@ -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 (
-

diff --git a/apps/cobranza/index.html b/apps/cobranza/index.html index 8b8ba2f..56c5bc1 100644 --- a/apps/cobranza/index.html +++ b/apps/cobranza/index.html @@ -6,7 +6,7 @@ - FRASA Desarrollos — Clientes + CRM360 FRASA — Clientes

diff --git a/apps/cobranza/public/manifest.webmanifest b/apps/cobranza/public/manifest.webmanifest index dea4c27..6e754a6 100644 --- a/apps/cobranza/public/manifest.webmanifest +++ b/apps/cobranza/public/manifest.webmanifest @@ -1,5 +1,5 @@ { - "name": "Frasa · Clientes", + "name": "CRM360 FRASA · Clientes", "short_name": "Clientes", "start_url": "/pay/", "scope": "/pay/", diff --git a/apps/cobranza/src/components/LoginScreen.tsx b/apps/cobranza/src/components/LoginScreen.tsx index 1c80ce7..e7fe963 100644 --- a/apps/cobranza/src/components/LoginScreen.tsx +++ b/apps/cobranza/src/components/LoginScreen.tsx @@ -26,7 +26,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes

- FRASA. + CRM360 FRASA.

Acceso al ecosistema diff --git a/apps/cotizador/index.html b/apps/cotizador/index.html index e653a85..69dedaa 100644 --- a/apps/cotizador/index.html +++ b/apps/cotizador/index.html @@ -7,7 +7,7 @@ - FRASA Desarrollos — Cotizador + CRM360 FRASA — Cotizador

diff --git a/apps/cotizador/public/manifest.json b/apps/cotizador/public/manifest.json index 119022e..91947b4 100644 --- a/apps/cotizador/public/manifest.json +++ b/apps/cotizador/public/manifest.json @@ -1,6 +1,6 @@ { - "name": "FRASA CRM", - "short_name": "FRASA", + "name": "CRM360 FRASA", + "short_name": "CRM360", "start_url": "/quote/", "display": "standalone", "background_color": "#F8FAFC", diff --git a/apps/cotizador/public/manifest.webmanifest b/apps/cotizador/public/manifest.webmanifest index 55712cb..5d601d3 100644 --- a/apps/cotizador/public/manifest.webmanifest +++ b/apps/cotizador/public/manifest.webmanifest @@ -1,5 +1,5 @@ { - "name": "Frasa · Cotizador", + "name": "CRM360 FRASA · Cotizador", "short_name": "Cotizador", "start_url": "/quote/", "scope": "/quote/", diff --git a/apps/cotizador/src/components/LoginScreen.tsx b/apps/cotizador/src/components/LoginScreen.tsx index 55d3dc6..af19d71 100644 --- a/apps/cotizador/src/components/LoginScreen.tsx +++ b/apps/cotizador/src/components/LoginScreen.tsx @@ -26,7 +26,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes

- FRASA. + CRM360 FRASA.

Acceso al ecosistema diff --git a/apps/cotizador/src/pages/CotizadorPage.tsx b/apps/cotizador/src/pages/CotizadorPage.tsx index 88c8270..e3c8b97 100644 --- a/apps/cotizador/src/pages/CotizadorPage.tsx +++ b/apps/cotizador/src/pages/CotizadorPage.tsx @@ -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 (

-
{/* Encabezado de página */}
diff --git a/apps/dashboards/index.html b/apps/dashboards/index.html index 401b807..b6a92f3 100644 --- a/apps/dashboards/index.html +++ b/apps/dashboards/index.html @@ -6,7 +6,7 @@ - FRASA Desarrollos — Tableros + CRM360 FRASA — Tableros
diff --git a/apps/dashboards/public/manifest.webmanifest b/apps/dashboards/public/manifest.webmanifest index 598ae70..dc42e92 100644 --- a/apps/dashboards/public/manifest.webmanifest +++ b/apps/dashboards/public/manifest.webmanifest @@ -1,5 +1,5 @@ { - "name": "Frasa · Tableros", + "name": "CRM360 FRASA · Tableros", "short_name": "Tableros", "start_url": "/dash/", "scope": "/dash/", diff --git a/apps/dashboards/src/components/LoginScreen.tsx b/apps/dashboards/src/components/LoginScreen.tsx index c251f6d..37989fd 100644 --- a/apps/dashboards/src/components/LoginScreen.tsx +++ b/apps/dashboards/src/components/LoginScreen.tsx @@ -26,7 +26,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes

- FRASA. + CRM360 FRASA.

Acceso al ecosistema diff --git a/apps/dashboards/src/pages/DashboardPage.tsx b/apps/dashboards/src/pages/DashboardPage.tsx index aba12a7..47ddcbf 100644 --- a/apps/dashboards/src/pages/DashboardPage.tsx +++ b/apps/dashboards/src/pages/DashboardPage.tsx @@ -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 (

-
diff --git a/apps/landing/index.html b/apps/landing/index.html index ff1e395..342e462 100644 --- a/apps/landing/index.html +++ b/apps/landing/index.html @@ -4,10 +4,12 @@ + + - - FRASA Desarrollos — Ecosistema comercial + + CRM360 FRASA — Ecosistema comercial
diff --git a/apps/landing/public/apple-touch-icon.png b/apps/landing/public/apple-touch-icon.png new file mode 100644 index 0000000..af9b29f Binary files /dev/null and b/apps/landing/public/apple-touch-icon.png differ diff --git a/apps/landing/public/favicon.png b/apps/landing/public/favicon.png new file mode 100644 index 0000000..5653f23 Binary files /dev/null and b/apps/landing/public/favicon.png differ diff --git a/apps/landing/public/logo.png b/apps/landing/public/logo.png new file mode 100644 index 0000000..7ea01ea Binary files /dev/null and b/apps/landing/public/logo.png differ diff --git a/apps/landing/public/manifest.webmanifest b/apps/landing/public/manifest.webmanifest index a2fa0f6..d9c9434 100644 --- a/apps/landing/public/manifest.webmanifest +++ b/apps/landing/public/manifest.webmanifest @@ -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" } ] diff --git a/apps/landing/src/App.tsx b/apps/landing/src/App.tsx index ba23130..ea3d081 100644 --- a/apps/landing/src/App.tsx +++ b/apps/landing/src/App.tsx @@ -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 (
setVista('configuraciones')} /> - + setVista('home')} />
) @@ -59,7 +58,7 @@ export default function App() { return (
setVista('configuraciones')} /> - + setVista('home')} />
) @@ -69,7 +68,7 @@ export default function App() { return (
setVista('configuraciones')} /> - + setVista('home')} />
) @@ -79,7 +78,7 @@ export default function App() { return (
setVista('configuraciones')} /> - + setVista('home')} />
) @@ -88,7 +87,6 @@ export default function App() { return (
setVista('configuraciones')} /> -
@@ -99,7 +97,7 @@ export default function App() { Ecosistema comercial

- 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.

@@ -131,7 +129,7 @@ export default function App() {
-

Consult (AS) Technology

+

CRM360 FRASA · Consult (AS) Technology

diff --git a/apps/landing/src/components/ConfiguracionCuenta.tsx b/apps/landing/src/components/ConfiguracionCuenta.tsx index 2bbcee5..94bb9b6 100644 --- a/apps/landing/src/components/ConfiguracionCuenta.tsx +++ b/apps/landing/src/components/ConfiguracionCuenta.tsx @@ -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(null) const [cargandoPerfil, setCargandoPerfil] = useState(false) - const [usuarios, setUsuarios] = useState(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 (

- Inicia sesión para vincular tu usuario de Monday. + Inicia sesión para acceder a las herramientas del ecosistema.