diff --git a/CLAUDE.md b/CLAUDE.md index 9930ca7..9c2eeda 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -118,8 +118,9 @@ o son lógica pura (sección 5 del archivo, sin servicios de Google, probables e ### 4.4 Datos para la interfaz - `getDatosIniciales()` *(UI, al cargar)* — IVA, vigencia, marcas, catálogo, clientes, `usuario` (correo) y `vendedor` (`{email, nombre, apellido}`). - `getClientes()` *(UI)* — clientes que ya cotizaron, calculados desde la hoja `Cotizaciones` (`clientesDesdeFilas`). -- `listarCotizaciones(busqueda)` *(UI)* — últimas 100 cotizaciones, filtro por folio/cliente/empresa; `vendedor` = nombre + apellido - (filas viejas: `vendedor_email` o "Elaboró"). +- `listarCotizaciones(busqueda)` *(UI)* — últimas 100 cotizaciones, filtro por folio/cliente/empresa/proyecto; `vendedor` = + `vendedorDeFila`: nombre + apellido de la fila; en filas viejas (sin Q:S), el nombre que ese correo tiene en otras filas + (`nombresDeVendedores`, gana la más reciente); si no se conoce, el correo (`vendedor_email` o "Elaboró"). No modifica la hoja. ### 4.5 Guardar cotización - `guardarCotizacion(payload)` *(UI)* — exige vendedor verificado (si no, error de sesión); normaliza, agrega clave/unidad SAT @@ -156,6 +157,7 @@ o son lógica pura (sección 5 del archivo, sin servicios de Google, probables e normalizados. `normalizarProyecto(p)`, `nombreCarpetaProyecto(folio, cliente, nombre)` (quita `/ \ : * ? " < > |`), `proyectosDesdeFilas`, `filtrarProyectos`. - `normalizarProducto(p)`, `indiceDeClave(claves, clave)`. +- `nombresDeVendedores(filas)` (correo en minúsculas → "Nombre Apellido"), `vendedorDeFila(r, nombres)`. - `terminosParaPdf(cfg, marca)` — renglones de T&C de la marca; agrega la línea de vigencia si falta. - `textoCelda(s)` — antepone `'` a texto que empiece con `= + - @` para que Sheets no lo lea como fórmula. - `validarClaimsToken(claims, clientId, dominio, ahora)` — ver §10.3. @@ -196,6 +198,12 @@ del catálogo, claveSat/unidadSat), Notas, Marca (Helgen / Helgen Energy), Direc filas 0001–0004 no las tienen llenas. La T se agregó con Proyectos (10/2026): las filas anteriores quedan vacías y no se mueven. `guardarCotizacion` reescribe la fila de encabezados si no coincide con `ENCABEZADOS_COTIZACIONES`. +**Registros de prueba en la hoja real (excluir de reportes y totales; identificarlos por folio, no por texto):** +- `HLG-2026-0005` — cliente "PRUEBA - CANCELADA" (prueba de la migración, 19/10/2026). +- `HLG-2026-0006` — cliente renombrado a "PRUEBA – CANCELADA" (con guion largo; prueba del primer proyecto, 20/10/2026). +- `PRY-2026-0001` — "Proyecto de Prueba oficial", estado `Cancelado` (20/10/2026). Su carpeta en `Proyectos/2026` es de prueba. +- Las filas HLG-2026-0001 a 0004 son anteriores a la sesión con Google y también parecen pruebas, pero no se marcaron. + **Pestaña `Config`** (Clave | Valor, columna B en formato texto): `EMPRESA_NOMBRE` (Helgen Servicios Especializados), `EMPRESA_RFC` (HSE240304PC1), `EMPRESA_DIRECCION`, `EMPRESA_TELEFONO` (+52 442 644 5391), `EMPRESA_EMAIL`, `EMPRESA_WEB`, `PREFIJO_FOLIO` (HLG), `IVA_TASA` (0.16), `VIGENCIA_DIAS` (15), `MONEDA`, `LOGO_FILE_ID` (vacío = logo de `Logo.js`), diff --git a/Code.js b/Code.js index f650a96..ea21231 100644 --- a/Code.js +++ b/Code.js @@ -582,15 +582,17 @@ function getClientes() { /** Historial (últimas 100, con búsqueda opcional). */ function listarCotizaciones(busqueda) { const hoja = db_().getSheetByName('Cotizaciones'); - const datos = hoja.getDataRange().getDisplayValues().slice(1).reverse(); + const filas = hoja.getDataRange().getDisplayValues().slice(1); + // Correo → "Nombre Apellido" con todas las filas (antes de filtrar), para ponerle nombre a las filas viejas + const nombres = nombresDeVendedores(filas); + const datos = filas.reverse(); const q = String(busqueda || '').toLowerCase().trim(); return datos .filter(r => !q || (r[0] + ' ' + r[2] + ' ' + r[3] + ' ' + (r[19] || '')).toLowerCase().includes(q)) .slice(0, 100) .map(r => ({ folio: r[0], fecha: r[1], cliente: r[2], empresa: r[3], total: r[9], elaboro: r[10], pdf: r[11], - // Filas anteriores a la sesión con Google no tienen vendedor: se muestra el correo de "Elaboró" - vendedor: [r[17], r[18]].filter(Boolean).join(' ') || r[16] || r[10] || '', + vendedor: vendedorDeFila(r, nombres), // Filas anteriores a Proyectos (o cotizadas sin proyecto) proyecto: r[19] || 'Sin proyecto', })); @@ -985,6 +987,31 @@ function clientesDesdeFilas(filas) { return lista; } +/** + * Correo (minúsculas) → "Nombre Apellido", tomado de las filas de Cotizaciones que sí tienen vendedor_nombre / + * vendedor_apellido (columnas Q:S). Filas en orden de la hoja: si un correo aparece con nombres distintos, gana la más reciente. + */ +function nombresDeVendedores(filas) { + const mapa = {}; + (filas || []).forEach(r => { + const correo = String(r[16] || '').trim().toLowerCase(); + const nombre = [r[17], r[18]].map(x => String(x || '').trim()).filter(Boolean).join(' '); + if (correo && nombre) mapa[correo] = nombre; + }); + return mapa; +} + +/** + * Vendedor para el Historial: nombre y apellido de la fila; si no tiene (filas anteriores a la sesión con Google), + * el nombre que ese correo tiene en otras filas; si no se conoce, el correo (vendedor_email o "Elaboró"). + */ +function vendedorDeFila(r, nombres) { + const propio = [r[17], r[18]].map(x => String(x || '').trim()).filter(Boolean).join(' '); + if (propio) return propio; + const correo = String(r[16] || r[10] || '').trim(); + return (nombres || {})[correo.toLowerCase()] || correo; +} + /** Texto sin acentos, en minúsculas y con espacios simples (para comparar nombres). */ function textoNormal(s) { return String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '') diff --git a/tests/sesion.test.js b/tests/sesion.test.js index 1f0a7c4..4142958 100644 --- a/tests/sesion.test.js +++ b/tests/sesion.test.js @@ -84,6 +84,19 @@ assert.equal(post({funcion:'guardarCotizacion',args:[{...base,idSolicitud:'ok1'} // --- Historial con el nombre del vendedor; filas viejas muestran el correo de "Elaboró" --- const h=post({funcion:'listarCotizaciones',args:[''],token:'t-bueno'}).datos; assert.equal(h[0].vendedor,'Ana López Díaz'); assert.equal(h[1].vendedor,'antes@helgen.tech'); +// Filas viejas sin nombre: si su correo aparece con nombre en otra fila, se muestra ese nombre (sin tocar la hoja) +const vieja=['HLG-2026-0000','x','Vieja','','','','',1,0.16,1.16,'ANA.LOPEZ@helgen.tech','u','[]','','Helgen','']; +cot.m.splice(1,0,vieja.slice()); +const h2=post({funcion:'listarCotizaciones',args:[''],token:'t-bueno'}).datos; +assert.equal(h2.find(c=>c.folio==='HLG-2026-0000').vendedor,'Ana López Díaz','correo conocido (sin importar mayúsculas) = nombre'); +assert.equal(h2.find(c=>c.folio==='HLG-2026-0001').vendedor,'antes@helgen.tech','correo sin nombre conocido = correo'); +assert.deepEqual(cot.m[1],vieja,'la hoja no se modifica'); +cot.m.splice(1,1); +// Pura: si un correo tiene nombres distintos, gana la fila más reciente (la de más abajo) +const Q=(email,n,a)=>{const r=Array(19).fill('');r[16]=email;r[17]=n;r[18]=a;return r}; +assert.equal(ctx.nombresDeVendedores([Q('a@helgen.tech','Ana','Viejo'),Q('A@helgen.tech','Ana','Nuevo'),Q('b@helgen.tech','','')])['a@helgen.tech'],'Ana Nuevo'); +assert.equal(ctx.nombresDeVendedores([Q('b@helgen.tech','','')])['b@helgen.tech'],undefined,'sin nombre no entra al mapa'); +assert.equal(ctx.vendedorDeFila(Q('','',''),{}),'','fila sin datos de vendedor'); // Datos iniciales: usuario = correo del token const d=post({funcion:'getDatosIniciales',args:[],token:'t-bueno'}).datos; assert.equal(d.usuario,'ana.lopez@helgen.tech'); assert.equal(d.vendedor.nombre,'Ana');